diff --git a/README.md b/README.md index 9bfb1b2..708df1d 100644 --- a/README.md +++ b/README.md @@ -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 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 -### From Gem +### Gem -CouchRest Model depends on Rails 3's ActiveModel which has not yet been released. You'll need to add -`--pre` to the end of the gem install until the dependencies are stable: - - $ sudo gem install couchrest_model --pre + $ sudo gem install couchrest_model ### Bundler @@ -71,16 +68,14 @@ but no guarantees! ## Properties -Only attributes with a property definition will be stored be CouchRest Model (as opposed -to a normal CouchRest Document which will store everything). To help prevent confusion, -a property should be considered as the definition of an attribute. An attribute must be associated -with a property, but a property may not have any attributes associated if none have been set. +A property is the definition of an attribute, it describes what the attribute is called, how it should +be type casted and other options such as the default value. These replace your typical +`add_column` methods found in relational database migrations. +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 -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: +Here are a few examples of the way properties are used: class Cat < CouchRest::Model::Base property :name @@ -105,8 +100,7 @@ Properties create getters and setters similar to the following: write_attribute('name', value) end -Properties can also have a type which -will be used for casting data retrieved from CouchDB when the attribute is set: +Properties can also have a type which will be used for casting data retrieved from CouchDB when the attribute is set: class Cat < CouchRest::Model::Base 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! -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 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. -Defining a property as read-only will mean that its value is set only when read from the -database and that it will not have a setter method. You can however update a read-only -attribute using the `write_attribute` method: +A read-only property will only have a getter method, and its value is set when the document +is read from the database. You can however update a read-only attribute using the `write_attribute` method: class Cat < CouchRest::Model::Base property :name, String @@ -148,10 +141,23 @@ attribute using the `write_attribute` method: @cat.fall_off_balcony! @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 -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: class Cat < CouchRest::Model::Base @@ -193,15 +199,14 @@ you'd like to use. For example: @cat.toys.first.class == CatToy @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.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 -you'd like to model, the latest version of CouchRest Model (> 1.0.0) supports creating -anonymous classes: +you'd like to model, CouchRest Model supports creating anonymous classes: class Cat < CouchRest::Model::Base property :name, String @@ -216,7 +221,7 @@ anonymous classes: @cat.toys.last.rating == 5 @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 @@ -227,14 +232,67 @@ Two types at the moment: 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 -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. +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. 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 validates_uniqueness_of :code, :view => 'all' -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. +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. Examples: @@ -264,8 +320,7 @@ Examples: validates_uniqueness_of :title, :proxy => 'company.people' -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: +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: class Product < CouchRest::Model::Base property :code @@ -290,26 +345,41 @@ you'd like to ensure the ID is unique between several types of document. For exa 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 -CouchRest Model uses active_support for some of its internals. Ensure you have a stable active support gem installed -or at least 3.0.0.beta4. +None at the moment... -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 CouchRest Model is compatible with rails and provides some ActiveRecord-like methods. -The CouchRest companion rails project -[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. +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. -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. +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. ### Rails 3.0 @@ -321,9 +391,7 @@ In your Gemfile require the gem with a simple line: ## Testing -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). +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). ## Docs diff --git a/Rakefile b/Rakefile index d527aad..3d5b68f 100644 --- a/Rakefile +++ b/Rakefile @@ -27,7 +27,7 @@ begin 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.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("activemodel", "~> 3.0.0.rc") gemspec.add_dependency("tzinfo", "~> 0.3.22") diff --git a/couchrest_model.gemspec b/couchrest_model.gemspec index 2e1f388..bae1e33 100644 --- a/couchrest_model.gemspec +++ b/couchrest_model.gemspec @@ -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.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.email = %q{jchris@apache.org} s.extra_rdoc_files = [ @@ -25,14 +25,13 @@ Gem::Specification.new do |s| "history.txt", "lib/couchrest/model.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/callbacks.rb", "lib/couchrest/model/casted_array.rb", "lib/couchrest/model/casted_model.rb", "lib/couchrest/model/class_proxy.rb", "lib/couchrest/model/collection.rb", + "lib/couchrest/model/configuration.rb", "lib/couchrest/model/design_doc.rb", "lib/couchrest/model/document_queries.rb", "lib/couchrest/model/errors.rb", @@ -40,6 +39,7 @@ Gem::Specification.new do |s| "lib/couchrest/model/persistence.rb", "lib/couchrest/model/properties.rb", "lib/couchrest/model/property.rb", + "lib/couchrest/model/property_protection.rb", "lib/couchrest/model/support/couchrest.rb", "lib/couchrest/model/support/hash.rb", "lib/couchrest/model/typecast.rb", @@ -55,13 +55,14 @@ Gem::Specification.new do |s| "lib/rails/generators/couchrest_model/model/templates/model.rb", "spec/couchrest/assocations_spec.rb", "spec/couchrest/attachment_spec.rb", - "spec/couchrest/attribute_protection_spec.rb", "spec/couchrest/base_spec.rb", "spec/couchrest/casted_model_spec.rb", "spec/couchrest/casted_spec.rb", "spec/couchrest/class_proxy_spec.rb", + "spec/couchrest/configuration_spec.rb", "spec/couchrest/inherited_spec.rb", "spec/couchrest/persistence_spec.rb", + "spec/couchrest/property_protection_spec.rb", "spec/couchrest/property_spec.rb", "spec/couchrest/subclass_spec.rb", "spec/couchrest/validations.rb", @@ -93,16 +94,17 @@ Gem::Specification.new do |s| s.homepage = %q{http://github.com/couchrest/couchrest_model} s.rdoc_options = ["--charset=UTF-8"] 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.test_files = [ "spec/spec_helper.rb", + "spec/couchrest/configuration_spec.rb", "spec/couchrest/property_spec.rb", + "spec/couchrest/property_protection_spec.rb", "spec/couchrest/casted_spec.rb", "spec/couchrest/subclass_spec.rb", "spec/couchrest/persistence_spec.rb", "spec/couchrest/casted_model_spec.rb", - "spec/couchrest/attribute_protection_spec.rb", "spec/couchrest/assocations_spec.rb", "spec/couchrest/validations.rb", "spec/couchrest/view_spec.rb", @@ -130,15 +132,15 @@ Gem::Specification.new do |s| current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION s.specification_version = 3 - if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then - s.add_runtime_dependency(%q, ["~> 1.0.0"]) + if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then + s.add_runtime_dependency(%q, ["~> 1.0.1"]) s.add_runtime_dependency(%q, ["~> 1.15"]) s.add_runtime_dependency(%q, ["~> 3.0.0.rc"]) s.add_runtime_dependency(%q, ["~> 0.3.22"]) s.add_runtime_dependency(%q, ["~> 3.0.0.rc"]) s.add_development_dependency(%q, ["~> 2.0.0.beta.19"]) else - s.add_dependency(%q, ["~> 1.0.0"]) + s.add_dependency(%q, ["~> 1.0.1"]) s.add_dependency(%q, ["~> 1.15"]) s.add_dependency(%q, ["~> 3.0.0.rc"]) s.add_dependency(%q, ["~> 0.3.22"]) @@ -146,7 +148,7 @@ Gem::Specification.new do |s| s.add_dependency(%q, ["~> 2.0.0.beta.19"]) end else - s.add_dependency(%q, ["~> 1.0.0"]) + s.add_dependency(%q, ["~> 1.0.1"]) s.add_dependency(%q, ["~> 1.15"]) s.add_dependency(%q, ["~> 3.0.0.rc"]) s.add_dependency(%q, ["~> 0.3.22"]) diff --git a/history.txt b/history.txt index f05d44f..4f06656 100644 --- a/history.txt +++ b/history.txt @@ -1,11 +1,21 @@ == Next Version * Major enhancements - * Dirty Tracking via ActiveModel - * ActiveModel Attribute Methods support + * Support for configuration module and "model_type_key" option for overriding model's type key + * Added "mass_assign_any_attribute" configuration option to allow setting anything via the attribute= method. * Minor enhancements * 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 diff --git a/lib/couchrest/model/attributes.rb b/lib/couchrest/model/attributes.rb deleted file mode 100644 index c2f2484..0000000 --- a/lib/couchrest/model/attributes.rb +++ /dev/null @@ -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 - diff --git a/lib/couchrest/model/base.rb b/lib/couchrest/model/base.rb index 391f3b0..b52ad59 100644 --- a/lib/couchrest/model/base.rb +++ b/lib/couchrest/model/base.rb @@ -4,24 +4,23 @@ module CouchRest extend ActiveModel::Naming + include CouchRest::Model::Configuration include CouchRest::Model::Persistence include CouchRest::Model::Callbacks - include CouchRest::Model::DocumentQueries + include CouchRest::Model::DocumentQueries include CouchRest::Model::Views include CouchRest::Model::DesignDoc include CouchRest::Model::ExtendedAttachments include CouchRest::Model::ClassProxy include CouchRest::Model::Collection - include CouchRest::Model::AttributeProtection - include CouchRest::Model::Attributes + include CouchRest::Model::PropertyProtection include CouchRest::Model::Associations include CouchRest::Model::Validations - include CouchRest::Model::Dirty def self.subclasses @subclasses ||= [] end - + def self.inherited(subklass) super subklass.send(:include, CouchRest::Model::Properties) @@ -35,7 +34,7 @@ module CouchRest EOS subclasses << subklass end - + # Accessors attr_accessor :casted_by @@ -44,19 +43,19 @@ module CouchRest # using the provided document hash. # # Options supported: - # + # # * :directly_set_attributes: true when data comes directly from database # def initialize(doc = {}, options = {}) - prepare_all_attributes(doc, options) + doc = prepare_all_attributes(doc, options) super(doc) unless self['_id'] && self['_rev'] - self['couchrest-type'] = self.class.to_s + self[self.model_type_key] = self.class.to_s end after_initialize if respond_to?(:after_initialize) end - - + + # Temp solution to make the view_by methods available def self.method_missing(m, *args, &block) if has_view?(m) @@ -70,9 +69,9 @@ module CouchRest end super end - + ### instance methods - + # Gets a reference to the actual document in the DB # Calls up to the next document if there is one, # Otherwise we're at the top and we return self @@ -80,14 +79,14 @@ module CouchRest return self if base_doc? @casted_by.base_doc end - + # Checks if we're the top document def base_doc? !@casted_by end - + ## Compatibility with ActiveSupport and older frameworks - + # Hack so that CouchRest::Document, which descends from Hash, # doesn't appear to Rails routing as a Hash of options def is_a?(klass) @@ -99,14 +98,14 @@ module CouchRest def persisted? !new? end - + def to_key - new? ? nil : [id] + new? ? nil : [id] end alias :to_param :id alias :new_record? :new? alias :new_document? :new? - end + end end end diff --git a/lib/couchrest/model/casted_model.rb b/lib/couchrest/model/casted_model.rb index a44f987..4d931fc 100644 --- a/lib/couchrest/model/casted_model.rb +++ b/lib/couchrest/model/casted_model.rb @@ -1,39 +1,39 @@ module CouchRest::Model module CastedModel - + extend ActiveSupport::Concern included do - include CouchRest::Model::AttributeProtection - include CouchRest::Model::Attributes + include CouchRest::Model::Configuration include CouchRest::Model::Callbacks include CouchRest::Model::Properties + include CouchRest::Model::PropertyProtection include CouchRest::Model::Associations include CouchRest::Model::Validations attr_accessor :casted_by end - + def initialize(keys = {}) raise StandardError unless self.is_a? Hash prepare_all_attributes(keys) super() end - + def []= key, value super(key.to_s, value) end - + def [] key super(key.to_s) end - + # Gets a reference to the top level extended # document that a model is saved inside of def base_doc return nil unless @casted_by @casted_by.base_doc end - + # False if the casted model has already # been saved in the containing document def new? @@ -53,12 +53,12 @@ module CouchRest::Model end alias :to_key :id alias :to_param :id - + # Sets the attributes from a hash def update_attributes_without_saving(hash) hash.each do |k, v| raise NoMethodError, "#{k}= method not available, use property :#{k}" unless self.respond_to?("#{k}=") - end + end hash.each do |k, v| self.send("#{k}=",v) end diff --git a/lib/couchrest/model/collection.rb b/lib/couchrest/model/collection.rb index 17910e7..f12c7ec 100644 --- a/lib/couchrest/model/collection.rb +++ b/lib/couchrest/model/collection.rb @@ -82,6 +82,7 @@ module CouchRest design_doc['views'][view_name.to_s] && design_doc['views'][view_name.to_s]["couchrest-defaults"]) || {} view_options = default_view_options.merge(options) + view_options.delete(:database) [design_doc, view_name, view_options] end @@ -94,6 +95,8 @@ module CouchRest raise ArgumentError, 'search_name is required' if search_name.nil? search_options = options.clone + search_options.delete(:database) + [design_doc, search_name, search_options] end diff --git a/lib/couchrest/model/configuration.rb b/lib/couchrest/model/configuration.rb new file mode 100644 index 0000000..765a4b1 --- /dev/null +++ b/lib/couchrest/model/configuration.rb @@ -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 + + diff --git a/lib/couchrest/model/design_doc.rb b/lib/couchrest/model/design_doc.rb index 244fba0..cedba14 100644 --- a/lib/couchrest/model/design_doc.rb +++ b/lib/couchrest/model/design_doc.rb @@ -31,7 +31,7 @@ module CouchRest "views" => { 'all' => { 'map' => "function(doc) { - if (doc['couchrest-type'] == '#{self.to_s}') { + if (doc['#{self.model_type_key}'] == '#{self.to_s}') { emit(doc['_id'],1); } }" diff --git a/lib/couchrest/model/dirty.rb b/lib/couchrest/model/dirty.rb deleted file mode 100644 index ae5b461..0000000 --- a/lib/couchrest/model/dirty.rb +++ /dev/null @@ -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 - # #_changed?, #_change, - # #reset_!, #_will_change!, - # and #_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 diff --git a/lib/couchrest/model/document_queries.rb b/lib/couchrest/model/document_queries.rb index 3cc2673..48ec485 100644 --- a/lib/couchrest/model/document_queries.rb +++ b/lib/couchrest/model/document_queries.rb @@ -8,21 +8,21 @@ module CouchRest 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 # CouchRest::Database#view options. def all(opts = {}, &block) view(:all, opts, &block) 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 # CouchRest::Database#view options def count(opts = {}, &block) all({:raw => true, :limit => 0}.merge(opts), &block)['total_rows'] 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. # # ==== Returns diff --git a/lib/couchrest/model/persistence.rb b/lib/couchrest/model/persistence.rb index a48955a..372148d 100644 --- a/lib/couchrest/model/persistence.rb +++ b/lib/couchrest/model/persistence.rb @@ -97,7 +97,7 @@ module CouchRest # ==== Returns # a document instance 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) end diff --git a/lib/couchrest/model/properties.rb b/lib/couchrest/model/properties.rb index 1a7506b..3f021ed 100644 --- a/lib/couchrest/model/properties.rb +++ b/lib/couchrest/model/properties.rb @@ -1,18 +1,13 @@ # encoding: utf-8 -require 'set' module CouchRest module Model module Properties + extend ActiveSupport::Concern - class IncludeError < StandardError; end - - def self.included(base) - base.class_eval <<-EOS, __FILE__, __LINE__ + 1 - 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?(:[]=)) + included do + extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties) + self.properties ||= [] + 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 # Returns the Class properties @@ -23,12 +18,45 @@ module CouchRest self.class.properties end + # Returns the Class properties with their values + # + # ==== Returns + # Array:: the list of properties with their values def properties_with_values props = {} properties.each { |property| props[property.name] = read_attribute(property.name) } props 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 return if self.respond_to?(:new?) && (new? == false) # TODO: cache the default object @@ -37,13 +65,54 @@ module CouchRest 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) 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 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 def property(name, *options, &block) @@ -91,7 +160,8 @@ module CouchRest type = [type] # inject as an array end 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) validates_casted_model property.name end @@ -99,15 +169,49 @@ module CouchRest property 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 - def #{property.alias.to_s} - #{property.name} + def #{property.name} + read_attribute('#{property.name}') end 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 # module ClassMethods + end end end diff --git a/lib/couchrest/model/property.rb b/lib/couchrest/model/property.rb index abefaab..d1aec7b 100644 --- a/lib/couchrest/model/property.rb +++ b/lib/couchrest/model/property.rb @@ -58,7 +58,8 @@ module CouchRest::Model if default.class == Proc default.call 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 diff --git a/lib/couchrest/model/attribute_protection.rb b/lib/couchrest/model/property_protection.rb similarity index 65% rename from lib/couchrest/model/attribute_protection.rb rename to lib/couchrest/model/property_protection.rb index 1ddbd80..73a2e74 100644 --- a/lib/couchrest/model/attribute_protection.rb +++ b/lib/couchrest/model/property_protection.rb @@ -1,7 +1,9 @@ module CouchRest module Model - module AttributeProtection - # Attribute protection from mass assignment to CouchRest::Model properties + module PropertyProtection + extend ActiveSupport::Concern + + # Property protection from mass assignment to CouchRest::Model properties # # Protected methods will be removed from # * new @@ -20,7 +22,7 @@ module CouchRest # # 3) Mix and match, and assume all unspecified properties are protected. # property :name, :accessible => true - # property :admin, :protected => true + # property :admin, :protected => true # ignored # property :phone # this will be automatically protected # # Note: the timestamps! method protectes the created_at and updated_at properties @@ -32,11 +34,16 @@ module CouchRest module ClassMethods 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 def protected_properties - properties.select { |prop| prop.options[:protected] } + accessibles = accessible_properties + properties.reject { |prop| accessibles.include?(prop) } end end @@ -48,28 +55,17 @@ module CouchRest self.class.protected_properties end + # Return a new copy of the attributes hash with protected attributes + # removed. def remove_protected_attributes(attributes) - protected_names = properties_to_remove_from_mass_assignment.map { |prop| prop.name } - return attributes if protected_names.empty? + protected_names = protected_properties.map { |prop| prop.name } + 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) - 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 - - to_remove end + end end end diff --git a/lib/couchrest/model/typecast.rb b/lib/couchrest/model/typecast.rb index 496558d..45a3114 100644 --- a/lib/couchrest/model/typecast.rb +++ b/lib/couchrest/model/typecast.rb @@ -79,7 +79,7 @@ module CouchRest # Match numeric string def typecast_to_numeric(value, method) 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) else value diff --git a/lib/couchrest/model/view.rb b/lib/couchrest/model/view.rb new file mode 100644 index 0000000..4737b56 --- /dev/null +++ b/lib/couchrest/model/view.rb @@ -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 diff --git a/lib/couchrest/model/views.rb b/lib/couchrest/model/views.rb index 68e55bd..74a8660 100644 --- a/lib/couchrest/model/views.rb +++ b/lib/couchrest/model/views.rb @@ -23,7 +23,7 @@ module CouchRest # view_by :tags, # :map => # "function(doc) { - # if (doc['couchrest-type'] == 'Post' && doc.tags) { + # if (doc['model'] == 'Post' && doc.tags) { # doc.tags.forEach(function(tag){ # emit(doc.tag, 1); # }); @@ -39,7 +39,7 @@ module CouchRest # function: # # function(doc) { - # if (doc['couchrest-type'] == 'Post' && doc.date) { + # if (doc['model'] == 'Post' && doc.date) { # emit(doc.date, null); # } # } @@ -77,7 +77,7 @@ module CouchRest ducktype = opts.delete(:ducktype) unless ducktype || opts[:map] opts[:guards] ||= [] - opts[:guards].push "(doc['couchrest-type'] == '#{self.to_s}')" + opts[:guards].push "(doc['#{model_type_key}'] == '#{self.to_s}')" end keys.push opts design_doc.view_by(*keys) diff --git a/lib/couchrest/railtie.rb b/lib/couchrest/railtie.rb index e167785..7c55e0f 100644 --- a/lib/couchrest/railtie.rb +++ b/lib/couchrest/railtie.rb @@ -4,9 +4,9 @@ require "active_model/railtie" module CouchrestModel # = Active Record Railtie class Railtie < Rails::Railtie - config.generators.orm :couchrest + config.generators.orm :couchrest_model config.generators.test_framework :test_unit, :fixture => false end end - \ No newline at end of file + diff --git a/lib/couchrest_model.rb b/lib/couchrest_model.rb index e69bbf4..181bb9c 100644 --- a/lib/couchrest_model.rb +++ b/lib/couchrest_model.rb @@ -35,6 +35,7 @@ require 'couchrest/model/errors' require "couchrest/model/persistence" require "couchrest/model/typecast" require "couchrest/model/property" +require "couchrest/model/property_protection" require "couchrest/model/casted_array" require "couchrest/model/properties" require "couchrest/model/validations" @@ -45,10 +46,8 @@ require "couchrest/model/design_doc" require "couchrest/model/extended_attachments" require "couchrest/model/class_proxy" require "couchrest/model/collection" -require "couchrest/model/attribute_protection" -require "couchrest/model/attributes" require "couchrest/model/associations" -require "couchrest/model/dirty" +require "couchrest/model/configuration" # Monkey patches applied to couchrest require "couchrest/model/support/couchrest" diff --git a/spec/couchrest/base_spec.rb b/spec/couchrest/base_spec.rb index 35f182b..e10d9be 100644 --- a/spec/couchrest/base_spec.rb +++ b/spec/couchrest/base_spec.rb @@ -8,23 +8,23 @@ require File.join(FIXTURE_PATH, 'more', 'card') require File.join(FIXTURE_PATH, 'base') describe "Model Base" do - + before(:each) do @obj = WithDefaultValues.new end - + describe "instance database connection" do it "should use the default database" do @obj.database.name.should == 'couchrest-model-test' end - + it "should override the default db" do @obj.database = TEST_SERVER.database!('couchrest-extendedmodel-test') @obj.database.name.should == 'couchrest-extendedmodel-test' @obj.database.delete! end end - + describe "a new model" do it "should be a new document" do @obj = Basic.new @@ -36,13 +36,13 @@ describe "Model Base" do it "should not failed on a nil value in argument" do @obj = Basic.new(nil) - @obj.should == { 'couchrest-type' => 'Basic' } + @obj.should_not be_nil end end - + describe "ActiveModel compatability Basic" do - before(:each) do + before(:each) do @obj = Basic.new(nil) end @@ -86,7 +86,7 @@ describe "Model Base" do context "when the document is not new" do it "returns id" do @obj.save - @obj.persisted?.should == true + @obj.persisted?.should == true end end end @@ -100,7 +100,7 @@ describe "Model Base" do end - + describe "update attributes without saving" do before(:each) do 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['title'].should == "something else" end - + it "should not flip out if an attribute= method is missing and ignore it" do lambda { @art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger") }.should_not raise_error @art.slug.should == "big-bad-danger" end - + #it "should not change other attributes if there is an error" do # 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 # @art['title'].should == "big bad danger" #end end - + describe "update attributes" do before(:each) do a = Article.get "big-bad-danger" rescue nil @@ -164,7 +164,7 @@ describe "Model Base" do loaded['title'].should == "super danger" end end - + describe "with default" do it "should have the default value set at initalization" do @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 @obj.default_false.should == false end - + 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 == @obj.set_by_proc @obj.set_by_proc.should < Time.now end - + it "should let you overwrite the default values" do obj = WithDefaultValues.new(:preset => 'test') obj.preset = 'test' end - + it "should work with a default empty array" do obj = WithDefaultValues.new(:tags => ['spec']) obj.tags.should == ['spec'] end - + it "should set default value of read-only property" do obj = WithDefaultValues.new obj.read_only_with_default.should == 'generic' @@ -207,7 +207,7 @@ describe "Model Base" do obj.tags.should == ['spec'] end end - + describe "a doc with template values (CR::Model spec)" do before(:all) do WithTemplateAndUniqueID.all.map{|o| o.destroy} @@ -228,8 +228,8 @@ describe "Model Base" do tmpl2_reloaded.preset.should == 'not_value' end end - - + + describe "finding all instances of a model" do before(:all) do WithTemplateAndUniqueID.req_design_doc_refresh @@ -246,32 +246,32 @@ describe "Model Base" do d['views']['all']['map'].should include('WithTemplateAndUniqueID') end it "should find all" do - rs = WithTemplateAndUniqueID.all + rs = WithTemplateAndUniqueID.all rs.length.should == 4 end end - + describe "counting all instances of a model" do before(:each) do @db = reset_test_db! WithTemplateAndUniqueID.req_design_doc_refresh end - + it ".count should return 0 if there are no docuemtns" do WithTemplateAndUniqueID.count.should == 0 end - + it ".count should return the number of documents" do WithTemplateAndUniqueID.new('important-field' => '1').save WithTemplateAndUniqueID.new('important-field' => '2').save WithTemplateAndUniqueID.new('important-field' => '3').save - + WithTemplateAndUniqueID.count.should == 3 end end - + describe "finding the first instance of a model" do - before(:each) do + before(:each) do @db = reset_test_db! # WithTemplateAndUniqueID.req_design_doc_refresh # Removed by Sam Lown, design doc should be loaded automatically WithTemplateAndUniqueID.new('important-field' => '1').save @@ -309,7 +309,7 @@ describe "Model Base" do WithTemplateAndUniqueID.design_doc['_rev'].should eql(rev) end end - + describe "getting a model with a subobject field" do before(:all) do course_doc = { @@ -332,7 +332,7 @@ describe "Model Base" do @course['ends_at'].should == Time.parse("2008/12/19 13:00:00 +0800") end end - + describe "timestamping" do before(:each) do oldart = Article.get "saving-this" rescue nil @@ -340,7 +340,7 @@ describe "Model Base" do @art = Article.new(:title => "Saving this") @art.save end - + it "should define the updated_at and created_at getters and set the values" do @obj.save obj = WithDefaultValues.get(@obj.id) @@ -349,15 +349,15 @@ describe "Model Base" do obj.updated_at.should be_an_instance_of(Time) obj.created_at.to_s.should == @obj.updated_at.to_s end - + it "should not change created_at on update" do - 2.times do + 2.times do lambda do @art.save end.should_not change(@art, :created_at) end end - + it "should set the time on create" do (Time.now - @art.created_at).should < 2 foundart = Article.get @art.id @@ -368,7 +368,7 @@ describe "Model Base" do @art.created_at.should < @art.updated_at end end - + describe "getter and setter methods" do it "should try to call the arg= method before setting :arg in the hash" do @doc = WithGetterAndSetterMethods.new(:arg => "foo") @@ -384,41 +384,41 @@ describe "Model Base" do @doc['some_value'].should eql('value') end end - + describe "recursive validation on a model" do before :each do reset_test_db! @cat = Cat.new(:name => 'Sockington') end - + it "should not save if a nested casted model is invalid" do @cat.favorite_toy = CatToy.new @cat.should_not be_valid @cat.save.should be_false lambda{@cat.save!}.should raise_error end - + it "should save when nested casted model is valid" do @cat.favorite_toy = CatToy.new(:name => 'Squeaky') @cat.should be_valid @cat.save.should be_true lambda{@cat.save!}.should_not raise_error end - + it "should not save when nested collection contains an invalid casted model" do @cat.toys = [CatToy.new(:name => 'Feather'), CatToy.new] @cat.should_not be_valid @cat.save.should be_false lambda{@cat.save!}.should raise_error end - + it "should save when nested collection contains valid casted models" do @cat.toys = [CatToy.new(:name => 'feather'), CatToy.new(:name => 'ball-o-twine')] @cat.should be_valid @cat.save.should be_true lambda{@cat.save!}.should_not raise_error end - + it "should not fail if the nested casted model doesn't have validation" do Cat.property :trainer, Person Cat.validates_presence_of :name diff --git a/spec/couchrest/configuration_spec.rb b/spec/couchrest/configuration_spec.rb new file mode 100644 index 0000000..46681c0 --- /dev/null +++ b/spec/couchrest/configuration_spec.rb @@ -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 diff --git a/spec/couchrest/dirty_spec.rb b/spec/couchrest/dirty_spec.rb deleted file mode 100644 index c7c2f10..0000000 --- a/spec/couchrest/dirty_spec.rb +++ /dev/null @@ -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 '#_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', '#_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', '#_was' do - it 'should return what the attribute was' do - @dm.name = 'william' - @dm.name_was.should == 'will' - end - end - - describe 'Dirty Tracking', '#reset_!' 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', '#_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 diff --git a/spec/couchrest/persistence_spec.rb b/spec/couchrest/persistence_spec.rb index 6723653..9c8cb2e 100644 --- a/spec/couchrest/persistence_spec.rb +++ b/spec/couchrest/persistence_spec.rb @@ -25,7 +25,7 @@ describe "Model Persistence" do end 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) end @@ -114,7 +114,7 @@ describe "Model Persistence" do end it "should set the type" do - @sobj['couchrest-type'].should == 'Basic' + @sobj[@sobj.model_type_key].should == 'Basic' end it "should accept true or false on save for validation" do diff --git a/spec/couchrest/attribute_protection_spec.rb b/spec/couchrest/property_protection_spec.rb similarity index 81% rename from spec/couchrest/attribute_protection_spec.rb rename to spec/couchrest/property_protection_spec.rb index c940b93..eb5ca55 100644 --- a/spec/couchrest/attribute_protection_spec.rb +++ b/spec/couchrest/property_protection_spec.rb @@ -34,6 +34,12 @@ describe "Model Attributes" do user.name.should == "will" user.phone.should == "555-5555" 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 describe "Model Base", "accessible flag" do @@ -65,6 +71,12 @@ describe "Model Attributes" do user.name.should == "will" user.admin.should == false 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 describe "Model Base", "protected flag" do @@ -96,6 +108,21 @@ describe "Model Attributes" do user.name.should == "will" user.admin.should == false 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 describe "Model Base", "mixing protected and accessible flags" do @@ -115,6 +142,7 @@ describe "Model Attributes" do user.admin.should == false user.phone.should == 'unset phone number' end + end describe "from database" do @@ -150,7 +178,7 @@ describe "Model Attributes" do it "Base#all should not strip protected attributes" do # all creates a CollectionProxy docs = WithProtected.all(:key => @user.id) - docs.size.should == 1 + docs.length.should == 1 reloaded = docs.first verify_attrs reloaded end diff --git a/spec/couchrest/property_spec.rb b/spec/couchrest/property_spec.rb index 802d7b2..2c4eab0 100644 --- a/spec/couchrest/property_spec.rb +++ b/spec/couchrest/property_spec.rb @@ -9,20 +9,6 @@ require File.join(FIXTURE_PATH, 'more', 'event') require File.join(FIXTURE_PATH, 'more', 'user') 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 @@ -107,6 +93,7 @@ describe "Model properties" do expect { @card.write_attribute(:this_property_should_not_exist, 823) }.to raise_error(ArgumentError) end + it "should let you use write_attribute on readonly properties" do lambda { @card.read_only_value = "foo" @@ -128,6 +115,34 @@ describe "Model properties" do 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 @@ -310,12 +325,28 @@ describe "Model properties" do @course['estimate'].should eql(-24.35) 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| it "does not typecast non-numeric value #{value.inspect}" do @course.estimate = value @course['estimate'].should equal(value) end end + end describe 'when type primitive is a Integer' do diff --git a/spec/couchrest/subclass_spec.rb b/spec/couchrest/subclass_spec.rb index 8344b90..78a8ed8 100644 --- a/spec/couchrest/subclass_spec.rb +++ b/spec/couchrest/subclass_spec.rb @@ -92,8 +92,8 @@ describe "Subclassing a Model" do OnlineCourse.design_doc['views'].keys.should_not include('by_title') end - it "should have an all view with a guard clause for couchrest-type == subclass name in the map function" do - OnlineCourse.design_doc['views']['all']['map'].should =~ /if \(doc\['couchrest-type'\] == 'OnlineCourse'\)/ + 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\['#{OnlineCourse.model_type_key}'\] == 'OnlineCourse'\)/ end end diff --git a/spec/fixtures/more/article.rb b/spec/fixtures/more/article.rb index 3a7a405..ef8ce2a 100644 --- a/spec/fixtures/more/article.rb +++ b/spec/fixtures/more/article.rb @@ -9,7 +9,7 @@ class Article < CouchRest::Model::Base view_by :tags, :map => "function(doc) { - if (doc['couchrest-type'] == 'Article' && doc.tags) { + if (doc['#{model_type_key}'] == 'Article' && doc.tags) { doc.tags.forEach(function(tag){ emit(tag, 1); }); diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d26d3d7..fdcadba 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -10,7 +10,7 @@ unless defined?(FIXTURE_PATH) COUCHHOST = "http://127.0.0.1:5984" TESTDB = 'couchrest-model-test' - TEST_SERVER = CouchRest.new + TEST_SERVER = CouchRest.new COUCHHOST TEST_SERVER.default_database = TESTDB DB = TEST_SERVER.database(TESTDB) end