adding rdoc to method properties_with_values

This commit is contained in:
Lucas Renan 2010-12-22 23:09:00 -02:00
commit 92a10dbfc9
30 changed files with 764 additions and 506 deletions

162
README.md
View file

@ -9,16 +9,13 @@ for validations and callbacks.
If your project is still running Rails 2.3, you'll have to continue using ExtendedDocument as If your project is still running Rails 2.3, you'll have to continue using ExtendedDocument as
it is not possible to load ActiveModel into programs that do not use ActiveSupport 3.0. it is not possible to load ActiveModel into programs that do not use ActiveSupport 3.0.
CouchRest Model only supports CouchDB 0.10.0 or newer. CouchRest Model is only tested on CouchDB 1.0.0 or newer.
## Install ## Install
### From Gem ### Gem
CouchRest Model depends on Rails 3's ActiveModel which has not yet been released. You'll need to add $ sudo gem install couchrest_model
`--pre` to the end of the gem install until the dependencies are stable:
$ sudo gem install couchrest_model --pre
### Bundler ### Bundler
@ -71,16 +68,14 @@ but no guarantees!
## Properties ## Properties
Only attributes with a property definition will be stored be CouchRest Model (as opposed A property is the definition of an attribute, it describes what the attribute is called, how it should
to a normal CouchRest Document which will store everything). To help prevent confusion, be type casted and other options such as the default value. These replace your typical
a property should be considered as the definition of an attribute. An attribute must be associated `add_column` methods found in relational database migrations.
with a property, but a property may not have any attributes associated if none have been set.
Attributes with a property definition will have setter and getter methods defined for them. Any other attibute
can be set in the same way you'd update a Hash, this funcionality is inherited from CouchRest Documents.
In its simplest form, a property Here are a few examples of the way properties are used:
will only create a getter and setter passing all attribute data directly to the database. Assuming the attribute
provided responds to `to_json`, there will not be any problems saving it, but when loading the
data back it will either be a string, number, array, or hash:
class Cat < CouchRest::Model::Base class Cat < CouchRest::Model::Base
property :name property :name
@ -105,8 +100,7 @@ Properties create getters and setters similar to the following:
write_attribute('name', value) write_attribute('name', value)
end end
Properties can also have a type which Properties can also have a type which will be used for casting data retrieved from CouchDB when the attribute is set:
will be used for casting data retrieved from CouchDB when the attribute is set:
class Cat < CouchRest::Model::Base class Cat < CouchRest::Model::Base
property :name, String property :name, String
@ -120,7 +114,7 @@ will be used for casting data retrieved from CouchDB when the attribute is set:
@cat.last_fed_at < 20.minutes.ago # True! @cat.last_fed_at < 20.minutes.ago # True!
Booleans or TrueClass will also create a getter with question mark at the end: Boolean or TrueClass types will create a getter with question mark at the end:
class Cat < CouchRest::Model::Base class Cat < CouchRest::Model::Base
property :awake, TrueClass, :default => true property :awake, TrueClass, :default => true
@ -130,9 +124,8 @@ Booleans or TrueClass will also create a getter with question mark at the end:
Adding the +:default+ option will ensure the attribute always has a value. Adding the +:default+ option will ensure the attribute always has a value.
Defining a property as read-only will mean that its value is set only when read from the A read-only property will only have a getter method, and its value is set when the document
database and that it will not have a setter method. You can however update a read-only is read from the database. You can however update a read-only attribute using the `write_attribute` method:
attribute using the `write_attribute` method:
class Cat < CouchRest::Model::Base class Cat < CouchRest::Model::Base
property :name, String property :name, String
@ -148,10 +141,23 @@ attribute using the `write_attribute` method:
@cat.fall_off_balcony! @cat.fall_off_balcony!
@cat.lives # Now 8! @cat.lives # Now 8!
Mass assigning attributes is also possible in a similar fashion to ActiveRecord:
@cat.attributes = { :name => "Felix" }
@cat.save
Is the same as:
@cat.update_attributes(:name => "Felix")
By default, attributes without a property will not be updated via the `#attributes=` method. This provents useless data being passed to database, for example from an HTML form. However, if you would like truely
dynamic attributes, the `mass_assign_any_attribute` configuration option when set to true will
store everything you put into the `Base#attributes=` method.
## Property Arrays ## Property Arrays
An attribute may also contain an array of data. CouchRest Model handles this, along An attribute may contain an array of data. CouchRest Model handles this, along
with casting, by defining the class of the child attributes inside an Array: with casting, by defining the class of the child attributes inside an Array:
class Cat < CouchRest::Model::Base class Cat < CouchRest::Model::Base
@ -193,15 +199,14 @@ you'd like to use. For example:
@cat.toys.first.class == CatToy @cat.toys.first.class == CatToy
@cat.toys.first.name == 'mouse' @cat.toys.first.name == 'mouse'
Additionally, any hashes sent to the property will automatically be converted: Any hashes sent to the property will automatically be converted:
@cat.toys << {:name => 'catnip ball'} @cat.toys << {:name => 'catnip ball'}
@cat.toys.last.is_a?(CatToy) # True! @cat.toys.last.is_a?(CatToy) # True!
Of course, to use your own classes they *must* be defined before the parent uses them otherwise To use your own classes they *must* be defined before the parent uses them otherwise
Ruby will bring up a missing constant error. To avoid this, or if you have a really simple array of data Ruby will bring up a missing constant error. To avoid this, or if you have a really simple array of data
you'd like to model, the latest version of CouchRest Model (> 1.0.0) supports creating you'd like to model, CouchRest Model supports creating anonymous classes:
anonymous classes:
class Cat < CouchRest::Model::Base class Cat < CouchRest::Model::Base
property :name, String property :name, String
@ -216,7 +221,7 @@ anonymous classes:
@cat.toys.last.rating == 5 @cat.toys.last.rating == 5
@cat.toys.last.name == 'catnip ball' @cat.toys.last.name == 'catnip ball'
Using this method of anonymous classes will *only* create arrays of objects. Anonymous classes will *only* create arrays of objects.
## Assocations ## Assocations
@ -227,14 +232,67 @@ Two types at the moment:
collection_of :tags collection_of :tags
TODO: Document properly! This is a somewhat controvesial feature of CouchRest Model that some document database purists may fringe at. CouchDB does not yet povide many features to support relationships between documents but the fact of that matter is that its a very useful paradigm for modelling data systems.
In the near future we hope to add support for a `has_many` relationship that takes of the _Linked Documents_ feature that arrived in CouchDB 0.11.
### Belongs To
Creates a property in the document with `_id` added to the end of the name of the foreign model with getter and setter methods to access the model.
Example:
class Cat < CouchRest::Model::Base
belongs_to :mother
property :name
end
kitty = Cat.new(:name => "Felix")
kitty.mother = Mother.find_by_name('Sophie')
Providing a object to the setter, `mother` in the example will automagically update the `mother_id` attribute. Retrieving the data later is just as expected:
kitty = Cat.find_by_name "Felix"
kitty.mother.name == 'Sophie'
Belongs_to accepts a few options to add a bit more felxibility:
* `:class_name` - the camel case string name of the class used to load the model.
* `:foreign_key` - the name of the property to use instead of the attribute name with `_id` on the end.
* `:proxy` - a string that when evaluated provides a proxy model that responds to `#get`.
The last option, `:proxy` is a feature currently in testing that allows objects to be loaded from a proxy class, such as `ClassProxy`. For example:
class Invoice < CouchRest::Model::Base
attr_accessor :company
belongs_to :project, :proxy => 'self.company.projects'
end
A project instance in this scenario would need to be loaded by calling `#get(project_id)` on `self.company.projects` in the scope of an instance of the Invoice. We hope to document and work on this powerful feature in the near future.
### Collection Of
A collection_of relationship is much like belongs_to except that rather than just one foreign key, an array of foreign keys can be stored. This is one of the great features of a document database. This relationship uses a proxy object to automatically update two arrays; one containing the objects being used, and a second with the foreign keys used to the find them.
The best example of this in use is with Labels:
class Invoice < CouchRest::Model::Base
collection_of :labels
end
invoice = Invoice.new
invoice.labels << Label.get('xyz')
invoice.labels << Label.get('abc')
invoice.labels.map{|l| l.name} # produces ['xyz', 'abc']
See the belongs_to relationship for the options that can be used. Note that this isn't especially efficient, a `get` is performed for each model in the array. As with a has_many relationship, we hope to be able to take advantage of the Linked Documents feature to avoid multiple requests.
## Validations ## Validations
CouchRest Model automatically includes the new ActiveModel validations, so they should work just as the traditional Rails CouchRest Model automatically includes the new ActiveModel validations, so they should work just as the traditional Rails validations. For more details, please see the ActiveModel::Validations documentation.
validations. For more details, please see the ActiveModel::Validations documentation.
CouchRest Model adds the possibility to check the uniqueness of attributes using the `validates_uniqueness_of` class method, for example: CouchRest Model adds the possibility to check the uniqueness of attributes using the `validates_uniqueness_of` class method, for example:
@ -251,9 +309,7 @@ you'd like to avoid the typical RestClient Conflict error:
unique_id :code unique_id :code
validates_uniqueness_of :code, :view => 'all' validates_uniqueness_of :code, :view => 'all'
Given that the uniqueness check performs a request to the database, it is also possible Given that the uniqueness check performs a request to the database, it is also possible to include a `:proxy` parameter. This allows you to call a method on the document and provide an alternate proxy object.
to include a @:proxy@ parameter. This allows you to
call a method on the document and provide an alternate proxy object.
Examples: Examples:
@ -264,8 +320,7 @@ Examples:
validates_uniqueness_of :title, :proxy => 'company.people' validates_uniqueness_of :title, :proxy => 'company.people'
A really interesting use of +:proxy+ and +:view+ together could be where A really interesting use of `:proxy` and `:view` together could be where you'd like to ensure the ID is unique between several types of document. For example:
you'd like to ensure the ID is unique between several types of document. For example:
class Product < CouchRest::Model::Base class Product < CouchRest::Model::Base
property :code property :code
@ -290,26 +345,41 @@ you'd like to ensure the ID is unique between several types of document. For exa
Pretty cool! Pretty cool!
## Configuration
CouchRest Model supports a few configuration options. These can be set either for the whole Model code
base or for a specific model of your chosing. To configure globally, provide something similar to the
following in your projects loading code:
CouchRestModel::Model::Base.configure do |config|
config.mass_assign_any_attribute = true
config.model_type_key = 'couchrest-type'
end
To set for a specific model:
class Cat < CouchRest::Model::Base
mass_assign_any_attribute true
end
Options currently avilable are:
* `mass_assign_any_attribute` - false by default, when true any attribute may be updated via the update_attributes or attributes= methods.
* `model_type_key` - 'couchrest-type' by default, is the name of property that holds the class name of each CouchRest Model.
## Notable Issues ## Notable Issues
CouchRest Model uses active_support for some of its internals. Ensure you have a stable active support gem installed None at the moment...
or at least 3.0.0.beta4.
JSON gem versions 1.4.X are kown to cause problems with stack overflows and general badness. Version 1.2.4 appears to work fine.
## Ruby on Rails ## Ruby on Rails
CouchRest Model is compatible with rails and provides some ActiveRecord-like methods. CouchRest Model is compatible with rails and provides some ActiveRecord-like methods.
The CouchRest companion rails project The CouchRest companion rails project [http://github.com/hpoydar/couchrest-rails](http://github.com/hpoydar/couchrest-rails) is great for providing default connection details for your database. At the time of writting however it does not provide explicit support for CouchRest Model.
[http://github.com/hpoydar/couchrest-rails](http://github.com/hpoydar/couchrest-rails) is great
for provided default connection details for your database. At the time of writting however it
does not provide explicit support for CouchRest Model.
CouchRest Model and the original CouchRest ExtendedDocument do not share the same namespace, CouchRest Model and the original CouchRest ExtendedDocument do not share the same namespace, as such you should not have any problems using them both at the same time. This might help with migrations.
as such you should not have any problems using them both at the same time. This might
help with migrations.
### Rails 3.0 ### Rails 3.0
@ -321,9 +391,7 @@ In your Gemfile require the gem with a simple line:
## Testing ## Testing
The most complete documentation is the spec/ directory. To validate your The most complete documentation is the spec/ directory. To validate your CouchRest install, from the project root directory run `rake`, or `autotest` (requires RSpec and optionally ZenTest for autotest support).
CouchRest install, from the project root directory run `rake`, or `autotest`
(requires RSpec and optionally ZenTest for autotest support).
## Docs ## Docs

View file

@ -27,7 +27,7 @@ begin
gemspec.extra_rdoc_files = %w( README.md LICENSE THANKS.md ) gemspec.extra_rdoc_files = %w( README.md LICENSE THANKS.md )
gemspec.files = %w( LICENSE README.md Rakefile THANKS.md history.txt couchrest.gemspec) + Dir["{examples,lib,spec}/**/*"] - Dir["spec/tmp"] gemspec.files = %w( LICENSE README.md Rakefile THANKS.md history.txt couchrest.gemspec) + Dir["{examples,lib,spec}/**/*"] - Dir["spec/tmp"]
gemspec.has_rdoc = true gemspec.has_rdoc = true
gemspec.add_dependency("couchrest", "~> 1.0.0") gemspec.add_dependency("couchrest", "~> 1.0.1")
gemspec.add_dependency("mime-types", "~> 1.15") gemspec.add_dependency("mime-types", "~> 1.15")
gemspec.add_dependency("activemodel", "~> 3.0.0.rc") gemspec.add_dependency("activemodel", "~> 3.0.0.rc")
gemspec.add_dependency("tzinfo", "~> 0.3.22") gemspec.add_dependency("tzinfo", "~> 0.3.22")

View file

@ -9,7 +9,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{2010-08-24} s.date = %q{2010-10-23}
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 = [
@ -25,14 +25,13 @@ Gem::Specification.new do |s|
"history.txt", "history.txt",
"lib/couchrest/model.rb", "lib/couchrest/model.rb",
"lib/couchrest/model/associations.rb", "lib/couchrest/model/associations.rb",
"lib/couchrest/model/attribute_protection.rb",
"lib/couchrest/model/attributes.rb",
"lib/couchrest/model/base.rb", "lib/couchrest/model/base.rb",
"lib/couchrest/model/callbacks.rb", "lib/couchrest/model/callbacks.rb",
"lib/couchrest/model/casted_array.rb", "lib/couchrest/model/casted_array.rb",
"lib/couchrest/model/casted_model.rb", "lib/couchrest/model/casted_model.rb",
"lib/couchrest/model/class_proxy.rb", "lib/couchrest/model/class_proxy.rb",
"lib/couchrest/model/collection.rb", "lib/couchrest/model/collection.rb",
"lib/couchrest/model/configuration.rb",
"lib/couchrest/model/design_doc.rb", "lib/couchrest/model/design_doc.rb",
"lib/couchrest/model/document_queries.rb", "lib/couchrest/model/document_queries.rb",
"lib/couchrest/model/errors.rb", "lib/couchrest/model/errors.rb",
@ -40,6 +39,7 @@ Gem::Specification.new do |s|
"lib/couchrest/model/persistence.rb", "lib/couchrest/model/persistence.rb",
"lib/couchrest/model/properties.rb", "lib/couchrest/model/properties.rb",
"lib/couchrest/model/property.rb", "lib/couchrest/model/property.rb",
"lib/couchrest/model/property_protection.rb",
"lib/couchrest/model/support/couchrest.rb", "lib/couchrest/model/support/couchrest.rb",
"lib/couchrest/model/support/hash.rb", "lib/couchrest/model/support/hash.rb",
"lib/couchrest/model/typecast.rb", "lib/couchrest/model/typecast.rb",
@ -55,13 +55,14 @@ Gem::Specification.new do |s|
"lib/rails/generators/couchrest_model/model/templates/model.rb", "lib/rails/generators/couchrest_model/model/templates/model.rb",
"spec/couchrest/assocations_spec.rb", "spec/couchrest/assocations_spec.rb",
"spec/couchrest/attachment_spec.rb", "spec/couchrest/attachment_spec.rb",
"spec/couchrest/attribute_protection_spec.rb",
"spec/couchrest/base_spec.rb", "spec/couchrest/base_spec.rb",
"spec/couchrest/casted_model_spec.rb", "spec/couchrest/casted_model_spec.rb",
"spec/couchrest/casted_spec.rb", "spec/couchrest/casted_spec.rb",
"spec/couchrest/class_proxy_spec.rb", "spec/couchrest/class_proxy_spec.rb",
"spec/couchrest/configuration_spec.rb",
"spec/couchrest/inherited_spec.rb", "spec/couchrest/inherited_spec.rb",
"spec/couchrest/persistence_spec.rb", "spec/couchrest/persistence_spec.rb",
"spec/couchrest/property_protection_spec.rb",
"spec/couchrest/property_spec.rb", "spec/couchrest/property_spec.rb",
"spec/couchrest/subclass_spec.rb", "spec/couchrest/subclass_spec.rb",
"spec/couchrest/validations.rb", "spec/couchrest/validations.rb",
@ -93,16 +94,17 @@ Gem::Specification.new do |s|
s.homepage = %q{http://github.com/couchrest/couchrest_model} s.homepage = %q{http://github.com/couchrest/couchrest_model}
s.rdoc_options = ["--charset=UTF-8"] s.rdoc_options = ["--charset=UTF-8"]
s.require_paths = ["lib"] s.require_paths = ["lib"]
s.rubygems_version = %q{1.3.6} s.rubygems_version = %q{1.3.7}
s.summary = %q{Extends the CouchRest Document for advanced modelling.} s.summary = %q{Extends the CouchRest Document for advanced modelling.}
s.test_files = [ s.test_files = [
"spec/spec_helper.rb", "spec/spec_helper.rb",
"spec/couchrest/configuration_spec.rb",
"spec/couchrest/property_spec.rb", "spec/couchrest/property_spec.rb",
"spec/couchrest/property_protection_spec.rb",
"spec/couchrest/casted_spec.rb", "spec/couchrest/casted_spec.rb",
"spec/couchrest/subclass_spec.rb", "spec/couchrest/subclass_spec.rb",
"spec/couchrest/persistence_spec.rb", "spec/couchrest/persistence_spec.rb",
"spec/couchrest/casted_model_spec.rb", "spec/couchrest/casted_model_spec.rb",
"spec/couchrest/attribute_protection_spec.rb",
"spec/couchrest/assocations_spec.rb", "spec/couchrest/assocations_spec.rb",
"spec/couchrest/validations.rb", "spec/couchrest/validations.rb",
"spec/couchrest/view_spec.rb", "spec/couchrest/view_spec.rb",
@ -130,15 +132,15 @@ Gem::Specification.new do |s|
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
s.specification_version = 3 s.specification_version = 3
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
s.add_runtime_dependency(%q<couchrest>, ["~> 1.0.0"]) s.add_runtime_dependency(%q<couchrest>, ["~> 1.0.1"])
s.add_runtime_dependency(%q<mime-types>, ["~> 1.15"]) s.add_runtime_dependency(%q<mime-types>, ["~> 1.15"])
s.add_runtime_dependency(%q<activemodel>, ["~> 3.0.0.rc"]) s.add_runtime_dependency(%q<activemodel>, ["~> 3.0.0.rc"])
s.add_runtime_dependency(%q<tzinfo>, ["~> 0.3.22"]) s.add_runtime_dependency(%q<tzinfo>, ["~> 0.3.22"])
s.add_runtime_dependency(%q<railties>, ["~> 3.0.0.rc"]) s.add_runtime_dependency(%q<railties>, ["~> 3.0.0.rc"])
s.add_development_dependency(%q<rspec>, ["~> 2.0.0.beta.19"]) s.add_development_dependency(%q<rspec>, ["~> 2.0.0.beta.19"])
else else
s.add_dependency(%q<couchrest>, ["~> 1.0.0"]) s.add_dependency(%q<couchrest>, ["~> 1.0.1"])
s.add_dependency(%q<mime-types>, ["~> 1.15"]) s.add_dependency(%q<mime-types>, ["~> 1.15"])
s.add_dependency(%q<activemodel>, ["~> 3.0.0.rc"]) s.add_dependency(%q<activemodel>, ["~> 3.0.0.rc"])
s.add_dependency(%q<tzinfo>, ["~> 0.3.22"]) s.add_dependency(%q<tzinfo>, ["~> 0.3.22"])
@ -146,7 +148,7 @@ Gem::Specification.new do |s|
s.add_dependency(%q<rspec>, ["~> 2.0.0.beta.19"]) s.add_dependency(%q<rspec>, ["~> 2.0.0.beta.19"])
end end
else else
s.add_dependency(%q<couchrest>, ["~> 1.0.0"]) s.add_dependency(%q<couchrest>, ["~> 1.0.1"])
s.add_dependency(%q<mime-types>, ["~> 1.15"]) s.add_dependency(%q<mime-types>, ["~> 1.15"])
s.add_dependency(%q<activemodel>, ["~> 3.0.0.rc"]) s.add_dependency(%q<activemodel>, ["~> 3.0.0.rc"])
s.add_dependency(%q<tzinfo>, ["~> 0.3.22"]) s.add_dependency(%q<tzinfo>, ["~> 0.3.22"])

View file

@ -1,11 +1,21 @@
== Next Version == Next Version
* Major enhancements * Major enhancements
* Dirty Tracking via ActiveModel * Support for configuration module and "model_type_key" option for overriding model's type key
* ActiveModel Attribute Methods support * Added "mass_assign_any_attribute" configuration option to allow setting anything via the attribute= method.
* Minor enhancements * Minor enhancements
* Fixing find("") issue (thanks epochwolf) * Fixing find("") issue (thanks epochwolf)
* Altered protected attributes so that hash provided to #attributes= is not modified
* Altering typecasting for floats to better handle commas and points
* Fixing the lame pagination bug where database url (and pass!!) were included in view requests (Thanks James Hayton)
Notes:
* 2010-10-22 @samlown:
* ActiveModel Attribute support was added but has now been removed due to major performance issues.
Until these have been resolved (if possible?!) they should not be included. See the
'active_model_attrs' if you'd like to test.
== CouchRest Model 1.0.0.beta8 == CouchRest Model 1.0.0.beta8

View file

@ -1,132 +0,0 @@
module CouchRest
module Model
ReadOnlyPropertyError = Class.new(StandardError)
# Attributes Suffixes provide methods from ActiveModel
# to hook into. See methods such as #attribute= and
# #attribute? for their implementation
AttributeMethodSuffixes = ['', '=', '?']
module Attributes
extend ActiveSupport::Concern
included do
include ActiveModel::AttributeMethods
attribute_method_suffix *AttributeMethodSuffixes
end
module ClassMethods
def attributes
properties.map {|prop| prop.name}
end
end
def initialize(*args)
self.class.attribute_method_suffix *AttributeMethodSuffixes
super
end
def attributes
self.class.attributes
end
## Reads the attribute value.
# Assuming you have a property :title this would be called
# by `model_instance.title`
def attribute(name)
read_attribute(name)
end
## Sets the attribute value.
# Assuming you have a property :title this would be called
# by `model_instance.title = 'hello'`
def attribute=(name, value)
raise ReadOnlyPropertyError, 'read only property' if find_property!(name).read_only
write_attribute(name, value)
end
## Tests for both presence and truthiness of the attribute.
# Assuming you have a property :title # this would be called
# by `model_instance.title?`
def attribute?(name)
value = read_attribute(name)
!(value.nil? || value == false)
end
## Support for handling attributes
#
# This would be better in the properties file, but due to scoping issues
# this is not yet possible.
def prepare_all_attributes(doc = {}, options = {})
apply_all_property_defaults
if options[:directly_set_attributes]
directly_set_read_only_attributes(doc)
else
remove_protected_attributes(doc)
end
directly_set_attributes(doc) unless doc.nil?
end
# Takes a hash as argument, and applies the values by using writer methods
# for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are
# missing. In case of error, no attributes are changed.
def update_attributes_without_saving(hash)
# Remove any protected and update all the rest. Any attributes
# which do not have a property will simply be ignored.
attrs = remove_protected_attributes(hash)
directly_set_attributes(attrs)
end
alias :attributes= :update_attributes_without_saving
def read_attribute(property)
prop = find_property!(property)
self[prop.to_s]
end
def write_attribute(property, value)
prop = find_property!(property)
self[prop.to_s] = prop.cast(self, value)
end
private
def read_only_attributes
properties.select { |prop| prop.read_only }.map { |prop| prop.name }
end
def directly_set_attributes(hash)
r_o_a = read_only_attributes
hash.each do |attribute_name, attribute_value|
next if r_o_a.include? attribute_name
if self.respond_to?("#{attribute_name}=")
self.send("#{attribute_name}=", hash.delete(attribute_name))
end
end
end
def directly_set_read_only_attributes(hash)
r_o_a = read_only_attributes
property_list = attributes
hash.each do |attribute_name, attribute_value|
next unless r_o_a.include? attribute_name
if property_list.include?(attribute_name)
write_attribute(attribute_name, hash.delete(attribute_name))
end
end
end
def set_attributes(hash)
attrs = remove_protected_attributes(hash)
directly_set_attributes(attrs)
end
def check_properties_exist(attrs)
property_list = attributes
attrs.each do |attribute_name, attribute_value|
raise NoMethodError, "Property #{attribute_name} not created" unless respond_to?("#{attribute_name}=") or property_list.include?(attribute_name)
end
end
end
end
end

View file

@ -4,24 +4,23 @@ module CouchRest
extend ActiveModel::Naming extend ActiveModel::Naming
include CouchRest::Model::Configuration
include CouchRest::Model::Persistence include CouchRest::Model::Persistence
include CouchRest::Model::Callbacks 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
include CouchRest::Model::ExtendedAttachments include CouchRest::Model::ExtendedAttachments
include CouchRest::Model::ClassProxy include CouchRest::Model::ClassProxy
include CouchRest::Model::Collection include CouchRest::Model::Collection
include CouchRest::Model::AttributeProtection include CouchRest::Model::PropertyProtection
include CouchRest::Model::Attributes
include CouchRest::Model::Associations include CouchRest::Model::Associations
include CouchRest::Model::Validations include CouchRest::Model::Validations
include CouchRest::Model::Dirty
def self.subclasses def self.subclasses
@subclasses ||= [] @subclasses ||= []
end end
def self.inherited(subklass) def self.inherited(subklass)
super super
subklass.send(:include, CouchRest::Model::Properties) subklass.send(:include, CouchRest::Model::Properties)
@ -35,7 +34,7 @@ module CouchRest
EOS EOS
subclasses << subklass subclasses << subklass
end end
# Accessors # Accessors
attr_accessor :casted_by attr_accessor :casted_by
@ -44,19 +43,19 @@ module CouchRest
# using the provided document hash. # using the provided document hash.
# #
# Options supported: # Options supported:
# #
# * :directly_set_attributes: true when data comes directly from database # * :directly_set_attributes: true when data comes directly from database
# #
def initialize(doc = {}, options = {}) def initialize(doc = {}, options = {})
prepare_all_attributes(doc, options) doc = prepare_all_attributes(doc, options)
super(doc) super(doc)
unless self['_id'] && self['_rev'] unless self['_id'] && self['_rev']
self['couchrest-type'] = self.class.to_s self[self.model_type_key] = self.class.to_s
end end
after_initialize if respond_to?(:after_initialize) after_initialize if respond_to?(:after_initialize)
end end
# Temp solution to make the view_by methods available # Temp solution to make the view_by methods available
def self.method_missing(m, *args, &block) def self.method_missing(m, *args, &block)
if has_view?(m) if has_view?(m)
@ -70,9 +69,9 @@ module CouchRest
end end
super super
end end
### instance methods ### instance methods
# Gets a reference to the actual document in the DB # Gets a reference to the actual document in the DB
# Calls up to the next document if there is one, # Calls up to the next document if there is one,
# Otherwise we're at the top and we return self # Otherwise we're at the top and we return self
@ -80,14 +79,14 @@ module CouchRest
return self if base_doc? return self if base_doc?
@casted_by.base_doc @casted_by.base_doc
end end
# Checks if we're the top document # Checks if we're the top document
def base_doc? def base_doc?
!@casted_by !@casted_by
end end
## Compatibility with ActiveSupport and older frameworks ## Compatibility with ActiveSupport and older frameworks
# Hack so that CouchRest::Document, which descends from Hash, # Hack so that CouchRest::Document, which descends from Hash,
# doesn't appear to Rails routing as a Hash of options # doesn't appear to Rails routing as a Hash of options
def is_a?(klass) def is_a?(klass)
@ -99,14 +98,14 @@ module CouchRest
def persisted? def persisted?
!new? !new?
end end
def to_key def to_key
new? ? nil : [id] new? ? nil : [id]
end end
alias :to_param :id alias :to_param :id
alias :new_record? :new? alias :new_record? :new?
alias :new_document? :new? alias :new_document? :new?
end end
end end
end end

View file

@ -1,39 +1,39 @@
module CouchRest::Model module CouchRest::Model
module CastedModel module CastedModel
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
include CouchRest::Model::AttributeProtection include CouchRest::Model::Configuration
include CouchRest::Model::Attributes
include CouchRest::Model::Callbacks include CouchRest::Model::Callbacks
include CouchRest::Model::Properties include CouchRest::Model::Properties
include CouchRest::Model::PropertyProtection
include CouchRest::Model::Associations include CouchRest::Model::Associations
include CouchRest::Model::Validations include CouchRest::Model::Validations
attr_accessor :casted_by attr_accessor :casted_by
end end
def initialize(keys = {}) def initialize(keys = {})
raise StandardError unless self.is_a? Hash raise StandardError unless self.is_a? Hash
prepare_all_attributes(keys) prepare_all_attributes(keys)
super() super()
end end
def []= key, value def []= key, value
super(key.to_s, value) super(key.to_s, value)
end end
def [] key def [] key
super(key.to_s) super(key.to_s)
end end
# Gets a reference to the top level extended # Gets a reference to the top level extended
# document that a model is saved inside of # document that a model is saved inside of
def base_doc def base_doc
return nil unless @casted_by return nil unless @casted_by
@casted_by.base_doc @casted_by.base_doc
end end
# False if the casted model has already # False if the casted model has already
# been saved in the containing document # been saved in the containing document
def new? def new?
@ -53,12 +53,12 @@ module CouchRest::Model
end end
alias :to_key :id alias :to_key :id
alias :to_param :id alias :to_param :id
# Sets the attributes from a hash # Sets the attributes from a hash
def update_attributes_without_saving(hash) def update_attributes_without_saving(hash)
hash.each do |k, v| hash.each do |k, v|
raise NoMethodError, "#{k}= method not available, use property :#{k}" unless self.respond_to?("#{k}=") raise NoMethodError, "#{k}= method not available, use property :#{k}" unless self.respond_to?("#{k}=")
end end
hash.each do |k, v| hash.each do |k, v|
self.send("#{k}=",v) self.send("#{k}=",v)
end end

View file

@ -82,6 +82,7 @@ module CouchRest
design_doc['views'][view_name.to_s] && design_doc['views'][view_name.to_s] &&
design_doc['views'][view_name.to_s]["couchrest-defaults"]) || {} design_doc['views'][view_name.to_s]["couchrest-defaults"]) || {}
view_options = default_view_options.merge(options) view_options = default_view_options.merge(options)
view_options.delete(:database)
[design_doc, view_name, view_options] [design_doc, view_name, view_options]
end end
@ -94,6 +95,8 @@ module CouchRest
raise ArgumentError, 'search_name is required' if search_name.nil? raise ArgumentError, 'search_name is required' if search_name.nil?
search_options = options.clone search_options = options.clone
search_options.delete(:database)
[design_doc, search_name, search_options] [design_doc, search_name, search_options]
end end

View file

@ -0,0 +1,51 @@
module CouchRest
# CouchRest Model Configuration support, stolen from Carrierwave by jnicklas
# http://github.com/jnicklas/carrierwave/blob/master/lib/carrierwave/uploader/configuration.rb
module Model
module Configuration
extend ActiveSupport::Concern
included do
add_config :model_type_key
add_config :mass_assign_any_attribute
configure do |config|
config.model_type_key = 'couchrest-type' # 'model'?
config.mass_assign_any_attribute = false
end
end
module ClassMethods
def add_config(name)
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def self.#{name}(value=nil)
@#{name} = value if value
return @#{name} if self.object_id == #{self.object_id} || defined?(@#{name})
name = superclass.#{name}
return nil if name.nil? && !instance_variable_defined?("@#{name}")
@#{name} = name && !name.is_a?(Module) && !name.is_a?(Symbol) && !name.is_a?(Numeric) && !name.is_a?(TrueClass) && !name.is_a?(FalseClass) ? name.dup : name
end
def self.#{name}=(value)
@#{name} = value
end
def #{name}
self.class.#{name}
end
RUBY
end
def configure
yield self
end
end
end
end
end

View file

@ -31,7 +31,7 @@ module CouchRest
"views" => { "views" => {
'all' => { 'all' => {
'map' => "function(doc) { 'map' => "function(doc) {
if (doc['couchrest-type'] == '#{self.to_s}') { if (doc['#{self.model_type_key}'] == '#{self.to_s}') {
emit(doc['_id'],1); emit(doc['_id'],1);
} }
}" }"

View file

@ -1,44 +0,0 @@
# encoding: utf-8
require 'active_model/dirty'
module CouchRest #:nodoc:
module Model #:nodoc:
# Dirty Tracking support via ActiveModel
# mixin methods include:
# #changed?, #changed, #changes, #previous_changes
# #<attribute>_changed?, #<attribute>_change,
# #reset_<attribute>!, #<attribute>_will_change!,
# and #<attribute>_was
#
# Please see the specs or the documentation of
# ActiveModel::Dirty for more information
module Dirty
extend ActiveSupport::Concern
included do
include ActiveModel::Dirty
after_save :clear_changed_attributes
end
def initialize(*args)
super
@changed_attributes.clear if @changed_attributes
end
def write_attribute(name, value)
meth = :"#{name}_will_change!"
__send__ meth if respond_to? meth
super
end
private
def clear_changed_attributes
@previously_changed = changes
@changed_attributes.clear
true
end
end
end
end

View file

@ -8,21 +8,21 @@ module CouchRest
module ClassMethods module ClassMethods
# Load all documents that have the "couchrest-type" field equal to the # Load all documents that have the model_type_key's field equal to the
# name of the current class. Take the standard set of # name of the current class. Take the standard set of
# CouchRest::Database#view options. # CouchRest::Database#view options.
def all(opts = {}, &block) def all(opts = {}, &block)
view(:all, opts, &block) view(:all, opts, &block)
end end
# Returns the number of documents that have the "couchrest-type" field # Returns the number of documents that have the model_type_key's field
# equal to the name of the current class. Takes the standard set of # equal to the name of the current class. Takes the standard set of
# CouchRest::Database#view options # CouchRest::Database#view options
def count(opts = {}, &block) def count(opts = {}, &block)
all({:raw => true, :limit => 0}.merge(opts), &block)['total_rows'] all({:raw => true, :limit => 0}.merge(opts), &block)['total_rows']
end end
# Load the first document that have the "couchrest-type" field equal to # Load the first document that have the model_type_key's field equal to
# the name of the current class. # the name of the current class.
# #
# ==== Returns # ==== Returns

View file

@ -97,7 +97,7 @@ module CouchRest
# ==== Returns # ==== Returns
# a document instance # a document instance
def create_from_database(doc = {}) def create_from_database(doc = {})
base = (doc['couchrest-type'].blank? || doc['couchrest-type'] == self.to_s) ? self : doc['couchrest-type'].constantize base = (doc[model_type_key].blank? || doc[model_type_key] == self.to_s) ? self : doc[model_type_key].constantize
base.new(doc, :directly_set_attributes => true) base.new(doc, :directly_set_attributes => true)
end end

View file

@ -1,18 +1,13 @@
# encoding: utf-8 # encoding: utf-8
require 'set'
module CouchRest module CouchRest
module Model module Model
module Properties module Properties
extend ActiveSupport::Concern
class IncludeError < StandardError; end included do
extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties)
def self.included(base) self.properties ||= []
base.class_eval <<-EOS, __FILE__, __LINE__ + 1 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?(:[]=))
extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties)
self.properties ||= []
EOS
base.extend(ClassMethods)
raise CouchRest::Mixins::Properties::IncludeError, "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 (base.new.respond_to?(:[]) && base.new.respond_to?(:[]=))
end end
# Returns the Class properties # Returns the Class properties
@ -23,12 +18,45 @@ module CouchRest
self.class.properties self.class.properties
end end
# Returns the Class properties with their values
#
# ==== Returns
# Array:: the list of properties with their values
def properties_with_values def properties_with_values
props = {} props = {}
properties.each { |property| props[property.name] = read_attribute(property.name) } properties.each { |property| props[property.name] = read_attribute(property.name) }
props props
end end
# Read the casted value of an attribute defined with a property.
#
# ==== Returns
# Object:: the casted attibutes value.
def read_attribute(property)
self[find_property!(property).to_s]
end
# Store a casted value in the current instance of an attribute defined
# with a property.
def write_attribute(property, value)
prop = find_property!(property)
self[prop.to_s] = prop.is_a?(String) ? value : prop.cast(self, value)
end
# Takes a hash as argument, and applies the values by using writer methods
# for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are
# missing. In case of error, no attributes are changed.
def update_attributes_without_saving(hash)
# Remove any protected and update all the rest. Any attributes
# which do not have a property will simply be ignored.
attrs = remove_protected_attributes(hash)
directly_set_attributes(attrs)
end
alias :attributes= :update_attributes_without_saving
private
# The following methods should be accessable by the Model::Base Class, but not by anything else!
def apply_all_property_defaults def apply_all_property_defaults
return if self.respond_to?(:new?) && (new? == false) return if self.respond_to?(:new?) && (new? == false)
# TODO: cache the default object # TODO: cache the default object
@ -37,13 +65,54 @@ module CouchRest
end end
end end
private def prepare_all_attributes(doc = {}, options = {})
apply_all_property_defaults
if options[:directly_set_attributes]
directly_set_read_only_attributes(doc)
else
doc = remove_protected_attributes(doc)
end
directly_set_attributes(doc) unless doc.nil?
end
def find_property!(property) def find_property!(property)
prop = property.is_a?(Property) ? property : self.class.properties.detect {|p| p.to_s == property.to_s} prop = property.is_a?(Property) ? property : self.class.properties.detect {|p| p.to_s == property.to_s}
raise ArgumentError, "Missing property definition for #{property.to_s}" unless prop raise ArgumentError, "Missing property definition for #{property.to_s}" if prop.nil?
prop prop
end end
# Set all the attributes and return a hash with the attributes
# that have not been accepted.
def directly_set_attributes(hash)
hash.reject do |attribute_name, attribute_value|
if self.respond_to?("#{attribute_name}=")
self.send("#{attribute_name}=", attribute_value)
true
elsif mass_assign_any_attribute # config option
self[attribute_name] = attribute_value
true
else
false
end
end
end
def directly_set_read_only_attributes(hash)
property_list = self.properties.map{|p| p.name}
hash.each do |attribute_name, attribute_value|
next if self.respond_to?("#{attribute_name}=")
if property_list.include?(attribute_name)
write_attribute(attribute_name, hash.delete(attribute_name))
end
end
end
def set_attributes(hash)
attrs = remove_protected_attributes(hash)
directly_set_attributes(attrs)
end
module ClassMethods module ClassMethods
def property(name, *options, &block) def property(name, *options, &block)
@ -91,7 +160,8 @@ module CouchRest
type = [type] # inject as an array type = [type] # inject as an array
end end
property = Property.new(name, type, options) property = Property.new(name, type, options)
create_property_alias(property) if property.alias create_property_getter(property)
create_property_setter(property) unless property.read_only == true
if property.type_class.respond_to?(:validates_casted_model) if property.type_class.respond_to?(:validates_casted_model)
validates_casted_model property.name validates_casted_model property.name
end end
@ -99,15 +169,49 @@ module CouchRest
property property
end end
def create_property_alias(property) # defines the getter for the property (and optional aliases)
def create_property_getter(property)
# meth = property.name
class_eval <<-EOS, __FILE__, __LINE__ + 1 class_eval <<-EOS, __FILE__, __LINE__ + 1
def #{property.alias.to_s} def #{property.name}
#{property.name} read_attribute('#{property.name}')
end end
EOS EOS
if ['boolean', TrueClass.to_s.downcase].include?(property.type.to_s.downcase)
class_eval <<-EOS, __FILE__, __LINE__
def #{property.name}?
value = read_attribute('#{property.name}')
!(value.nil? || value == false)
end
EOS
end
if property.alias
class_eval <<-EOS, __FILE__, __LINE__ + 1
alias #{property.alias.to_sym} #{property.name.to_sym}
EOS
end
end
# defines the setter for the property (and optional aliases)
def create_property_setter(property)
property_name = property.name
class_eval <<-EOS
def #{property_name}=(value)
write_attribute('#{property_name}', value)
end
EOS
if property.alias
class_eval <<-EOS
alias #{property.alias.to_sym}= #{property_name.to_sym}=
EOS
end
end end
end # module ClassMethods end # module ClassMethods
end end
end end
end end

View file

@ -58,7 +58,8 @@ module CouchRest::Model
if default.class == Proc if default.class == Proc
default.call default.call
else else
Marshal.load(Marshal.dump(default)) # Marshal.load(Marshal.dump(default)) # Removed as there are no failing tests and caused mutex errors
default
end end
end end

View file

@ -1,7 +1,9 @@
module CouchRest module CouchRest
module Model module Model
module AttributeProtection module PropertyProtection
# Attribute protection from mass assignment to CouchRest::Model properties extend ActiveSupport::Concern
# Property protection from mass assignment to CouchRest::Model properties
# #
# Protected methods will be removed from # Protected methods will be removed from
# * new # * new
@ -20,7 +22,7 @@ module CouchRest
# #
# 3) Mix and match, and assume all unspecified properties are protected. # 3) Mix and match, and assume all unspecified properties are protected.
# property :name, :accessible => true # property :name, :accessible => true
# property :admin, :protected => true # property :admin, :protected => true # ignored
# property :phone # this will be automatically protected # property :phone # this will be automatically protected
# #
# Note: the timestamps! method protectes the created_at and updated_at properties # Note: the timestamps! method protectes the created_at and updated_at properties
@ -32,11 +34,16 @@ module CouchRest
module ClassMethods module ClassMethods
def accessible_properties def accessible_properties
properties.select { |prop| prop.options[:accessible] } props = properties.select { |prop| prop.options[:accessible] }
if props.empty?
props = properties.select { |prop| !prop.options[:protected] }
end
props
end end
def protected_properties def protected_properties
properties.select { |prop| prop.options[:protected] } accessibles = accessible_properties
properties.reject { |prop| accessibles.include?(prop) }
end end
end end
@ -48,28 +55,17 @@ module CouchRest
self.class.protected_properties self.class.protected_properties
end end
# Return a new copy of the attributes hash with protected attributes
# removed.
def remove_protected_attributes(attributes) def remove_protected_attributes(attributes)
protected_names = properties_to_remove_from_mass_assignment.map { |prop| prop.name } protected_names = protected_properties.map { |prop| prop.name }
return attributes if protected_names.empty? return attributes if protected_names.empty? or attributes.nil?
attributes.reject! do |property_name, property_value| attributes.reject do |property_name, property_value|
protected_names.include?(property_name.to_s) protected_names.include?(property_name.to_s)
end if attributes
attributes || {}
end
private
def properties_to_remove_from_mass_assignment
to_remove = protected_properties
unless accessible_properties.empty?
to_remove += properties.reject { |prop| prop.options[:accessible] }
end end
to_remove
end end
end end
end end
end end

View file

@ -79,7 +79,7 @@ module CouchRest
# Match numeric string # Match numeric string
def typecast_to_numeric(value, method) def typecast_to_numeric(value, method)
if value.respond_to?(:to_str) if value.respond_to?(:to_str)
if value.to_str =~ /\A(-?(?:0|[1-9]\d*)(?:\.\d+)?|(?:\.\d+))\z/ if value.gsub(/,/, '.').gsub(/\.(?!\d*\Z)/, '').to_str =~ /\A(-?(?:0|[1-9]\d*)(?:\.\d+)?|(?:\.\d+))\z/
$1.send(method) $1.send(method)
else else
value value

190
lib/couchrest/model/view.rb Normal file
View file

@ -0,0 +1,190 @@
#### NOTE Work in progress! Not yet used!
module CouchRest
module Model
# A proxy class that allows view queries to be created using
# chained method calls. After each call a new instance of the method
# is created based on the original in a similar fashion to ruby's sequel
# library, or Rails 3's Arel.
#
# CouchDB views have inherent limitations, so joins and filters as used in
# a normal relational database are not possible. At least not yet!
#
#
#
class View
attr_accessor :query, :design, :database, :name
# Initialize a new View object. This method should not be called from outside CouchRest Model.
def initialize(parent, new_query = {}, name = nil)
if parent.is_a? Base
raise "Name must be provided for view to be initialized" if name.nil?
@name = name
@database = parent.database
@query = { :reduce => false }
elsif parent.is_a? View
@database = parent.database
@query = parent.query.dup
else
raise "View cannot be initialized without a parent Model or View"
end
@query.update(new_query)
super
end
# == View Execution Methods
#
# Send a request to the CouchDB database using the current query values.
# Inmediatly send a request to the database for all documents provided by the query.
#
def all(&block)
args = include_docs.query
end
# Inmediatly send a request for the first result of the dataset. This will override
# any limit set in the view previously.
def first(&block)
args = limit(1).include_docs.query
end
def info
end
def offset
end
def total_rows
end
def rows
end
# == View Filter Methods
#
# View filters return an copy of the view instance with the query
# modified appropriatly. Errors will be raised if the methods
# are combined in an incorrect fashion.
#
# Find all entries in the index whose key matches the value provided.
#
# Cannot be used when the +#startkey+ or +#endkey+ have been set.
def key(value)
raise "View#key cannot be used when startkey or endkey have been set" unless query[:startkey].nil? && query[:endkey].nil?
update_query(:key => value)
end
# Find all index keys that start with the value provided. May or may not be used in
# conjunction with the +endkey+ option.
#
# When the +#descending+ option is used (not the default), the start and end keys should
# be reversed.
#
# Cannot be used if the key has been set.
def startkey(value)
raise "View#startkey cannot be used when key has been set" unless query[:key].nil?
update_query(:startkey => value)
end
# The result set should start from the position of the provided document.
# The value may be provided as an object that responds to the +#id+ call
# or a string.
def startkey_doc(value)
update_query(:startkey_docid => value.is_a?(String) ? value : value.id
end
# The opposite of +#startkey+, finds all index entries whose key is before the value specified.
#
# See the +#startkey+ method for more details and the +#inclusive_end+ option.
def endkey(value)
raise "View#endkey cannot be used when key has been set" unless query[:key].nil?
update_query(:endkey => value)
end
# The result set should end at the position of the provided document.
# The value may be provided as an object that responds to the +#id+ call
# or a string.
def endkey_doc(value)
update_query(:endkey_docid => value.is_a?(String) ? value : value.id
end
# The results should be provided in descending order.
#
# Descending is false by default, this method will enable it and cannot be undone.
def descending
update_query(:descending => true)
end
# Limit the result set to the value supplied.
def limit(value)
update_query(:limit => value)
end
# Skip the number of entries in the index specified by value. This would be
# the equivilent of an offset in SQL.
#
# The CouchDB documentation states that the skip option should not be used
# with large data sets as it is inefficient. Use the +startkey_doc+ method
# instead to skip ranges efficiently.
def skip(value = 0)
update_query(:skip => value)
end
# Use the reduce function on the view. If none is available this method will fail.
def reduce
update_query(:reduce => true)
end
# Control whether the reduce function reduces to a set of distinct keys or to a single
# result row.
#
# By default the value is false, and can only be set when the view's +#reduce+ option
# has been set.
def group
raise "View#reduce must have been set before grouping is permitted" unless query[:reduce]
update_query(:group => true)
end
def group_level(value)
raise "View#reduce and View#group must have been set before group_level is called" unless query[:reduce] && query[:group]
update_query(:group_level => value.to_i)
end
protected
def update_query(new_query = {})
self.class.new(self, new_query)
end
# Used internally to ensure that docs are provided. Should not be used outside of
# the view class under normal circumstances.
def include_docs
raise "Documents cannot be returned from a view that is prepared for a reduce" if query[:reduce]
update_query(:include_docs => true)
end
def execute(&block)
end
end
end
end

View file

@ -23,7 +23,7 @@ module CouchRest
# view_by :tags, # view_by :tags,
# :map => # :map =>
# "function(doc) { # "function(doc) {
# if (doc['couchrest-type'] == 'Post' && doc.tags) { # if (doc['model'] == 'Post' && doc.tags) {
# doc.tags.forEach(function(tag){ # doc.tags.forEach(function(tag){
# emit(doc.tag, 1); # emit(doc.tag, 1);
# }); # });
@ -39,7 +39,7 @@ module CouchRest
# function: # function:
# #
# function(doc) { # function(doc) {
# if (doc['couchrest-type'] == 'Post' && doc.date) { # if (doc['model'] == 'Post' && doc.date) {
# emit(doc.date, null); # emit(doc.date, null);
# } # }
# } # }
@ -77,7 +77,7 @@ module CouchRest
ducktype = opts.delete(:ducktype) ducktype = opts.delete(:ducktype)
unless ducktype || opts[:map] unless ducktype || opts[:map]
opts[:guards] ||= [] opts[:guards] ||= []
opts[:guards].push "(doc['couchrest-type'] == '#{self.to_s}')" opts[:guards].push "(doc['#{model_type_key}'] == '#{self.to_s}')"
end end
keys.push opts keys.push opts
design_doc.view_by(*keys) design_doc.view_by(*keys)

View file

@ -4,9 +4,9 @@ require "active_model/railtie"
module CouchrestModel module CouchrestModel
# = Active Record Railtie # = Active Record Railtie
class Railtie < Rails::Railtie class Railtie < Rails::Railtie
config.generators.orm :couchrest config.generators.orm :couchrest_model
config.generators.test_framework :test_unit, :fixture => false config.generators.test_framework :test_unit, :fixture => false
end end
end end

View file

@ -35,6 +35,7 @@ require 'couchrest/model/errors'
require "couchrest/model/persistence" require "couchrest/model/persistence"
require "couchrest/model/typecast" require "couchrest/model/typecast"
require "couchrest/model/property" require "couchrest/model/property"
require "couchrest/model/property_protection"
require "couchrest/model/casted_array" require "couchrest/model/casted_array"
require "couchrest/model/properties" require "couchrest/model/properties"
require "couchrest/model/validations" require "couchrest/model/validations"
@ -45,10 +46,8 @@ require "couchrest/model/design_doc"
require "couchrest/model/extended_attachments" require "couchrest/model/extended_attachments"
require "couchrest/model/class_proxy" require "couchrest/model/class_proxy"
require "couchrest/model/collection" require "couchrest/model/collection"
require "couchrest/model/attribute_protection"
require "couchrest/model/attributes"
require "couchrest/model/associations" require "couchrest/model/associations"
require "couchrest/model/dirty" require "couchrest/model/configuration"
# Monkey patches applied to couchrest # Monkey patches applied to couchrest
require "couchrest/model/support/couchrest" require "couchrest/model/support/couchrest"

View file

@ -8,23 +8,23 @@ require File.join(FIXTURE_PATH, 'more', 'card')
require File.join(FIXTURE_PATH, 'base') require File.join(FIXTURE_PATH, 'base')
describe "Model Base" do describe "Model Base" do
before(:each) do before(:each) do
@obj = WithDefaultValues.new @obj = WithDefaultValues.new
end end
describe "instance database connection" do describe "instance database connection" do
it "should use the default database" do it "should use the default database" do
@obj.database.name.should == 'couchrest-model-test' @obj.database.name.should == 'couchrest-model-test'
end end
it "should override the default db" do it "should override the default db" do
@obj.database = TEST_SERVER.database!('couchrest-extendedmodel-test') @obj.database = TEST_SERVER.database!('couchrest-extendedmodel-test')
@obj.database.name.should == 'couchrest-extendedmodel-test' @obj.database.name.should == 'couchrest-extendedmodel-test'
@obj.database.delete! @obj.database.delete!
end end
end end
describe "a new model" do describe "a new model" do
it "should be a new document" do it "should be a new document" do
@obj = Basic.new @obj = Basic.new
@ -36,13 +36,13 @@ describe "Model Base" do
it "should not failed on a nil value in argument" do it "should not failed on a nil value in argument" do
@obj = Basic.new(nil) @obj = Basic.new(nil)
@obj.should == { 'couchrest-type' => 'Basic' } @obj.should_not be_nil
end end
end end
describe "ActiveModel compatability Basic" do describe "ActiveModel compatability Basic" do
before(:each) do before(:each) do
@obj = Basic.new(nil) @obj = Basic.new(nil)
end end
@ -86,7 +86,7 @@ describe "Model Base" do
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 == true
end end
end end
end end
@ -100,7 +100,7 @@ describe "Model Base" do
end end
describe "update attributes without saving" do describe "update attributes without saving" do
before(:each) do before(:each) do
a = Article.get "big-bad-danger" rescue nil a = Article.get "big-bad-danger" rescue nil
@ -134,22 +134,22 @@ describe "Model Base" do
@art.attributes = {'date' => Time.now, :title => "something else"} @art.attributes = {'date' => Time.now, :title => "something else"}
@art['title'].should == "something else" @art['title'].should == "something else"
end end
it "should not flip out if an attribute= method is missing and ignore it" do it "should not flip out if an attribute= method is missing and ignore it" do
lambda { lambda {
@art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger") @art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger")
}.should_not raise_error }.should_not raise_error
@art.slug.should == "big-bad-danger" @art.slug.should == "big-bad-danger"
end end
#it "should not change other attributes if there is an error" do #it "should not change other attributes if there is an error" do
# lambda { # lambda {
# @art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger") # @art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger")
# }.should raise_error # }.should raise_error
# @art['title'].should == "big bad danger" # @art['title'].should == "big bad danger"
#end #end
end end
describe "update attributes" do describe "update attributes" do
before(:each) do before(:each) do
a = Article.get "big-bad-danger" rescue nil a = Article.get "big-bad-danger" rescue nil
@ -164,7 +164,7 @@ describe "Model Base" do
loaded['title'].should == "super danger" loaded['title'].should == "super danger"
end end
end end
describe "with default" do describe "with default" do
it "should have the default value set at initalization" do it "should have the default value set at initalization" do
@obj.preset.should == {:right => 10, :top_align => false} @obj.preset.should == {:right => 10, :top_align => false}
@ -173,23 +173,23 @@ describe "Model Base" do
it "should have the default false value explicitly assigned" do it "should have the default false value explicitly assigned" do
@obj.default_false.should == false @obj.default_false.should == false
end end
it "should automatically call a proc default at initialization" do it "should automatically call a proc default at initialization" do
@obj.set_by_proc.should be_an_instance_of(Time) @obj.set_by_proc.should be_an_instance_of(Time)
@obj.set_by_proc.should == @obj.set_by_proc @obj.set_by_proc.should == @obj.set_by_proc
@obj.set_by_proc.should < Time.now @obj.set_by_proc.should < Time.now
end end
it "should let you overwrite the default values" do it "should let you overwrite the default values" do
obj = WithDefaultValues.new(:preset => 'test') obj = WithDefaultValues.new(:preset => 'test')
obj.preset = 'test' obj.preset = 'test'
end end
it "should work with a default empty array" do it "should work with a default empty array" do
obj = WithDefaultValues.new(:tags => ['spec']) obj = WithDefaultValues.new(:tags => ['spec'])
obj.tags.should == ['spec'] obj.tags.should == ['spec']
end end
it "should set default value of read-only property" do it "should set default value of read-only property" do
obj = WithDefaultValues.new obj = WithDefaultValues.new
obj.read_only_with_default.should == 'generic' obj.read_only_with_default.should == 'generic'
@ -207,7 +207,7 @@ describe "Model Base" do
obj.tags.should == ['spec'] obj.tags.should == ['spec']
end end
end end
describe "a doc with template values (CR::Model spec)" do describe "a doc with template values (CR::Model spec)" do
before(:all) do before(:all) do
WithTemplateAndUniqueID.all.map{|o| o.destroy} WithTemplateAndUniqueID.all.map{|o| o.destroy}
@ -228,8 +228,8 @@ describe "Model Base" do
tmpl2_reloaded.preset.should == 'not_value' tmpl2_reloaded.preset.should == 'not_value'
end end
end end
describe "finding all instances of a model" do describe "finding all instances of a model" do
before(:all) do before(:all) do
WithTemplateAndUniqueID.req_design_doc_refresh WithTemplateAndUniqueID.req_design_doc_refresh
@ -246,32 +246,32 @@ describe "Model Base" do
d['views']['all']['map'].should include('WithTemplateAndUniqueID') d['views']['all']['map'].should include('WithTemplateAndUniqueID')
end end
it "should find all" do it "should find all" do
rs = WithTemplateAndUniqueID.all rs = WithTemplateAndUniqueID.all
rs.length.should == 4 rs.length.should == 4
end end
end end
describe "counting all instances of a model" do describe "counting all instances of a model" do
before(:each) do before(:each) do
@db = reset_test_db! @db = reset_test_db!
WithTemplateAndUniqueID.req_design_doc_refresh WithTemplateAndUniqueID.req_design_doc_refresh
end end
it ".count should return 0 if there are no docuemtns" do it ".count should return 0 if there are no docuemtns" do
WithTemplateAndUniqueID.count.should == 0 WithTemplateAndUniqueID.count.should == 0
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('important-field' => '1').save
WithTemplateAndUniqueID.new('important-field' => '2').save WithTemplateAndUniqueID.new('important-field' => '2').save
WithTemplateAndUniqueID.new('important-field' => '3').save WithTemplateAndUniqueID.new('important-field' => '3').save
WithTemplateAndUniqueID.count.should == 3 WithTemplateAndUniqueID.count.should == 3
end end
end end
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.req_design_doc_refresh # Removed by Sam Lown, design doc should be loaded automatically # WithTemplateAndUniqueID.req_design_doc_refresh # Removed by Sam Lown, design doc should be loaded automatically
WithTemplateAndUniqueID.new('important-field' => '1').save WithTemplateAndUniqueID.new('important-field' => '1').save
@ -309,7 +309,7 @@ describe "Model Base" do
WithTemplateAndUniqueID.design_doc['_rev'].should eql(rev) WithTemplateAndUniqueID.design_doc['_rev'].should eql(rev)
end end
end end
describe "getting a model with a subobject field" do describe "getting a model with a subobject field" do
before(:all) do before(:all) do
course_doc = { course_doc = {
@ -332,7 +332,7 @@ describe "Model Base" do
@course['ends_at'].should == Time.parse("2008/12/19 13:00:00 +0800") @course['ends_at'].should == Time.parse("2008/12/19 13:00:00 +0800")
end end
end end
describe "timestamping" do describe "timestamping" do
before(:each) do before(:each) do
oldart = Article.get "saving-this" rescue nil oldart = Article.get "saving-this" rescue nil
@ -340,7 +340,7 @@ describe "Model Base" do
@art = Article.new(:title => "Saving this") @art = Article.new(:title => "Saving this")
@art.save @art.save
end end
it "should define the updated_at and created_at getters and set the values" do it "should define the updated_at and created_at getters and set the values" do
@obj.save @obj.save
obj = WithDefaultValues.get(@obj.id) obj = WithDefaultValues.get(@obj.id)
@ -349,15 +349,15 @@ describe "Model Base" do
obj.updated_at.should be_an_instance_of(Time) obj.updated_at.should be_an_instance_of(Time)
obj.created_at.to_s.should == @obj.updated_at.to_s obj.created_at.to_s.should == @obj.updated_at.to_s
end end
it "should not change created_at on update" do it "should not change created_at on update" do
2.times do 2.times do
lambda do lambda do
@art.save @art.save
end.should_not change(@art, :created_at) end.should_not change(@art, :created_at)
end end
end end
it "should set the time on create" do it "should set the time on create" do
(Time.now - @art.created_at).should < 2 (Time.now - @art.created_at).should < 2
foundart = Article.get @art.id foundart = Article.get @art.id
@ -368,7 +368,7 @@ describe "Model Base" do
@art.created_at.should < @art.updated_at @art.created_at.should < @art.updated_at
end end
end end
describe "getter and setter methods" do describe "getter and setter methods" do
it "should try to call the arg= method before setting :arg in the hash" do it "should try to call the arg= method before setting :arg in the hash" do
@doc = WithGetterAndSetterMethods.new(:arg => "foo") @doc = WithGetterAndSetterMethods.new(:arg => "foo")
@ -384,41 +384,41 @@ describe "Model Base" do
@doc['some_value'].should eql('value') @doc['some_value'].should eql('value')
end end
end end
describe "recursive validation on a model" do 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')
end end
it "should not save if a nested casted model is invalid" do it "should not save if a nested casted model is invalid" do
@cat.favorite_toy = CatToy.new @cat.favorite_toy = CatToy.new
@cat.should_not be_valid @cat.should_not be_valid
@cat.save.should be_false @cat.save.should be_false
lambda{@cat.save!}.should raise_error lambda{@cat.save!}.should raise_error
end end
it "should save when nested casted model is valid" do it "should save when nested casted model is valid" do
@cat.favorite_toy = CatToy.new(:name => 'Squeaky') @cat.favorite_toy = CatToy.new(:name => 'Squeaky')
@cat.should be_valid @cat.should be_valid
@cat.save.should be_true @cat.save.should be_true
lambda{@cat.save!}.should_not raise_error lambda{@cat.save!}.should_not raise_error
end end
it "should not save when nested collection contains an invalid casted model" do it "should not save when nested collection contains an invalid casted model" do
@cat.toys = [CatToy.new(:name => 'Feather'), CatToy.new] @cat.toys = [CatToy.new(:name => 'Feather'), CatToy.new]
@cat.should_not be_valid @cat.should_not be_valid
@cat.save.should be_false @cat.save.should be_false
lambda{@cat.save!}.should raise_error lambda{@cat.save!}.should raise_error
end end
it "should save when nested collection contains valid casted models" do it "should save when nested collection contains valid casted models" do
@cat.toys = [CatToy.new(:name => 'feather'), CatToy.new(:name => 'ball-o-twine')] @cat.toys = [CatToy.new(:name => 'feather'), CatToy.new(:name => 'ball-o-twine')]
@cat.should be_valid @cat.should be_valid
@cat.save.should be_true @cat.save.should be_true
lambda{@cat.save!}.should_not raise_error lambda{@cat.save!}.should_not raise_error
end end
it "should not fail if the nested casted model doesn't have validation" do it "should not fail if the nested casted model doesn't have validation" do
Cat.property :trainer, Person Cat.property :trainer, Person
Cat.validates_presence_of :name Cat.validates_presence_of :name

View file

@ -0,0 +1,78 @@
# encoding: utf-8
require File.expand_path('../../spec_helper', __FILE__)
require File.join(FIXTURE_PATH, 'more', 'cat')
describe CouchRest::Model::Base do
before do
@class = Class.new(CouchRest::Model::Base)
end
describe '.configure' do
it "should set a configuration parameter" do
@class.add_config :foo_bar
@class.configure do |config|
config.foo_bar = 'monkey'
end
@class.foo_bar.should == 'monkey'
end
end
describe '.add_config' do
it "should add a class level accessor" do
@class.add_config :foo_bar
@class.foo_bar = 'foo'
@class.foo_bar.should == 'foo'
end
['foo', :foo, 45, ['foo', :bar]].each do |val|
it "should be inheritable for a #{val.class}" do
@class.add_config :foo_bar
@child_class = Class.new(@class)
@class.foo_bar = val
@class.foo_bar.should == val
@child_class.foo_bar.should == val
@child_class.foo_bar = "bar"
@child_class.foo_bar.should == "bar"
@class.foo_bar.should == val
end
end
it "should add an instance level accessor" do
@class.add_config :foo_bar
@class.foo_bar = 'foo'
@class.new.foo_bar.should == 'foo'
end
it "should add a convenient in-class setter" do
@class.add_config :foo_bar
@class.foo_bar "monkey"
@class.foo_bar.should == "monkey"
end
end
describe "General examples" do
before(:all) do
@default_model_key = 'model'
end
it "should be possible to override on class using configure method" do
default_model_key = Cat.model_type_key
Cat.instance_eval do
model_type_key 'cat-type'
end
CouchRest::Model::Base.model_type_key.should eql(default_model_key)
Cat.model_type_key.should eql('cat-type')
cat = Cat.new
cat.model_type_key.should eql('cat-type')
end
end
end

View file

@ -1,126 +0,0 @@
require File.expand_path("../../spec_helper", __FILE__)
class DirtyModel < CouchRest::Model::Base
use_database TEST_SERVER.default_database
property :name
property :color
validates_presence_of :name
end
describe 'Dirty Tracking', '#changed?' do
before(:each) do
@dm = DirtyModel.new
@dm.name = 'will'
end
it 'brand new models should not be changed by default' do
DirtyModel.new.should_not be_changed
end
it 'save should reset changed?' do
@dm.should be_changed
@dm.save
@dm.should_not be_changed
end
it 'save! should reset changed?' do
@dm.should be_changed
@dm.save!
@dm.should_not be_changed
end
it 'a failed save should preserve changed?' do
@dm.name = ''
@dm.should be_changed
@dm.save.should be_false
@dm.should be_changed
end
it 'should be true if there have been changes' do
@dm.name = 'not will'
@dm.should be_changed
end
end
describe 'Dirty Tracking', '#changed' do
it 'should be an array of the changed attributes' do
dm = DirtyModel.new
dm.changed.should == []
dm.name = 'will'
dm.changed.should == ['name']
dm.color = 'red'
dm.changed.should =~ ['name', 'color']
end
end
describe 'Dirty Tracking', '#changes' do
it 'should be a Map of changed attrs => [original value, new value]' do
dm = DirtyModel.new(:name => 'will', :color => 'red')
dm.save!
dm.should_not be_changed
dm.name = 'william'
dm.color = 'blue'
dm.changes.should == { 'name' => ['will', 'william'], 'color' => ['red', 'blue'] }
end
end
describe 'Dirty Tracking', '#previous_changes' do
it 'should store the previous changes after a save' do
dm = DirtyModel.new(:name => 'will', :color => 'red')
dm.save!
dm.should_not be_changed
dm.name = 'william'
dm.save!
dm.previous_changes.should == { 'name' => ['will', 'william'] }
end
end
describe 'Dirty Tracking', 'attribute methods' do
before(:each) do
@dm = DirtyModel.new(:name => 'will', :color => 'red')
@dm.save!
end
describe '#<attr>_changed?' do
it 'it should know if a specific property was changed' do
@dm.name = 'william'
@dm.should be_name_changed
@dm.should_not be_color_changed
end
end
describe 'Dirty Tracking', '#<attr>_change' do
it 'should be an array of [original value, current value]' do
@dm.name = 'william'
@dm.name_change.should == ['will', 'william']
end
end
describe 'Dirty Tracking', '#<attr>_was' do
it 'should return what the attribute was' do
@dm.name = 'william'
@dm.name_was.should == 'will'
end
end
describe 'Dirty Tracking', '#reset_<attr>!' do
it 'should reset the attribute to what it was' do
@dm.name = 'william'
@dm.reset_name!
@dm.name.should == 'will'
end
end
describe 'Dirty Tracking', '#<attr>_will_change!' do
it 'should manually mark the attribute as changed' do
@dm.should_not be_name_changed
@dm.name_will_change!
@dm.should be_name_changed
end
end
end

View file

@ -25,7 +25,7 @@ describe "Model Persistence" do
end end
it "should instantialize document of different type" do it "should instantialize document of different type" do
doc = Article.create_from_database({'_id' => 'testitem2', '_rev' => 123, 'couchrest-type' => 'WithTemplateAndUniqueID', 'name' => 'my test'}) doc = Article.create_from_database({'_id' => 'testitem2', '_rev' => 123, Article.model_type_key => 'WithTemplateAndUniqueID', 'name' => 'my test'})
doc.class.should eql(WithTemplateAndUniqueID) doc.class.should eql(WithTemplateAndUniqueID)
end end
@ -114,7 +114,7 @@ describe "Model Persistence" do
end end
it "should set the type" do it "should set the type" do
@sobj['couchrest-type'].should == 'Basic' @sobj[@sobj.model_type_key].should == 'Basic'
end end
it "should accept true or false on save for validation" do it "should accept true or false on save for validation" do

View file

@ -34,6 +34,12 @@ describe "Model Attributes" do
user.name.should == "will" user.name.should == "will"
user.phone.should == "555-5555" user.phone.should == "555-5555"
end end
it "should provide a list of all properties as accessible" do
user = NoProtection.new(:name => "will", :phone => "555-5555")
user.accessible_properties.length.should eql(2)
user.protected_properties.should be_empty
end
end end
describe "Model Base", "accessible flag" do describe "Model Base", "accessible flag" do
@ -65,6 +71,12 @@ describe "Model Attributes" do
user.name.should == "will" user.name.should == "will"
user.admin.should == false user.admin.should == false
end end
it "should provide correct accessible and protected property lists" do
user = WithAccessible.new(:name => 'will', :admin => true)
user.accessible_properties.map{|p| p.to_s}.should eql(['name'])
user.protected_properties.map{|p| p.to_s}.should eql(['admin'])
end
end end
describe "Model Base", "protected flag" do describe "Model Base", "protected flag" do
@ -96,6 +108,21 @@ describe "Model Attributes" do
user.name.should == "will" user.name.should == "will"
user.admin.should == false user.admin.should == false
end end
it "should not modify the provided attribute hash" do
user = WithProtected.new
attrs = {:name => "will", :admin => true}
user.attributes = attrs
attrs[:admin].should be_true
attrs[:name].should eql('will')
end
it "should provide correct accessible and protected property lists" do
user = WithProtected.new(:name => 'will', :admin => true)
user.accessible_properties.map{|p| p.to_s}.should eql(['name'])
user.protected_properties.map{|p| p.to_s}.should eql(['admin'])
end
end end
describe "Model Base", "mixing protected and accessible flags" do describe "Model Base", "mixing protected and accessible flags" do
@ -115,6 +142,7 @@ describe "Model Attributes" do
user.admin.should == false user.admin.should == false
user.phone.should == 'unset phone number' user.phone.should == 'unset phone number'
end end
end end
describe "from database" do describe "from database" do
@ -150,7 +178,7 @@ describe "Model Attributes" do
it "Base#all should not strip protected attributes" do it "Base#all should not strip protected attributes" do
# all creates a CollectionProxy # all creates a CollectionProxy
docs = WithProtected.all(:key => @user.id) docs = WithProtected.all(:key => @user.id)
docs.size.should == 1 docs.length.should == 1
reloaded = docs.first reloaded = docs.first
verify_attrs reloaded verify_attrs reloaded
end end

View file

@ -9,20 +9,6 @@ require File.join(FIXTURE_PATH, 'more', 'event')
require File.join(FIXTURE_PATH, 'more', 'user') require File.join(FIXTURE_PATH, 'more', 'user')
require File.join(FIXTURE_PATH, 'more', 'course') require File.join(FIXTURE_PATH, 'more', 'course')
describe 'Attributes' do
class AttrDoc < CouchRest::Model::Base
property :one
property :two
end
it '.attributes should have an array of attribute names' do
AttrDoc.attributes.should =~ ['two', 'one']
end
it '#attributes should have an array of attribute names' do
AttrDoc.new.attributes.should =~ ['two', 'one']
end
end
describe "Model properties" do describe "Model properties" do
@ -107,6 +93,7 @@ describe "Model properties" do
expect { @card.write_attribute(:this_property_should_not_exist, 823) }.to raise_error(ArgumentError) expect { @card.write_attribute(:this_property_should_not_exist, 823) }.to raise_error(ArgumentError)
end end
it "should let you use write_attribute on readonly properties" do it "should let you use write_attribute on readonly properties" do
lambda { lambda {
@card.read_only_value = "foo" @card.read_only_value = "foo"
@ -128,6 +115,34 @@ describe "Model properties" do
end end
end end
describe "mass updating attributes without property" do
describe "when mass_assign_any_attribute false" do
it "should not allow them to be set" do
@card.attributes = {:test => 'fooobar'}
@card['test'].should be_nil
end
end
describe "when mass_assign_any_attribute true" do
before(:each) do
# dup Card class so that no other tests are effected
card_class = Card.dup
card_class.class_eval do
mass_assign_any_attribute true
end
@card = card_class.new(:first_name => 'Sam')
end
it 'should allow them to be updated' do
@card.attributes = {:test => 'fooobar'}
@card['test'].should eql('fooobar')
end
end
end
describe "mass assignment protection" do describe "mass assignment protection" do
@ -310,12 +325,28 @@ describe "Model properties" do
@course['estimate'].should eql(-24.35) @course['estimate'].should eql(-24.35)
end end
it 'return float of a number with commas instead of points for decimals' do
@course.estimate = '23,35'
@course['estimate'].should eql(23.35)
end
it "should handle numbers with commas and points" do
@course.estimate = '1,234.00'
@course.estimate.should eql(1234.00)
end
it "should handle a mis-match of commas and points and maintain the last one" do
@course.estimate = "1,232.434.123,323"
@course.estimate.should eql(1232434123.323)
end
[ Object.new, true, '00.0', '0.', '-.0', 'string' ].each do |value| [ Object.new, true, '00.0', '0.', '-.0', 'string' ].each do |value|
it "does not typecast non-numeric value #{value.inspect}" do it "does not typecast non-numeric value #{value.inspect}" do
@course.estimate = value @course.estimate = value
@course['estimate'].should equal(value) @course['estimate'].should equal(value)
end end
end end
end end
describe 'when type primitive is a Integer' do describe 'when type primitive is a Integer' do

View file

@ -92,8 +92,8 @@ describe "Subclassing a Model" do
OnlineCourse.design_doc['views'].keys.should_not include('by_title') OnlineCourse.design_doc['views'].keys.should_not include('by_title')
end end
it "should have an all view with a guard clause for couchrest-type == subclass name in the map function" do it "should have an all view with a guard clause for model == subclass name in the map function" do
OnlineCourse.design_doc['views']['all']['map'].should =~ /if \(doc\['couchrest-type'\] == 'OnlineCourse'\)/ OnlineCourse.design_doc['views']['all']['map'].should =~ /if \(doc\['#{OnlineCourse.model_type_key}'\] == 'OnlineCourse'\)/
end end
end end

View file

@ -9,7 +9,7 @@ class Article < CouchRest::Model::Base
view_by :tags, view_by :tags,
:map => :map =>
"function(doc) { "function(doc) {
if (doc['couchrest-type'] == 'Article' && doc.tags) { if (doc['#{model_type_key}'] == 'Article' && doc.tags) {
doc.tags.forEach(function(tag){ doc.tags.forEach(function(tag){
emit(tag, 1); emit(tag, 1);
}); });

View file

@ -10,7 +10,7 @@ unless defined?(FIXTURE_PATH)
COUCHHOST = "http://127.0.0.1:5984" COUCHHOST = "http://127.0.0.1:5984"
TESTDB = 'couchrest-model-test' TESTDB = 'couchrest-model-test'
TEST_SERVER = CouchRest.new TEST_SERVER = CouchRest.new COUCHHOST
TEST_SERVER.default_database = TESTDB TEST_SERVER.default_database = TESTDB
DB = TEST_SERVER.database(TESTDB) DB = TEST_SERVER.database(TESTDB)
end end