Merge remote branch 'refs/remotes/canonical/master' into design-mapper-more-auto-update-design-doc-aware

This commit is contained in:
Peter Williams 2011-11-29 08:22:30 -07:00
commit 88bb413ec2
66 changed files with 866 additions and 423 deletions

View file

@ -70,6 +70,10 @@ The example config above for example would use a database called "project_test".
## Generators ## Generators
### Configuration
$ rails generate couchrest_model:config
### Model ### Model
$ rails generate model person --orm=couchrest_model $ rails generate model person --orm=couchrest_model

View file

@ -1 +1 @@
1.1.0.beta5 1.1.2

View file

@ -6,7 +6,7 @@ Gem::Specification.new do |s|
s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version= s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
s.authors = ["J. Chris Anderson", "Matt Aimonetti", "Marcos Tapajos", "Will Leinweber", "Sam Lown"] s.authors = ["J. Chris Anderson", "Matt Aimonetti", "Marcos Tapajos", "Will Leinweber", "Sam Lown"]
s.date = %q{2011-04-29} s.date = File.mtime('VERSION')
s.description = %q{CouchRest Model provides aditional features to the standard CouchRest Document class such as properties, view designs, associations, callbacks, typecasting and validations.} s.description = %q{CouchRest Model provides aditional features to the standard CouchRest Document class such as properties, view designs, associations, callbacks, typecasting and validations.}
s.email = %q{jchris@apache.org} s.email = %q{jchris@apache.org}
s.extra_rdoc_files = [ s.extra_rdoc_files = [
@ -23,12 +23,14 @@ Gem::Specification.new do |s|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"] s.require_paths = ["lib"]
s.add_dependency(%q<couchrest>, "1.1.0.pre2") s.add_dependency(%q<couchrest>, "~> 1.1.2")
s.add_dependency(%q<mime-types>, "~> 1.15") s.add_dependency(%q<mime-types>, "~> 1.15")
s.add_dependency(%q<activemodel>, "~> 3.0") s.add_dependency(%q<activemodel>, "~> 3.0")
s.add_dependency(%q<tzinfo>, "~> 0.3.22") s.add_dependency(%q<tzinfo>, "~> 0.3.22")
s.add_development_dependency(%q<rspec>, ">= 2.0.0") s.add_development_dependency(%q<rspec>, "~> 2.6.0")
s.add_development_dependency(%q<json>, ["~> 1.5.1"])
s.add_development_dependency(%q<rack-test>, ">= 0.5.7") s.add_development_dependency(%q<rack-test>, ">= 0.5.7")
s.add_development_dependency("rake", ">= 0.8.0")
# s.add_development_dependency("jruby-openssl", ">= 0.7.3") # s.add_development_dependency("jruby-openssl", ">= 0.7.3")
end end

View file

@ -1,10 +1,41 @@
# CouchRest Model Change History # CouchRest Model Change History
## 1.1.0 - 2011-05-XX ## 1.1.3
* CouchRest::Model::Base.respond_to_missing? and respond_to? (Kim Burgestrand)
## 1.1.2 - 2011-07-23
* Minor fixes
* Upgrade to couchrest 1.1.2
* Override as_couch_json to ensure nil values not stored
* Removing restriction that prohibited objects that cast as an array to be loaded.
## 1.1.1 - 2011-07-04
* Minor fix
* Bumping CouchRest version dependency for important initialize method fix.
* Ensuring super on Embeddable#initialize can be called.
## 1.1.0 - 2011-06-25
* Major Alterations
* CastedModel no longer requires a Hash. Automatically includes all required methods.
* CastedModel module renamed to Embeddable (old still works!)
* Minor Fixes
* Validation callbacks now support context (thanks kostia)
* Document comparisons now performed using database and document ID (pointer by neocsr)
* Automatic config generation now supported (thanks lucasrenan)
* Comparing documents resorts to Hash comparison if both IDs are nil. (pointer by kostia)
## 1.1.0.rc1 - 2011-06-08
* New Features * New Features
* Properties with a nil value are now no longer sent to the database. * Properties with a nil value are now no longer sent to the database.
* Now possible to build new objects via CastedArray#build * Now possible to build new objects via CastedArray#build
* Implement #get! and #find! class methods
* Now is possible delete particular elements in casted array(Kostiantyn Kahanskyi)
* Minor fixes * Minor fixes
* #as_json now correctly uses ActiveSupports methods. * #as_json now correctly uses ActiveSupports methods.
@ -14,7 +45,12 @@
* DesignDoc cache refreshed if a database is deleted. * DesignDoc cache refreshed if a database is deleted.
* Fixing dirty tracking on collection_of association. * Fixing dirty tracking on collection_of association.
* Uniqueness Validation views created on initialization, not on demand! * Uniqueness Validation views created on initialization, not on demand!
* #destroy freezes object instead of removing _id and _rev, better for callbacks (pointer by karmi)
* #destroyed? method now available
* #reload no longer uses Hash#merge! which was causing issues with dirty tracking on casted models. (pointer by kostia)
* Non-property mass assignment on #new no longer possible without :directly_set_attributes option.
* Using CouchRest 1.1.0.pre3. (No more Hashes!)
* Fixing problem assigning a CastedHash to a property declared as a Hash (Kostiantyn Kahanskyi, gfmtim)
## 1.1.0.beta5 - 2011-04-30 ## 1.1.0.beta5 - 2011-04-30

View file

@ -1,13 +1,12 @@
module CouchRest module CouchRest
module Model module Model
class Base < Document class Base < CouchRest::Document
extend ActiveModel::Naming extend ActiveModel::Naming
include CouchRest::Model::Configuration include CouchRest::Model::Configuration
include CouchRest::Model::Connection include CouchRest::Model::Connection
include CouchRest::Model::Persistence include CouchRest::Model::Persistence
include CouchRest::Model::Callbacks
include CouchRest::Model::DocumentQueries include CouchRest::Model::DocumentQueries
include CouchRest::Model::Views include CouchRest::Model::Views
include CouchRest::Model::DesignDoc include CouchRest::Model::DesignDoc
@ -18,9 +17,11 @@ module CouchRest
include CouchRest::Model::PropertyProtection include CouchRest::Model::PropertyProtection
include CouchRest::Model::Associations include CouchRest::Model::Associations
include CouchRest::Model::Validations include CouchRest::Model::Validations
include CouchRest::Model::Callbacks
include CouchRest::Model::Designs include CouchRest::Model::Designs
include CouchRest::Model::CastedBy include CouchRest::Model::CastedBy
include CouchRest::Model::Dirty include CouchRest::Model::Dirty
include CouchRest::Model::Callbacks
def self.subclasses def self.subclasses
@subclasses ||= [] @subclasses ||= []
@ -46,22 +47,24 @@ module CouchRest
# #
# Options supported: # Options supported:
# #
# * :directly_set_attributes: true when data comes directly from database # * :directly_set_attributes, true when data comes directly from database
# * :database: provide an alternative database # * :database, provide an alternative database
# #
# If a block is provided the new model will be passed into the # If a block is provided the new model will be passed into the
# block so that it can be populated. # block so that it can be populated.
def initialize(doc = {}, options = {}) def initialize(attributes = {}, options = {})
doc = prepare_all_attributes(doc, options) super()
# set the instances database, if provided prepare_all_attributes(attributes, options)
# set the instance's database, if provided
self.database = options[:database] unless options[:database].nil? self.database = options[:database] unless options[:database].nil?
super(doc)
unless self['_id'] && self['_rev'] unless self['_id'] && self['_rev']
self[self.model_type_key] = self.class.to_s self[self.model_type_key] = self.class.to_s
end end
yield self if block_given? yield self if block_given?
after_initialize if respond_to?(:after_initialize) after_initialize if respond_to?(:after_initialize)
run_callbacks(:initialize) { self }
end end
@ -79,18 +82,19 @@ module CouchRest
super super
end end
## Compatibility with ActiveSupport and older frameworks # compatbility for 1.8, it does not use respond_to_missing?
# thing is, when using it like this only, doing method(:find_by_view)
# Hack so that CouchRest::Document, which descends from Hash, # will throw an error
# doesn't appear to Rails routing as a Hash of options def self.respond_to?(m, include_private = false)
def is_a?(klass) super || respond_to_missing?(m, include_private)
return false if klass == Hash
super
end end
alias :kind_of? :is_a?
def persisted? # ruby 1.9 feature
!new? # this allows ruby to know that the method is defined using
# method_missing, and as such, method(:find_by_view) will actually
# give a Method back, and not throw an error like in 1.8!
def self.respond_to_missing?(m, include_private = false)
has_view?(m) || has_view?(m.to_s[/^find_(by_.+)/, 1])
end end
def to_key def to_key
@ -100,6 +104,26 @@ module CouchRest
alias :to_param :id alias :to_param :id
alias :new_record? :new? alias :new_record? :new?
alias :new_document? :new? alias :new_document? :new?
# Compare this model with another by confirming to see
# if the IDs and their databases match!
#
# Camparison of the database is required in case the
# model has been proxied or loaded elsewhere.
#
# A Basic CouchRest document will only ever compare using
# a Hash comparison on the attributes.
def == other
return false unless other.is_a?(Base)
if id.nil? && other.id.nil?
# no ids? assume comparing nested and revert to hash comparison
to_hash == other.to_hash
else
database == other.database && id == other.id
end
end
alias :eql? :==
end end
end end
end end

View file

@ -5,21 +5,23 @@ module CouchRest #:nodoc:
module Callbacks module Callbacks
extend ActiveSupport::Concern extend ActiveSupport::Concern
CALLBACKS = [
:before_validation, :after_validation,
:after_initialize,
:before_create, :around_create, :after_create,
:before_destroy, :around_destroy, :after_destroy,
:before_save, :around_save, :after_save,
:before_update, :around_update, :after_update,
]
included do included do
extend ActiveModel::Callbacks extend ActiveModel::Callbacks
include ActiveModel::Validations::Callbacks
define_model_callbacks \ define_model_callbacks :initialize, :only => :after
:create, define_model_callbacks :create, :destroy, :save, :update
:destroy,
:save,
:update
end end
def valid?(*) #nodoc
_run_validation_callbacks { super }
end
end end
end end

View file

@ -50,6 +50,16 @@ module CouchRest::Model
super super
end end
def delete(obj)
couchrest_parent_will_change! if use_dirty? && self.length > 0
super(obj)
end
def delete_at(index)
couchrest_parent_will_change! if use_dirty? && self.length > 0
super(index)
end
def build(*args) def build(*args)
obj = casted_by_property.build(*args) obj = casted_by_property.build(*args)
self.push(obj) self.push(obj)

View file

@ -244,6 +244,7 @@ module CouchRest
else else
options = { :limit => per_page, :skip => per_page * (page - 1) } options = { :limit => per_page, :skip => per_page * (page - 1) }
end end
options[:include_docs] = true
view_options.merge(options) view_options.merge(options)
end end

View file

@ -1,10 +1,7 @@
module CouchRest module CouchRest
module Model module Model
module DocumentQueries module DocumentQueries
extend ActiveSupport::Concern
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods module ClassMethods

View file

@ -1,37 +1,37 @@
module CouchRest::Model module CouchRest::Model
module CastedModel module Embeddable
extend ActiveSupport::Concern extend ActiveSupport::Concern
# Include Attributes early to ensure super() will work
include CouchRest::Attributes
included do included do
include CouchRest::Model::Configuration include CouchRest::Model::Configuration
include CouchRest::Model::Callbacks
include CouchRest::Model::Properties include CouchRest::Model::Properties
include CouchRest::Model::PropertyProtection include CouchRest::Model::PropertyProtection
include CouchRest::Model::Associations include CouchRest::Model::Associations
include CouchRest::Model::Validations include CouchRest::Model::Validations
include CouchRest::Model::Callbacks
include CouchRest::Model::CastedBy include CouchRest::Model::CastedBy
include CouchRest::Model::Dirty include CouchRest::Model::Dirty
include CouchRest::Model::Callbacks
class_eval do class_eval do
# Override CastedBy's base_doc? # Override CastedBy's base_doc?
def base_doc? def base_doc?
false # Can never be base doc! false # Can never be base doc!
end end
end end
end end
def initialize(keys = {}) # Initialize a new Casted Model. Accepts the same
raise StandardError unless self.is_a? Hash # options as CouchRest::Model::Base for preparing and initializing
prepare_all_attributes(keys) # attributes.
def initialize(keys = {}, options = {})
super() super()
end prepare_all_attributes(keys, options)
run_callbacks(:initialize) { self }
def []= key, value
super(key.to_s, value)
end
def [] key
super(key.to_s)
end end
# False if the casted model has already # False if the casted model has already
@ -65,6 +65,14 @@ module CouchRest::Model
end end
alias :attributes= :update_attributes_without_saving alias :attributes= :update_attributes_without_saving
end # End Embeddable
# Provide backwards compatability with previous versions (pre 1.1.0)
module CastedModel
extend ActiveSupport::Concern
included do
include CouchRest::Model::Embeddable
end
end end
end end

View file

@ -21,14 +21,15 @@ module CouchRest
# Creates the document in the db. Raises an exception # Creates the document in the db. Raises an exception
# if the document is not created properly. # if the document is not created properly.
def create! def create!(options = {})
self.class.fail_validate!(self) unless self.create self.class.fail_validate!(self) unless self.create(options)
end end
# Trigger the callbacks (before, after, around) # Trigger the callbacks (before, after, around)
# only if the document isn't new # only if the document isn't new
def update(options = {}) def update(options = {})
raise "Calling #{self.class.name}#update on document that has not been created!" if self.new? raise "Cannot save a destroyed document!" if destroyed?
raise "Calling #{self.class.name}#update on document that has not been created!" if new?
return false unless perform_validations(options) return false unless perform_validations(options)
return true if !self.disable_dirty && !self.changed? return true if !self.disable_dirty && !self.changed?
_run_update_callbacks do _run_update_callbacks do
@ -54,19 +55,25 @@ module CouchRest
end end
# Deletes the document from the database. Runs the :destroy callbacks. # Deletes the document from the database. Runs the :destroy callbacks.
# Removes the <tt>_id</tt> and <tt>_rev</tt> fields, preparing the
# document to be saved to a new <tt>_id</tt> if required.
def destroy def destroy
_run_destroy_callbacks do _run_destroy_callbacks do
result = database.delete_doc(self) result = database.delete_doc(self)
if result['ok'] if result['ok']
self.delete('_rev') @_destroyed = true
self.delete('_id') self.freeze
end end
result['ok'] result['ok']
end end
end end
def destroyed?
!!@_destroyed
end
def persisted?
!new? && !destroyed?
end
# Update the document's attributes and save. For example: # Update the document's attributes and save. For example:
# #
# doc.update_attributes :name => "Fred" # doc.update_attributes :name => "Fred"
@ -85,7 +92,7 @@ module CouchRest
# #
# Returns self. # Returns self.
def reload def reload
merge!(self.class.get(id)) prepare_all_attributes(database.get(id), :directly_set_attributes => true)
self self
end end
@ -104,22 +111,25 @@ module CouchRest
module ClassMethods module ClassMethods
# Creates a new instance, bypassing attribute protection # Creates a new instance, bypassing attribute protection and
# uses the type field to determine which model to use to instanatiate
# the new object.
# #
# ==== Returns # ==== Returns
# a document instance # a document instance
# #
def build_from_database(doc = {}) def build_from_database(doc = {}, options = {}, &block)
base = (doc[model_type_key].blank? || doc[model_type_key] == self.to_s) ? self : doc[model_type_key].constantize src = doc[model_type_key]
base.new(doc, :directly_set_attributes => true) base = (src.blank? || src == self.to_s) ? self : src.constantize
base.new(doc, options.merge(:directly_set_attributes => true), &block)
end end
# Defines an instance and save it directly to the database # Defines an instance and save it directly to the database
# #
# ==== Returns # ==== Returns
# returns the reloaded document # returns the reloaded document
def create(attributes = {}) def create(attributes = {}, &block)
instance = new(attributes) instance = new(attributes, &block)
instance.create instance.create
instance instance
end end
@ -128,8 +138,8 @@ module CouchRest
# #
# ==== Returns # ==== Returns
# returns the reloaded document or raises an exception # returns the reloaded document or raises an exception
def create!(attributes = {}) def create!(attributes = {}, &block)
instance = new(attributes) instance = new(attributes, &block)
instance.create! instance.create!
instance instance
end end

View file

@ -12,8 +12,10 @@ module CouchRest
raise "You can only mixin Properties in a class responding to [] and []=, if you tried to mixin CastedModel, make sure your class inherits from Hash or responds to the proper methods" unless (method_defined?(:[]) && method_defined?(:[]=)) raise "You can only mixin Properties in a class responding to [] and []=, if you tried to mixin CastedModel, make sure your class inherits from Hash or responds to the proper methods" unless (method_defined?(:[]) && method_defined?(:[]=))
end end
def as_json(options = nil) # Provide an attribute hash ready to be sent to CouchDB but with
Hash[self].reject{|k,v| v.nil?}.as_json(options) # all the nil attributes removed.
def as_couch_json
super.delete_if{|k,v| v.nil?}
end end
# Returns the Class properties with their values # Returns the Class properties with their values
@ -80,17 +82,18 @@ module CouchRest
self.disable_dirty = dirty self.disable_dirty = dirty
end end
def prepare_all_attributes(doc = {}, options = {}) def prepare_all_attributes(attrs = {}, options = {})
self.disable_dirty = !!options[:directly_set_attributes] self.disable_dirty = !!options[:directly_set_attributes]
apply_all_property_defaults apply_all_property_defaults
if options[:directly_set_attributes] if options[:directly_set_attributes]
directly_set_read_only_attributes(doc) directly_set_read_only_attributes(attrs)
directly_set_attributes(attrs, true)
else else
doc = remove_protected_attributes(doc) attrs = remove_protected_attributes(attrs)
directly_set_attributes(attrs)
end end
res = doc.nil? ? doc : directly_set_attributes(doc)
self.disable_dirty = false self.disable_dirty = false
res self
end end
def find_property!(property) def find_property!(property)
@ -101,16 +104,13 @@ module CouchRest
# Set all the attributes and return a hash with the attributes # Set all the attributes and return a hash with the attributes
# that have not been accepted. # that have not been accepted.
def directly_set_attributes(hash) def directly_set_attributes(hash, mass_assign = false)
hash.reject do |attribute_name, attribute_value| return if hash.nil?
if self.respond_to?("#{attribute_name}=") hash.reject do |key, value|
self.send("#{attribute_name}=", attribute_value) if self.respond_to?("#{key}=")
true self.send("#{key}=", value)
elsif mass_assign_any_attribute # config option elsif mass_assign || mass_assign_any_attribute
self[attribute_name] = attribute_value self[key] = value
true
else
false
end end
end end
end end
@ -151,15 +151,13 @@ module CouchRest
# These properties are casted as Time objects, so they should always # These properties are casted as Time objects, so they should always
# be set to UTC. # be set to UTC.
def timestamps! def timestamps!
class_eval <<-EOS, __FILE__, __LINE__ property(:updated_at, Time, :read_only => true, :protected => true, :auto_validation => false)
property(:updated_at, Time, :read_only => true, :protected => true, :auto_validation => false) property(:created_at, Time, :read_only => true, :protected => true, :auto_validation => false)
property(:created_at, Time, :read_only => true, :protected => true, :auto_validation => false)
set_callback :save, :before do |object| set_callback :save, :before do |object|
write_attribute('updated_at', Time.now) write_attribute('updated_at', Time.now)
write_attribute('created_at', Time.now) if object.new? write_attribute('created_at', Time.now) if object.new?
end end
EOS
end end
protected protected
@ -170,8 +168,8 @@ module CouchRest
# check if this property is going to casted # check if this property is going to casted
type = options.delete(:type) || options.delete(:cast_as) type = options.delete(:type) || options.delete(:cast_as)
if block_given? if block_given?
type = Class.new(Hash) do type = Class.new do
include CastedModel include Embeddable
end end
if block.arity == 1 # Traditional, with options if block.arity == 1 # Traditional, with options
type.class_eval { yield type } type.class_eval { yield type }
@ -193,42 +191,32 @@ module CouchRest
# defines the getter for the property (and optional aliases) # defines the getter for the property (and optional aliases)
def create_property_getter(property) def create_property_getter(property)
# meth = property.name define_method(property.name) do
class_eval <<-EOS, __FILE__, __LINE__ + 1 read_attribute(property.name)
def #{property.name} end
read_attribute('#{property.name}')
end
EOS
if ['boolean', TrueClass.to_s.downcase].include?(property.type.to_s.downcase) if ['boolean', TrueClass.to_s.downcase].include?(property.type.to_s.downcase)
class_eval <<-EOS, __FILE__, __LINE__ define_method("#{property.name}?") do
def #{property.name}? value = read_attribute(property.name)
value = read_attribute('#{property.name}') !(value.nil? || value == false)
!(value.nil? || value == false) end
end
EOS
end end
if property.alias if property.alias
class_eval <<-EOS, __FILE__, __LINE__ + 1 alias_method(property.alias, property.name.to_sym)
alias #{property.alias.to_sym} #{property.name.to_sym}
EOS
end end
end end
# defines the setter for the property (and optional aliases) # defines the setter for the property (and optional aliases)
def create_property_setter(property) def create_property_setter(property)
property_name = property.name name = property.name
class_eval <<-EOS
def #{property_name}=(value) define_method("#{name}=") do |value|
write_attribute('#{property_name}', value) write_attribute(name, value)
end end
EOS
if property.alias if property.alias
class_eval <<-EOS alias_method "#{property.alias}=", "#{name}="
alias #{property.alias.to_sym}= #{property_name.to_sym}=
EOS
end end
end end

View file

@ -27,19 +27,15 @@ module CouchRest::Model
if value.nil? if value.nil?
value = [] value = []
elsif [Hash, HashWithIndifferentAccess].include?(value.class) elsif [Hash, HashWithIndifferentAccess].include?(value.class)
# Assume provided as a Hash where key is index! # Assume provided as a params hash where key is index
data = value value = parameter_hash_to_array(value)
value = [ ]
data.keys.sort.each do |k|
value << data[k]
end
elsif !value.is_a?(Array) elsif !value.is_a?(Array)
raise "Expecting an array or keyed hash for property #{parent.class.name}##{self.name}" raise "Expecting an array or keyed hash for property #{parent.class.name}##{self.name}"
end end
arr = value.collect { |data| cast_value(parent, data) } arr = value.collect { |data| cast_value(parent, data) }
# allow casted_by calls to be passed up chain by wrapping in CastedArray # allow casted_by calls to be passed up chain by wrapping in CastedArray
CastedArray.new(arr, self, parent) CastedArray.new(arr, self, parent)
elsif (type == Object || type == Hash) && (value.class == Hash) elsif (type == Object || type == Hash) && (value.is_a?(Hash))
# allow casted_by calls to be passed up chain by wrapping in CastedHash # allow casted_by calls to be passed up chain by wrapping in CastedHash
CastedHash[value, self, parent] CastedHash[value, self, parent]
elsif !value.nil? elsif !value.nil?
@ -47,9 +43,8 @@ module CouchRest::Model
end end
end end
# Cast an individual value, not an array # Cast an individual value
def cast_value(parent, value) def cast_value(parent, value)
raise "An array inside an array cannot be casted, use CastedModel" if value.is_a?(Array)
value = typecast_value(value, self) value = typecast_value(value, self)
associate_casted_value_to_parent(parent, value) associate_casted_value_to_parent(parent, value)
end end
@ -78,6 +73,14 @@ module CouchRest::Model
private private
def parameter_hash_to_array(source)
value = [ ]
source.keys.each do |k|
value[k.to_i] = source[k]
end
value.compact
end
def associate_casted_value_to_parent(parent, value) def associate_casted_value_to_parent(parent, value)
value.casted_by = parent if value.respond_to?(:casted_by) value.casted_by = parent if value.respond_to?(:casted_by)
value.casted_by_property = self if value.respond_to?(:casted_by_property) value.casted_by_property = self if value.respond_to?(:casted_by_property)

View file

@ -73,12 +73,12 @@ module CouchRest
end end
# Base # Base
def new(*args) def new(attrs = {}, options = {}, &block)
proxy_update(model.new(*args)) proxy_block_update(:new, attrs, options, &block)
end end
def build_from_database(doc = {}) def build_from_database(attrs = {}, options = {}, &block)
proxy_update(model.build_from_database(doc)) proxy_block_update(:build_from_database, attrs, options, &block)
end end
def method_missing(m, *args, &block) def method_missing(m, *args, &block)
@ -170,6 +170,13 @@ module CouchRest
end end
end end
def proxy_block_update(method, *args, &block)
model.send(method, *args) do |doc|
proxy_update(doc)
yield doc if block_given?
end
end
end end
end end
end end

View file

@ -18,7 +18,7 @@ CouchRest::Design.class_eval do
flatten = flatten =
lambda {|r| lambda {|r|
(recurse = lambda {|v| (recurse = lambda {|v|
if v.is_a?(Hash) if v.is_a?(Hash) || v.is_a?(CouchRest::Document)
v.to_a.map{|v| recurse.call(v)}.flatten v.to_a.map{|v| recurse.call(v)}.flatten
elsif v.is_a?(Array) elsif v.is_a?(Array)
v.flatten.map{|v| recurse.call(v)} v.flatten.map{|v| recurse.call(v)}

View file

@ -13,18 +13,29 @@ module CouchRest
# Validations may be applied to both Model::Base and Model::CastedModel # Validations may be applied to both Model::Base and Model::CastedModel
module Validations module Validations
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do include ActiveModel::Validations
include ActiveModel::Validations
include ActiveModel::Validations::Callbacks
end
# Determine if the document is valid.
#
# @example Is the document valid?
# person.valid?
#
# @example Is the document valid in a context?
# person.valid?(:create)
#
# @param [ Symbol ] context The optional validation context.
#
# @return [ true, false ] True if valid, false if not.
#
def valid?(context = nil)
super context ? context : (new? ? :create : :update)
end
module ClassMethods module ClassMethods
# Validates the associated casted model. This method should not be # Validates the associated casted model. This method should not be
# used within your code as it is automatically included when a CastedModel # used within your code as it is automatically included when a CastedModel
# is used inside the model. # is used inside the model.
#
def validates_casted_model(*args) def validates_casted_model(*args)
validates_with(CastedModelValidator, _merge_attributes(args)) validates_with(CastedModelValidator, _merge_attributes(args))
end end

View file

@ -2,10 +2,13 @@ require "rails"
require "active_model/railtie" require "active_model/railtie"
module CouchRest module CouchRest
# = Active Record Railtie
class ModelRailtie < Rails::Railtie class ModelRailtie < Rails::Railtie
config.generators.orm :couchrest_model def self.generator
config.generators.test_framework :test_unit, :fixture => false config.respond_to?(:app_generators) ? :app_generators : :generators
end
config.send(generator).orm :couchrest_model
config.send(generator).test_framework :test_unit, :fixture => false
initializer "couchrest_model.configure_default_connection" do initializer "couchrest_model.configure_default_connection" do
CouchRest::Model::Base.configure do |conf| CouchRest::Model::Base.configure do |conf|

View file

@ -33,7 +33,6 @@ require "couchrest/model/property_protection"
require "couchrest/model/properties" require "couchrest/model/properties"
require "couchrest/model/casted_array" require "couchrest/model/casted_array"
require "couchrest/model/casted_hash" require "couchrest/model/casted_hash"
require "couchrest/model/casted_model"
require "couchrest/model/validations" require "couchrest/model/validations"
require "couchrest/model/callbacks" require "couchrest/model/callbacks"
require "couchrest/model/document_queries" require "couchrest/model/document_queries"
@ -58,10 +57,10 @@ require "couchrest/model/core_extensions/hash"
require "couchrest/model/core_extensions/time_parsing" require "couchrest/model/core_extensions/time_parsing"
# Base libraries # Base libraries
require "couchrest/model/casted_model" require "couchrest/model/embeddable"
require "couchrest/model/base" require "couchrest/model/base"
# Add rails support *after* everything has loaded
# Add rails support *after* everything has loaded
if defined?(Rails) if defined?(Rails)
require "couchrest/railtie" require "couchrest/railtie"
end end

View file

@ -0,0 +1,18 @@
require 'rails/generators/couchrest_model'
module CouchrestModel
module Generators
class ConfigGenerator < Rails::Generators::Base
source_root File.expand_path('../templates', __FILE__)
def app_name
Rails::Application.subclasses.first.parent.to_s.underscore
end
def copy_configuration_file
template 'couchdb.yml', File.join('config', "couchdb.yml")
end
end
end
end

View file

@ -0,0 +1,21 @@
development: &development
protocol: 'http'
host: localhost
port: 5984
prefix: <%= app_name %>
suffix: development
username:
password:
test:
<<: *development
suffix: test
production:
protocol: 'https'
host: localhost
port: 5984
prefix: <%= app_name %>
suffix: production
username: root
password: 123

View file

@ -1,40 +0,0 @@
require File.expand_path('../../spec_helper', __FILE__)
begin
require 'rubygems' unless ENV['SKIP_RUBYGEMS']
require 'active_support/json'
ActiveSupport::JSON.backend = :JSONGem
class PlainParent
class_inheritable_accessor :foo
self.foo = :bar
end
class PlainChild < PlainParent
end
class ExtendedParent < CouchRest::Model::Base
class_inheritable_accessor :foo
self.foo = :bar
end
class ExtendedChild < ExtendedParent
end
describe "Using chained inheritance without CouchRest::Model::Base" do
it "should preserve inheritable attributes" do
PlainParent.foo.should == :bar
PlainChild.foo.should == :bar
end
end
describe "Using chained inheritance with CouchRest::Model::Base" do
it "should preserve inheritable attributes" do
ExtendedParent.foo.should == :bar
ExtendedChild.foo.should == :bar
end
end
rescue LoadError
puts "This spec requires 'active_support/json' to be loaded"
end

View file

@ -22,6 +22,7 @@ class Article < CouchRest::Model::Base
property :date, Date property :date, Date
property :slug, :read_only => true property :slug, :read_only => true
property :user_id
property :title property :title
property :tags, [String] property :tags, [String]

View file

@ -83,11 +83,25 @@ class WithCallBacks < CouchRest::Model::Base
end end
end end
# Following two fixture classes have __intentionally__ diffent syntax for setting the validation context
class WithContextualValidationOnCreate < CouchRest::Model::Base
use_database TEST_SERVER.default_database
property(:name, String)
validates(:name, :presence => {:on => :create})
end
class WithContextualValidationOnUpdate < CouchRest::Model::Base
use_database TEST_SERVER.default_database
property(:name, String)
validates(:name, :presence => true, :on => :update)
end
class WithTemplateAndUniqueID < CouchRest::Model::Base class WithTemplateAndUniqueID < CouchRest::Model::Base
use_database TEST_SERVER.default_database use_database TEST_SERVER.default_database
unique_id do |model| unique_id do |model|
model['important-field'] model.slug
end end
property :slug
property :preset, :default => 'value' property :preset, :default => 'value'
property :has_no_default property :has_no_default
end end

View file

@ -1,3 +1,5 @@
require 'person'
class Card < CouchRest::Model::Base class Card < CouchRest::Model::Base
# Set the default database to use # Set the default database to use
use_database DB use_database DB

View file

@ -1,6 +1,6 @@
class CatToy < Hash class CatToy
include ::CouchRest::Model::CastedModel include CouchRest::Model::Embeddable
property :name property :name
@ -17,3 +17,7 @@ class Cat < CouchRest::Model::Base
property :number property :number
end end
class ChildCat < Cat
property :mother, Cat
property :siblings, [Cat]
end

View file

@ -1,5 +1,5 @@
require File.join(FIXTURE_PATH, 'more', 'question') require 'question'
require File.join(FIXTURE_PATH, 'more', 'person') require 'person'
class Course < CouchRest::Model::Base class Course < CouchRest::Model::Base
use_database TEST_SERVER.default_database use_database TEST_SERVER.default_database

5
spec/fixtures/models/key_chain.rb vendored Normal file
View file

@ -0,0 +1,5 @@
class KeyChain < CouchRest::Model::Base
use_database(DB)
property(:keys, Hash)
end

4
spec/fixtures/models/membership.rb vendored Normal file
View file

@ -0,0 +1,4 @@
class Membership
include CouchRest::Model::Embeddable
end

View file

@ -1,5 +1,7 @@
class Person < Hash require 'cat'
include ::CouchRest::Model::CastedModel
class Person
include ::CouchRest::Model::Embeddable
property :pet, Cat property :pet, Cat
property :name, [String] property :name, [String]

6
spec/fixtures/models/project.rb vendored Normal file
View file

@ -0,0 +1,6 @@
class Project < CouchRest::Model::Base
use_database DB
property :name, String
timestamps!
view_by :name
end

7
spec/fixtures/models/question.rb vendored Normal file
View file

@ -0,0 +1,7 @@
class Question
include ::CouchRest::Model::Embeddable
property :q
property :a
end

View file

@ -1,5 +1,6 @@
require File.join(FIXTURE_PATH, 'more', 'client') require 'client'
require File.join(FIXTURE_PATH, 'more', 'sale_entry') require 'sale_entry'
class SaleInvoice < CouchRest::Model::Base class SaleInvoice < CouchRest::Model::Base
use_database DB use_database DB

View file

@ -1,7 +0,0 @@
class Question < Hash
include ::CouchRest::Model::CastedModel
property :q
property :a
end

View file

@ -0,0 +1,8 @@
require File.expand_path('../../spec_helper', __FILE__)
describe CouchRest::Model::Validations do
let(:invoice) do
Invoice.new()
end
end

View file

@ -1,11 +1,16 @@
$LOAD_PATH.unshift(File.dirname(__FILE__))
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
require "bundler/setup" require "bundler/setup"
require "rubygems" require "rubygems"
require "rspec" # Satisfies Autotest and anyone else not using the Rake tasks require "rspec"
require File.join(File.dirname(__FILE__), '..','lib','couchrest_model') require 'couchrest_model'
# check the following file to see how to use the spec'd features.
unless defined?(FIXTURE_PATH) unless defined?(FIXTURE_PATH)
MODEL_PATH = File.join(File.dirname(__FILE__), "fixtures", "models")
$LOAD_PATH.unshift(MODEL_PATH)
FIXTURE_PATH = File.join(File.dirname(__FILE__), '/fixtures') FIXTURE_PATH = File.join(File.dirname(__FILE__), '/fixtures')
SCRATCH_PATH = File.join(File.dirname(__FILE__), '/tmp') SCRATCH_PATH = File.join(File.dirname(__FILE__), '/tmp')
@ -16,17 +21,6 @@ unless defined?(FIXTURE_PATH)
DB = TEST_SERVER.database(TESTDB) DB = TEST_SERVER.database(TESTDB)
end end
class Basic < CouchRest::Model::Base
use_database TEST_SERVER.default_database
end
def reset_test_db!
DB.recreate! rescue nil
# Reset the Design Cache
Thread.current[:couchrest_design_cache] = {}
DB
end
RSpec.configure do |config| RSpec.configure do |config|
config.before(:all) { reset_test_db! } config.before(:all) { reset_test_db! }
@ -39,6 +33,21 @@ RSpec.configure do |config|
end end
end end
# Require each of the fixture models
Dir[ File.join(MODEL_PATH, "*.rb") ].sort.each { |file| require File.basename(file) }
class Basic < CouchRest::Model::Base
use_database TEST_SERVER.default_database
end
def reset_test_db!
DB.recreate! rescue nil
# Reset the Design Cache
Thread.current[:couchrest_design_cache] = {}
DB
end
def couchdb_lucene_available? def couchdb_lucene_available?
lucene_path = "http://localhost:5985/" lucene_path = "http://localhost:5985/"
url = URI.parse(lucene_path) url = URI.parse(lucene_path)

View file

@ -0,0 +1,30 @@
# encoding: utf-8
require 'spec_helper'
require 'test/unit/assertions'
require 'active_model/lint'
class CompliantModel < CouchRest::Model::Base
end
describe CouchRest::Model::Base do
include Test::Unit::Assertions
include ActiveModel::Lint::Tests
before :each do
@model = CompliantModel.new
end
describe "active model lint tests" do
ActiveModel::Lint::Tests.public_instance_methods.map{|m| m.to_s}.grep(/^test/).each do |m|
example m.gsub('_',' ') do
send m
end
end
end
def model
@model
end
end

View file

@ -1,7 +1,5 @@
# encoding: utf-8 # encoding: utf-8
require File.expand_path('../../spec_helper', __FILE__) require 'spec_helper'
require File.join(FIXTURE_PATH, 'more', 'sale_invoice')
describe "Assocations" do describe "Assocations" do

View file

@ -1,4 +1,4 @@
require File.expand_path('../../spec_helper', __FILE__) require 'spec_helper'
describe "Model attachments" do describe "Model attachments" do

View file

@ -1,11 +1,5 @@
# encoding: utf-8 # encoding: utf-8
require "spec_helper"
require File.expand_path("../../spec_helper", __FILE__)
require File.join(FIXTURE_PATH, 'more', 'cat')
require File.join(FIXTURE_PATH, 'more', 'article')
require File.join(FIXTURE_PATH, 'more', 'course')
require File.join(FIXTURE_PATH, 'more', 'card')
require File.join(FIXTURE_PATH, 'base')
describe "Model Base" do describe "Model Base" do
@ -49,6 +43,43 @@ describe "Model Base" do
@obj.database.should eql('database') @obj.database.should eql('database')
end end
it "should only set defined properties" do
@doc = WithDefaultValues.new(:name => 'test', :foo => 'bar')
@doc['name'].should eql('test')
@doc['foo'].should be_nil
end
it "should set all properties with :directly_set_attributes option" do
@doc = WithDefaultValues.new({:name => 'test', :foo => 'bar'}, :directly_set_attributes => true)
@doc['name'].should eql('test')
@doc['foo'].should eql('bar')
end
it "should set the model type" do
@doc = WithDefaultValues.new()
@doc[WithDefaultValues.model_type_key].should eql('WithDefaultValues')
end
it "should call after_initialize method if available" do
@doc = WithAfterInitializeMethod.new
@doc['some_value'].should eql('value')
end
it "should call after_initialize after block" do
@doc = WithAfterInitializeMethod.new {|d| d.some_value = "foo"}
@doc['some_value'].should eql('foo')
end
it "should call after_initialize callback if available" do
klass = Class.new(CouchRest::Model::Base)
klass.class_eval do # for ruby 1.8.7
property :name
after_initialize :set_name
def set_name; self.name = "foobar"; end
end
@doc = klass.new
@doc.name.should eql("foobar")
end
end end
describe "ActiveModel compatability Basic" do describe "ActiveModel compatability Basic" do
@ -90,14 +121,22 @@ describe "Model Base" do
describe "#persisted?" do describe "#persisted?" do
context "when the document is new" do context "when the document is new" do
it "returns false" do it "returns false" do
@obj.persisted?.should == false @obj.persisted?.should be_false
end end
end end
context "when the document is not new" do context "when the document is not new" do
it "returns id" do it "returns id" do
@obj.save @obj.save
@obj.persisted?.should == true @obj.persisted?.should be_true
end
end
context "when the document is destroyed" do
it "returns false" do
@obj.save
@obj.destroy
@obj.persisted?.should be_false
end end
end end
end end
@ -109,7 +148,57 @@ describe "Model Base" do
end end
end end
describe "#destroyed?" do
it "should be present" do
@obj.should respond_to(:destroyed?)
end
it "should return false with new object" do
@obj.destroyed?.should be_false
end
it "should return true after destroy" do
@obj.save
@obj.destroy
@obj.destroyed?.should be_true
end
end
end
describe "comparisons" do
describe "#==" do
context "on saved document" do
it "should be true on same document" do
p = Project.create
p.should eql(p)
end
it "should be true after loading" do
p = Project.create
p.should eql(Project.get(p.id))
end
it "should not be true if databases do not match" do
p = Project.create
p2 = p.dup
p2.stub!(:database).and_return('other')
p.should_not eql(p2)
end
it "should always be false if one document not saved" do
p = Project.create(:name => 'test')
o = Project.new(:name => 'test')
p.should_not eql(o)
end
end
context "with new documents" do
it "should be true when attributes match" do
p = Project.new(:name => 'test')
o = Project.new(:name => 'test')
p.should eql(o)
end
it "should not be true when attributes don't match" do
p = Project.new(:name => 'test')
o = Project.new(:name => 'testing')
p.should_not eql(o)
end
end
end
end end
describe "update attributes without saving" do describe "update attributes without saving" do
@ -232,7 +321,7 @@ describe "Model Base" do
WithTemplateAndUniqueID.all.map{|o| o.destroy} WithTemplateAndUniqueID.all.map{|o| o.destroy}
WithTemplateAndUniqueID.database.bulk_delete WithTemplateAndUniqueID.database.bulk_delete
@tmpl = WithTemplateAndUniqueID.new @tmpl = WithTemplateAndUniqueID.new
@tmpl2 = WithTemplateAndUniqueID.new(:preset => 'not_value', 'important-field' => '1') @tmpl2 = WithTemplateAndUniqueID.new(:preset => 'not_value', 'slug' => '1')
end end
it "should have fields set when new" do it "should have fields set when new" do
@tmpl.preset.should == 'value' @tmpl.preset.should == 'value'
@ -253,10 +342,10 @@ describe "Model Base" do
before(:all) do before(:all) do
WithTemplateAndUniqueID.all.map{|o| o.destroy} WithTemplateAndUniqueID.all.map{|o| o.destroy}
WithTemplateAndUniqueID.database.bulk_delete WithTemplateAndUniqueID.database.bulk_delete
WithTemplateAndUniqueID.new('important-field' => '1').save WithTemplateAndUniqueID.new('slug' => '1').save
WithTemplateAndUniqueID.new('important-field' => '2').save WithTemplateAndUniqueID.new('slug' => '2').save
WithTemplateAndUniqueID.new('important-field' => '3').save WithTemplateAndUniqueID.new('slug' => '3').save
WithTemplateAndUniqueID.new('important-field' => '4').save WithTemplateAndUniqueID.new('slug' => '4').save
end end
it "should find all" do it "should find all" do
rs = WithTemplateAndUniqueID.all rs = WithTemplateAndUniqueID.all
@ -274,9 +363,9 @@ describe "Model Base" do
end end
it ".count should return the number of documents" do it ".count should return the number of documents" do
WithTemplateAndUniqueID.new('important-field' => '1').save WithTemplateAndUniqueID.new('slug' => '1').save
WithTemplateAndUniqueID.new('important-field' => '2').save WithTemplateAndUniqueID.new('slug' => '2').save
WithTemplateAndUniqueID.new('important-field' => '3').save WithTemplateAndUniqueID.new('slug' => '3').save
WithTemplateAndUniqueID.count.should == 3 WithTemplateAndUniqueID.count.should == 3
end end
@ -285,14 +374,14 @@ describe "Model Base" do
describe "finding the first instance of a model" do describe "finding the first instance of a model" do
before(:each) do before(:each) do
@db = reset_test_db! @db = reset_test_db!
WithTemplateAndUniqueID.new('important-field' => '1').save WithTemplateAndUniqueID.new('slug' => '1').save
WithTemplateAndUniqueID.new('important-field' => '2').save WithTemplateAndUniqueID.new('slug' => '2').save
WithTemplateAndUniqueID.new('important-field' => '3').save WithTemplateAndUniqueID.new('slug' => '3').save
WithTemplateAndUniqueID.new('important-field' => '4').save WithTemplateAndUniqueID.new('slug' => '4').save
end end
it "should find first" do it "should find first" do
rs = WithTemplateAndUniqueID.first rs = WithTemplateAndUniqueID.first
rs['important-field'].should == "1" rs['slug'].should == "1"
end end
it "should return nil if no instances are found" do it "should return nil if no instances are found" do
WithTemplateAndUniqueID.all.each {|obj| obj.destroy } WithTemplateAndUniqueID.all.each {|obj| obj.destroy }
@ -370,14 +459,7 @@ describe "Model Base" do
end end
end end
describe "initialization" do describe "recursive validation on a model" do
it "should call after_initialize method if available" do
@doc = WithAfterInitializeMethod.new
@doc['some_value'].should eql('value')
end
end
describe "recursive validation on a model" do
before :each do before :each do
reset_test_db! reset_test_db!
@cat = Cat.new(:name => 'Sockington') @cat = Cat.new(:name => 'Sockington')

View file

@ -1,7 +1,4 @@
require File.expand_path('../../spec_helper', __FILE__) require "spec_helper"
require File.join(FIXTURE_PATH, 'more', 'cat')
require File.join(FIXTURE_PATH, 'more', 'person')
require File.join(FIXTURE_PATH, 'more', 'card')
class Driver < CouchRest::Model::Base class Driver < CouchRest::Model::Base
use_database TEST_SERVER.default_database use_database TEST_SERVER.default_database

View file

@ -1,4 +1,4 @@
require File.expand_path("../../spec_helper", __FILE__) require "spec_helper"
class UnattachedDoc < CouchRest::Model::Base class UnattachedDoc < CouchRest::Model::Base
# Note: no use_database here # Note: no use_database here

View file

@ -1,5 +1,4 @@
require File.expand_path("../../spec_helper", __FILE__) require "spec_helper"
require File.join(FIXTURE_PATH, 'more', 'article')
describe "Collections" do describe "Collections" do
@ -27,21 +26,20 @@ describe "Collections" do
end end
it "should provide a class method for paginate" do it "should provide a class method for paginate" do
articles = Article.paginate(:design_doc => 'Article', :view_name => 'by_date', articles = Article.paginate(:design_doc => 'Article', :view_name => 'by_date',
:per_page => 3, :descending => true, :key => Date.today, :include_docs => true) :per_page => 3, :descending => true, :key => Date.today)
articles.size.should == 3 articles.size.should == 3
articles = Article.paginate(:design_doc => 'Article', :view_name => 'by_date', articles = Article.paginate(:design_doc => 'Article', :view_name => 'by_date',
:per_page => 3, :page => 2, :descending => true, :key => Date.today, :include_docs => true) :per_page => 3, :page => 2, :descending => true, :key => Date.today)
articles.size.should == 3 articles.size.should == 3
articles = Article.paginate(:design_doc => 'Article', :view_name => 'by_date', articles = Article.paginate(:design_doc => 'Article', :view_name => 'by_date',
:per_page => 3, :page => 3, :descending => true, :key => Date.today, :include_docs => true) :per_page => 3, :page => 3, :descending => true, :key => Date.today)
articles.size.should == 1 articles.size.should == 1
end end
it "should provide a class method for paginated_each" do it "should provide a class method for paginated_each" do
options = { :design_doc => 'Article', :view_name => 'by_date', options = { :design_doc => 'Article', :view_name => 'by_date',
:per_page => 3, :page => 1, :descending => true, :key => Date.today, :per_page => 3, :page => 1, :descending => true, :key => Date.today }
:include_docs => true }
Article.paginated_each(options) do |a| Article.paginated_each(options) do |a|
a.should_not be_nil a.should_not be_nil
end end

View file

@ -1,8 +1,7 @@
# encoding: utf-8 # encoding: utf-8
require File.expand_path('../../spec_helper', __FILE__) require "spec_helper"
require File.join(FIXTURE_PATH, 'more', 'cat')
describe CouchRest::Model::Base do describe CouchRest::Model::Configuration do
before do before do
@class = Class.new(CouchRest::Model::Base) @class = Class.new(CouchRest::Model::Base)

View file

@ -1,7 +1,7 @@
# encoding: utf-8 # encoding: utf-8
require File.expand_path('../../spec_helper', __FILE__) require 'spec_helper'
describe CouchRest::Model::Base do describe CouchRest::Model::Connection do
before do before do
@class = Class.new(CouchRest::Model::Base) @class = Class.new(CouchRest::Model::Base)

View file

@ -1,10 +1,7 @@
# encoding: utf-8 # encoding: utf-8
require 'spec_helper'
require File.expand_path("../../spec_helper", __FILE__) describe CouchRest::Model::DesignDoc do
require File.join(FIXTURE_PATH, 'base')
require File.join(FIXTURE_PATH, 'more', 'article')
describe "Design Documents" do
before :all do before :all do
reset_test_db! reset_test_db!
@ -231,7 +228,7 @@ describe "Design Documents" do
describe "lazily refreshing the design document" do describe "lazily refreshing the design document" do
before(:all) do before(:all) do
@db = reset_test_db! @db = reset_test_db!
WithTemplateAndUniqueID.new('important-field' => '1').save WithTemplateAndUniqueID.new('slug' => '1').save
end end
it "should not save the design doc twice" do it "should not save the design doc twice" do
WithTemplateAndUniqueID.all WithTemplateAndUniqueID.all

View file

@ -1,10 +1,9 @@
require File.expand_path("../../spec_helper", __FILE__) require "spec_helper"
class DesignModel < CouchRest::Model::Base class DesignModel < CouchRest::Model::Base
end end
describe "Design" do describe CouchRest::Model::Designs do
it "should accessable from model" do it "should accessable from model" do
DesignModel.respond_to?(:design).should be_true DesignModel.respond_to?(:design).should be_true

View file

@ -1,12 +1,6 @@
require File.expand_path("../../spec_helper", __FILE__) require "spec_helper"
require File.join(FIXTURE_PATH, 'more', 'cat') class WithCastedModelMixin
require File.join(FIXTURE_PATH, 'more', 'article')
require File.join(FIXTURE_PATH, 'more', 'course')
require File.join(FIXTURE_PATH, 'more', 'card')
require File.join(FIXTURE_PATH, 'base')
class WithCastedModelMixin < Hash
include CouchRest::Model::CastedModel include CouchRest::Model::CastedModel
property :name property :name
property :details, Object, :default => {} property :details, Object, :default => {}
@ -241,6 +235,14 @@ describe "Dirty" do
end end
end end
it "should report changes if an array is popped after reload" do
should_change_array do |array, obj|
obj.reload
obj.keywords.pop
end
end
it "should report no changes if an empty array is popped" do it "should report no changes if an empty array is popped" do
should_not_change_array do |array, obj| should_not_change_array do |array, obj|
array.clear array.clear
@ -249,6 +251,50 @@ describe "Dirty" do
end end
end end
it "should report changes on deletion from an array" do
should_change_array do |array, obj|
array << "keyword"
obj.save!
array.delete_at(0)
end
should_change_array do |array, obj|
array << "keyword"
obj.save!
array.delete("keyword")
end
end
it "should report changes on deletion from an array after reload" do
should_change_array do |array, obj|
array << "keyword"
obj.save!
obj.reload
array.delete_at(0)
end
should_change_array do |array, obj|
array << "keyword"
obj.save!
obj.reload
array.delete("keyword")
end
end
it "should report no changes on deletion from an empty array" do
should_not_change_array do |array, obj|
array.clear
obj.save!
array.delete_at(0)
end
should_not_change_array do |array, obj|
array.clear
obj.save!
array.delete("keyword")
end
end
it "should report changes if an array is pushed" do it "should report changes if an array is pushed" do
should_change_array do |array, obj| should_change_array do |array, obj|
array.push("keyword") array.push("keyword")

View file

@ -1,26 +1,25 @@
# encoding: utf-8 # encoding: utf-8
require "spec_helper"
require File.expand_path('../../spec_helper', __FILE__) class WithCastedModelMixin
require File.join(FIXTURE_PATH, 'more', 'cat') include CouchRest::Model::Embeddable
require File.join(FIXTURE_PATH, 'more', 'person')
require File.join(FIXTURE_PATH, 'more', 'card')
require File.join(FIXTURE_PATH, 'more', 'question')
require File.join(FIXTURE_PATH, 'more', 'course')
class WithCastedModelMixin < Hash
include CouchRest::Model::CastedModel
property :name property :name
property :no_value property :no_value
property :details, Object, :default => {} property :details, Object, :default => {}
property :casted_attribute, WithCastedModelMixin property :casted_attribute, WithCastedModelMixin
end end
class OldFashionedMixin < Hash
include CouchRest::Model::CastedModel
property :name
end
class DummyModel < CouchRest::Model::Base class DummyModel < CouchRest::Model::Base
use_database TEST_SERVER.default_database use_database TEST_SERVER.default_database
raise "Default DB not set" if TEST_SERVER.default_database.nil? raise "Default DB not set" if TEST_SERVER.default_database.nil?
property :casted_attribute, WithCastedModelMixin property :casted_attribute, WithCastedModelMixin
property :keywords, [String] property :keywords, [String]
property :old_casted_attribute, OldFashionedMixin
property :sub_models do |child| property :sub_models do |child|
child.property :title child.property :title
end end
@ -29,8 +28,8 @@ class DummyModel < CouchRest::Model::Base
end end
end end
class WithCastedCallBackModel < Hash class WithCastedCallBackModel
include CouchRest::Model::CastedModel include CouchRest::Model::Embeddable
property :name property :name
property :run_before_validation property :run_before_validation
property :run_after_validation property :run_after_validation
@ -51,19 +50,7 @@ class CastedCallbackDoc < CouchRest::Model::Base
property :callback_model, WithCastedCallBackModel property :callback_model, WithCastedCallBackModel
end end
describe CouchRest::Model::CastedModel do describe CouchRest::Model::Embeddable do
describe "A non hash class including CastedModel" do
it "should fail raising and include error" do
lambda do
class NotAHashButWithCastedModelMixin
include CouchRest::CastedModel
property :name
end
end.should raise_error
end
end
describe "isolated" do describe "isolated" do
before(:each) do before(:each) do
@ -82,7 +69,27 @@ describe CouchRest::Model::CastedModel do
it "should always return base_doc? as false" do it "should always return base_doc? as false" do
@obj.base_doc?.should be_false @obj.base_doc?.should be_false
end end
it "should call after_initialize callback if available" do
klass = Class.new do
include CouchRest::Model::CastedModel
after_initialize :set_name
property :name
def set_name; self.name = "foobar"; end
end
@obj = klass.new
@obj.name.should eql("foobar")
end
it "should allow override of initialize with super" do
klass = Class.new do
include CouchRest::Model::Embeddable
after_initialize :set_name
property :name
def set_name; self.name = "foobar"; end
def initialize(attrs = {}); super(); end
end
@obj = klass.new
@obj.name.should eql("foobar")
end
end end
describe "casted as an attribute, but without a value" do describe "casted as an attribute, but without a value" do
@ -162,6 +169,33 @@ describe CouchRest::Model::CastedModel do
end end
end end
# Basic testing for an old fashioned casted hash
describe "old hash casted as attribute" do
before :each do
@obj = DummyModel.new(:old_casted_attribute => {:name => 'Testing'})
@casted_obj = @obj.old_casted_attribute
end
it "should be available from its parent" do
@casted_obj.should be_an_instance_of(OldFashionedMixin)
end
it "should have the getters defined" do
@casted_obj.name.should == 'Testing'
end
it "should know who casted it" do
@casted_obj.casted_by.should == @obj
end
it "should know which property casted it" do
@casted_obj.casted_by_property.should == @obj.properties.detect{|p| p.to_s == 'old_casted_attribute'}
end
it "should return nil for the unknown attribute" do
@casted_obj["unknown"].should be_nil
end
end
describe "casted as an array of a different type" do describe "casted as an array of a different type" do
before(:each) do before(:each) do
@obj = DummyModel.new(:keywords => ['couch', 'sofa', 'relax', 'canapé']) @obj = DummyModel.new(:keywords => ['couch', 'sofa', 'relax', 'canapé'])

View file

@ -0,0 +1,33 @@
require 'spec_helper'
class PlainParent
class_inheritable_accessor :foo
self.foo = :bar
end
class PlainChild < PlainParent
end
class ExtendedParent < CouchRest::Model::Base
class_inheritable_accessor :foo
self.foo = :bar
end
class ExtendedChild < ExtendedParent
end
describe "Using chained inheritance without CouchRest::Model::Base" do
it "should preserve inheritable attributes" do
PlainParent.foo.should == :bar
PlainChild.foo.should == :bar
end
end
describe "Using chained inheritance with CouchRest::Model::Base" do
it "should preserve inheritable attributes" do
ExtendedParent.foo.should == :bar
ExtendedChild.foo.should == :bar
end
end

View file

@ -1,12 +1,7 @@
# encoding: utf-8 # encoding: utf-8
require File.expand_path('../../spec_helper', __FILE__) require 'spec_helper'
require File.join(FIXTURE_PATH, 'base')
require File.join(FIXTURE_PATH, 'more', 'cat')
require File.join(FIXTURE_PATH, 'more', 'article')
require File.join(FIXTURE_PATH, 'more', 'course')
require File.join(FIXTURE_PATH, 'more', 'card')
describe "Model Persistence" do describe CouchRest::Model::Persistence do
before(:each) do before(:each) do
@obj = WithDefaultValues.new @obj = WithDefaultValues.new
@ -34,8 +29,8 @@ describe "Model Persistence" do
describe "basic saving and retrieving" do describe "basic saving and retrieving" do
it "should work fine" do it "should work fine" do
@obj.name = "should be easily saved and retrieved" @obj.name = "should be easily saved and retrieved"
@obj.save @obj.save!
saved_obj = WithDefaultValues.get(@obj.id) saved_obj = WithDefaultValues.get!(@obj.id)
saved_obj.should_not be_nil saved_obj.should_not be_nil
end end
@ -81,6 +76,18 @@ describe "Model Persistence" do
article.should_not be_new article.should_not be_new
end end
it "yields new instance to block before saving (#create)" do
article = Article.create{|a| a.title = 'my create init block test'}
article.title.should == 'my create init block test'
article.should_not be_new
end
it "yields new instance to block before saving (#create!)" do
article = Article.create{|a| a.title = 'my create bang init block test'}
article.title.should == 'my create bang init block test'
article.should_not be_new
end
it "should trigger the create callbacks" do it "should trigger the create callbacks" do
doc = WithCallBacks.create(:name => 'my other test') doc = WithCallBacks.create(:name => 'my other test')
doc.run_before_create.should be_true doc.run_before_create.should be_true
@ -210,34 +217,34 @@ describe "Model Persistence" do
it "should require the field" do it "should require the field" do
lambda{@templated.save}.should raise_error lambda{@templated.save}.should raise_error
@templated['important-field'] = 'very-important' @templated['slug'] = 'very-important'
@templated.save.should be_true @templated.save.should be_true
end end
it "should save with the id" do it "should save with the id" do
@templated['important-field'] = 'very-important' @templated['slug'] = 'very-important'
@templated.save.should be_true @templated.save.should be_true
t = WithTemplateAndUniqueID.get('very-important') t = WithTemplateAndUniqueID.get('very-important')
t.should == @templated t.should == @templated
end end
it "should not change the id on update" do it "should not change the id on update" do
@templated['important-field'] = 'very-important' @templated['slug'] = 'very-important'
@templated.save.should be_true @templated.save.should be_true
@templated['important-field'] = 'not-important' @templated['slug'] = 'not-important'
@templated.save.should be_true @templated.save.should be_true
t = WithTemplateAndUniqueID.get('very-important') t = WithTemplateAndUniqueID.get('very-important')
t.id.should == @templated.id t.id.should == @templated.id
end end
it "should raise an error when the id is taken" do it "should raise an error when the id is taken" do
@templated['important-field'] = 'very-important' @templated['slug'] = 'very-important'
@templated.save.should be_true @templated.save.should be_true
lambda{WithTemplateAndUniqueID.new('important-field' => 'very-important').save}.should raise_error lambda{WithTemplateAndUniqueID.new('slug' => 'very-important').save}.should raise_error
end end
it "should set the id" do it "should set the id" do
@templated['important-field'] = 'very-important' @templated['slug'] = 'very-important'
@templated.save.should be_true @templated.save.should be_true
@templated.id.should == 'very-important' @templated.id.should == 'very-important'
end end
@ -245,22 +252,31 @@ describe "Model Persistence" do
describe "destroying an instance" do describe "destroying an instance" do
before(:each) do before(:each) do
@dobj = Basic.new @dobj = Event.new
@dobj.save.should be_true @dobj.save.should be_true
end end
it "should return true" do it "should return true" do
result = @dobj.destroy result = @dobj.destroy
result.should be_true result.should be_true
end end
it "should be resavable" do
@dobj.destroy
@dobj.rev.should be_nil
@dobj.id.should be_nil
@dobj.save.should be_true
end
it "should make it go away" do it "should make it go away" do
@dobj.destroy @dobj.destroy
lambda{Basic.get!(@dobj.id)}.should raise_error lambda{Basic.get!(@dobj.id)}.should raise_error(CouchRest::Model::DocumentNotFound)
end
it "should freeze the object" do
@dobj.destroy
# In Ruby 1.9.2 this raises RuntimeError, in 1.8.7 TypeError, D'OH!
lambda { @dobj.subject = "Test" }.should raise_error(StandardError)
end
it "trying to save after should fail" do
@dobj.destroy
lambda { @dobj.save }.should raise_error(StandardError)
lambda{Basic.get!(@dobj.id)}.should raise_error(CouchRest::Model::DocumentNotFound)
end
it "should make destroyed? true" do
@dobj.destroyed?.should be_false
@dobj.destroy
@dobj.destroyed?.should be_true
end end
end end
@ -340,6 +356,27 @@ describe "Model Persistence" do
end end
end end
describe "with contextual validation on ”create”" do
it "should validate only within ”create” context" do
doc = WithContextualValidationOnCreate.new
doc.save.should be_false
doc.name = "Alice"
doc.save.should be_true
doc.update_attributes(:name => nil).should be_true
end
end
describe "with contextual validation on ”update”" do
it "should validate only within ”update” context" do
doc = WithContextualValidationOnUpdate.new
doc.save.should be_true
doc.update_attributes(:name => nil).should be_false
doc.update_attributes(:name => "Bob").should be_true
end
end
describe "save" do describe "save" do
it "should run the after filter after saving" do it "should run the after filter after saving" do
@doc.run_after_save.should be_nil @doc.run_after_save.should be_nil

View file

@ -1,4 +1,4 @@
require File.expand_path("../../spec_helper", __FILE__) require "spec_helper"
describe "Model Attributes" do describe "Model Attributes" do

View file

@ -1,17 +1,7 @@
# encoding: utf-8 # encoding: utf-8
require File.expand_path('../../spec_helper', __FILE__) require 'spec_helper'
require File.join(FIXTURE_PATH, 'more', 'article')
require File.join(FIXTURE_PATH, 'more', 'cat')
require File.join(FIXTURE_PATH, 'more', 'person')
require File.join(FIXTURE_PATH, 'more', 'card')
require File.join(FIXTURE_PATH, 'more', 'invoice')
require File.join(FIXTURE_PATH, 'more', 'service')
require File.join(FIXTURE_PATH, 'more', 'event')
require File.join(FIXTURE_PATH, 'more', 'user')
require File.join(FIXTURE_PATH, 'more', 'course')
describe CouchRest::Model::Property do
describe "Model properties" do
before(:each) do before(:each) do
reset_test_db! reset_test_db!
@ -72,15 +62,23 @@ describe "Model properties" do
@card.updated_at.should_not be_nil @card.updated_at.should_not be_nil
end end
describe "#as_json" do describe "#as_couch_json" do
it "should provide a simple hash from model" do it "should provide a simple hash from model" do
@card.as_json.class.should eql(Hash) @card.as_couch_json.class.should eql(Hash)
end end
it "should remove properties from Hash if value is nil" do it "should remove properties from Hash if value is nil" do
@card.last_name = nil @card.last_name = nil
@card.as_json.keys.include?('last_name').should be_false @card.as_couch_json.keys.include?('last_name').should be_false
end
end
describe "#as_json" do
it "should provide a simple hash from model" do
@card.as_json.class.should eql(Hash)
end end
it "should pass options to Active Support's as_json" do it "should pass options to Active Support's as_json" do
@ -239,6 +237,16 @@ describe "Model properties" do
end end
describe "properties of hash of casted models" do
it "should be able to assign a casted hash to a hash property" do
chain = KeyChain.new
keys = {"House" => "8==$", "Office" => "<>==U"}
chain.keys = keys
chain.keys = chain.keys
chain.keys.should == keys
end
end
describe "properties of array of casted models" do describe "properties of array of casted models" do
before(:each) do before(:each) do
@ -265,9 +273,9 @@ describe "properties of array of casted models" do
end end
it "should allow attribute to be set from hash with ordered keys and sub-hashes" do it "should allow attribute to be set from hash with ordered keys and sub-hashes" do
@course.questions = { '0' => {:q => "Test1"}, '1' => {:q => 'Test2'} } @course.questions = { '10' => {:q => 'Test10'}, '0' => {:q => "Test1"}, '1' => {:q => 'Test2'} }
@course.questions.length.should eql(2) @course.questions.length.should eql(3)
@course.questions.last.q.should eql('Test2') @course.questions.last.q.should eql('Test10')
@course.questions.last.class.should eql(Question) @course.questions.last.class.should eql(Question)
end end
@ -284,7 +292,7 @@ describe "properties of array of casted models" do
it "should raise an error if attempting to set single value for array type" do it "should raise an error if attempting to set single value for array type" do
lambda { lambda {
@course.questions = Question.new(:q => 'test1') @course.questions = Question.new(:q => 'test1')
}.should raise_error }.should raise_error(/Expecting an array/)
end end
@ -315,6 +323,28 @@ describe "a casted model retrieved from the database" do
end end
end end
describe "nested models (not casted)" do
before(:each) do
reset_test_db!
@cat = ChildCat.new(:name => 'Stimpy')
@cat.mother = {:name => 'Stinky'}
@cat.siblings = [{:name => 'Feather'}, {:name => 'Felix'}]
@cat.save
@cat = ChildCat.get(@cat.id)
end
it "should correctly save single relation" do
@cat.mother.name.should eql('Stinky')
@cat.mother.casted_by.should eql(@cat)
end
it "should correctly save collection" do
@cat.siblings.first.name.should eql("Feather")
@cat.siblings.last.casted_by.should eql(@cat)
end
end
describe "Property Class" do describe "Property Class" do
it "should provide name as string" do it "should provide name as string" do
@ -420,15 +450,18 @@ describe "Property Class" do
ary.last.should eql(Date.new(2011, 05, 22)) ary.last.should eql(Date.new(2011, 05, 22))
end end
it "should raise and error if value is array when type is not" do it "should cast an object that provides an array" do
property = CouchRest::Model::Property.new(:test, Date) prop = Class.new do
attr_accessor :ary
def initialize(val); self.ary = val; end
def as_json; ary; end
end
property = CouchRest::Model::Property.new(:test, prop)
parent = mock("FooClass") parent = mock("FooClass")
lambda { cast = property.cast(parent, [1, 2])
cast = property.cast(parent, [Date.new(2010, 6, 1)]) cast.ary.should eql([1, 2])
}.should raise_error
end end
it "should set parent as casted_by object in CastedArray" do it "should set parent as casted_by object in CastedArray" do
property = CouchRest::Model::Property.new(:test, [Object]) property = CouchRest::Model::Property.new(:test, [Object])
parent = mock("FooObject") parent = mock("FooObject")

View file

@ -1,6 +1,4 @@
require File.expand_path("../../spec_helper", __FILE__) require "spec_helper"
require File.join(FIXTURE_PATH, 'more', 'cat')
class DummyProxyable < CouchRest::Model::Base class DummyProxyable < CouchRest::Model::Base
proxy_database_method :db proxy_database_method :db
@ -12,7 +10,7 @@ end
class ProxyKitten < CouchRest::Model::Base class ProxyKitten < CouchRest::Model::Base
end end
describe "Proxyable" do describe CouchRest::Model::Proxyable do
describe "#proxy_database" do describe "#proxy_database" do
@ -87,7 +85,7 @@ describe "Proxyable" do
DummyProxyable.proxy_for(:cats) DummyProxyable.proxy_for(:cats)
@obj = DummyProxyable.new @obj = DummyProxyable.new
CouchRest::Model::Proxyable::ModelProxy.should_receive(:new).with(Cat, @obj, 'dummy_proxyable', 'db').and_return(true) CouchRest::Model::Proxyable::ModelProxy.should_receive(:new).with(Cat, @obj, 'dummy_proxyable', 'db').and_return(true)
@obj.should_receive('proxy_database').and_return('db') @obj.should_receive(:proxy_database).and_return('db')
@obj.cats @obj.cats
end end
@ -165,15 +163,13 @@ describe "Proxyable" do
end end
it "should proxy new call" do it "should proxy new call" do
Cat.should_receive(:new).and_return({}) @obj.should_receive(:proxy_block_update).with(:new, 'attrs', 'opts')
@obj.should_receive(:proxy_update).and_return(true) @obj.new('attrs', 'opts')
@obj.new
end end
it "should proxy build_from_database" do it "should proxy build_from_database" do
Cat.should_receive(:build_from_database).and_return({}) @obj.should_receive(:proxy_block_update).with(:build_from_database, 'attrs', 'opts')
@obj.should_receive(:proxy_update).with({}).and_return(true) @obj.build_from_database('attrs', 'opts')
@obj.build_from_database
end end
describe "#method_missing" do describe "#method_missing" do
@ -313,6 +309,15 @@ describe "Proxyable" do
@obj.send(:proxy_update_all, docs) @obj.send(:proxy_update_all, docs)
end end
describe "#proxy_block_update" do
it "should proxy block updates" do
doc = { }
@obj.model.should_receive(:new).and_yield(doc)
@obj.should_receive(:proxy_update).with(doc)
@obj.send(:proxy_block_update, :new)
end
end
end end
end end

View file

@ -1,8 +1,4 @@
require File.expand_path("../../spec_helper", __FILE__) require "spec_helper"
require File.join(FIXTURE_PATH, 'more', 'cat')
require File.join(FIXTURE_PATH, 'more', 'person')
require File.join(FIXTURE_PATH, 'more', 'card')
require File.join(FIXTURE_PATH, 'more', 'course')
# add a default value # add a default value
Card.property :bg_color, :default => '#ccc' Card.property :bg_color, :default => '#ccc'

View file

@ -1,8 +1,5 @@
# encoding: utf-8 # encoding: utf-8
require File.expand_path('../../spec_helper', __FILE__) require 'spec_helper'
require File.join(FIXTURE_PATH, 'more', 'cat')
require File.join(FIXTURE_PATH, 'more', 'person')
require File.join(FIXTURE_PATH, 'more', 'course')
describe "Type Casting" do describe "Type Casting" do

View file

@ -1,14 +1,6 @@
require File.expand_path("../../spec_helper", __FILE__) require "spec_helper"
require File.join(FIXTURE_PATH, 'more', 'cat') describe CouchRest::Model::Validations do
require File.join(FIXTURE_PATH, 'more', 'article')
require File.join(FIXTURE_PATH, 'more', 'course')
require File.join(FIXTURE_PATH, 'more', 'card')
require File.join(FIXTURE_PATH, 'base')
# TODO Move validations from other specs to here
describe "Validations" do
describe "Uniqueness" do describe "Uniqueness" do

View file

@ -1,10 +1,6 @@
require File.expand_path("../../spec_helper", __FILE__) require "spec_helper"
require File.join(FIXTURE_PATH, 'more', 'cat')
require File.join(FIXTURE_PATH, 'more', 'person')
require File.join(FIXTURE_PATH, 'more', 'article')
require File.join(FIXTURE_PATH, 'more', 'course')
describe "Model views" do describe CouchRest::Model::Views do
class Unattached < CouchRest::Model::Base class Unattached < CouchRest::Model::Base
property :title property :title
@ -18,7 +14,6 @@ describe "Model views" do
end end
end end
describe "ClassMethods" do describe "ClassMethods" do
# NOTE! Add more unit tests! # NOTE! Add more unit tests!
@ -179,6 +174,21 @@ describe "Model views" do
end end
describe "#method_missing for find_by methods" do
before(:all) { reset_test_db! }
specify { Course.should respond_to :find_by_title_and_active }
specify { Course.should respond_to :by_title }
specify "#method should work in ruby 1.9, but not 1.8" do
if RUBY_VERSION >= "1.9"
Course.method(:find_by_title_and_active).should be_a Method
else
expect { Course.method(:find_by_title_and_active) }.to raise_error(NameError)
end
end
end
describe "a ducktype view" do describe "a ducktype view" do
before(:all) do before(:all) do
reset_test_db! reset_test_db!