diff --git a/README.md b/README.md index 01081e9..e96a296 100644 --- a/README.md +++ b/README.md @@ -72,18 +72,16 @@ but no guarantees! ## Properties A property is the definition of an attribute, it describes what the attribute is called, how it should -be type casted, if at all, and other options such as the default value. These replace your typical -`add_column` methods typically found in migrations. +be type casted and other options such as the default value. These replace your typical +`add_column` methods typically found in relational database migrations. -By default only attributes with a property definition will be stored in CouchRest Model, as opposed -to a normal CouchRest Document which will store everything. This however can be disabled using the -`allow_dynamic_properties` configuration option either for all of CouchRest Model, or for specific -models. See the configuration section for more details. +Attributes with a property definition will have setter and getter methods defined for them. Any other attibute +you'd like to set can be done using the regular CouchRest Document, in the same way you'd update a Hash. -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: +Properties allow for type casting. Simply provide a Class along with the property definition and CouchRest Model +will convert any value provided to the property into a new instance of the Class. + +Here are a few examples of the way properties are used: class Cat < CouchRest::Model::Base property :name @@ -151,6 +149,20 @@ 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") + +Attributes without a property definition however will not be updated this way, this is useful to +provent useless data being passed from an HTML form for example. 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 @@ -300,19 +312,19 @@ base or for a specific model of your chosing. To configure globally, provide som following in your projects loading code: CouchRestModel::Model::Base.configure do |config| - config.allow_dynamic_properties = true + config.mass_assign_any_attribute = true config.model_type_key = 'couchrest-type' end To set for a specific model: class Cat < CouchRest::Model::Base - allow_dynamic_properties true + mass_assign_any_attribute true end Options currently avilable are: - * `allow_dynamic_properties` - false by default, when true properties do not need to be defined to be stored, although they will have no accessors. + * `mass_assign_any_attribute` - false by default, when true any attribute may be updated via the update_attributes or attributes= methods. * `model_type_key` - 'model' by default, useful for migrating from an older CouchRest ExtendedDocument when the default used to be 'couchrest-type'. diff --git a/history.txt b/history.txt index 13199f0..1b25d35 100644 --- a/history.txt +++ b/history.txt @@ -3,6 +3,7 @@ * Major enhancements * IMPORTANT: Model's class name key changed from 'couchrest-type' to 'model' * 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) diff --git a/lib/couchrest/model/attribute_protection.rb b/lib/couchrest/model/attribute_protection.rb index 1ddbd80..ed852a3 100644 --- a/lib/couchrest/model/attribute_protection.rb +++ b/lib/couchrest/model/attribute_protection.rb @@ -1,6 +1,8 @@ module CouchRest module Model module AttributeProtection + extend ActiveSupport::Concern + # Attribute protection from mass assignment to CouchRest::Model properties # # Protected methods will be removed from diff --git a/lib/couchrest/model/base.rb b/lib/couchrest/model/base.rb index 8a7350b..bef793f 100644 --- a/lib/couchrest/model/base.rb +++ b/lib/couchrest/model/base.rb @@ -14,7 +14,6 @@ module CouchRest include CouchRest::Model::ClassProxy include CouchRest::Model::Collection include CouchRest::Model::AttributeProtection - include CouchRest::Model::Attributes include CouchRest::Model::Associations include CouchRest::Model::Validations diff --git a/lib/couchrest/model/casted_model.rb b/lib/couchrest/model/casted_model.rb index 8dc2f9e..ea74c02 100644 --- a/lib/couchrest/model/casted_model.rb +++ b/lib/couchrest/model/casted_model.rb @@ -5,10 +5,9 @@ module CouchRest::Model included do include CouchRest::Model::Configuration - include CouchRest::Model::AttributeProtection - include CouchRest::Model::Attributes include CouchRest::Model::Callbacks include CouchRest::Model::Properties + include CouchRest::Model::AttributeProtection include CouchRest::Model::Associations include CouchRest::Model::Validations attr_accessor :casted_by diff --git a/lib/couchrest/model/configuration.rb b/lib/couchrest/model/configuration.rb index 2dd9c88..2c4919a 100644 --- a/lib/couchrest/model/configuration.rb +++ b/lib/couchrest/model/configuration.rb @@ -9,11 +9,11 @@ module CouchRest included do add_config :model_type_key - add_config :allow_dynamic_properties + add_config :mass_assign_any_attribute configure do |config| config.model_type_key = 'model' - config.allow_dynamic_properties = false + config.mass_assign_any_attribute = false end end diff --git a/lib/couchrest/model/properties.rb b/lib/couchrest/model/properties.rb index d6981a3..f431c21 100644 --- a/lib/couchrest/model/properties.rb +++ b/lib/couchrest/model/properties.rb @@ -2,16 +2,12 @@ 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 @@ -22,16 +18,36 @@ module CouchRest self.class.properties end + # Read the casted value of an attribute defined with a property. + # + # ==== Returns + # Object:: the casted attibutes value. def read_attribute(property) - prop = find_property!(property) - self[prop.to_s] + 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 @@ -40,14 +56,48 @@ 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 + 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 allow_dynamic_properties or !prop.nil? - prop || property + raise ArgumentError, "Missing property definition for #{property.to_s}" if prop.nil? + prop end + def directly_set_attributes(hash) + hash.each do |attribute_name, attribute_value| + if self.respond_to?("#{attribute_name}=") + self.send("#{attribute_name}=", hash.delete(attribute_name)) + elsif mass_assign_any_attribute # config option + self[attribute_name] = attribute_value + 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) diff --git a/lib/couchrest_model.rb b/lib/couchrest_model.rb index d161368..8abb75b 100644 --- a/lib/couchrest_model.rb +++ b/lib/couchrest_model.rb @@ -46,7 +46,6 @@ 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/configuration" diff --git a/spec/couchrest/configuration_spec.rb b/spec/couchrest/configuration_spec.rb index d0e681b..8e4ff35 100644 --- a/spec/couchrest/configuration_spec.rb +++ b/spec/couchrest/configuration_spec.rb @@ -73,9 +73,7 @@ describe CouchRest::Model::Base do it "should be possible to override on class using configure method" do Cat.instance_eval do - configure do |config| - config.model_type_key = 'cat-type' - end + 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') diff --git a/spec/couchrest/property_spec.rb b/spec/couchrest/property_spec.rb index cbcc00f..363da5b 100644 --- a/spec/couchrest/property_spec.rb +++ b/spec/couchrest/property_spec.rb @@ -88,12 +88,6 @@ describe "Model properties" do expect { @card.write_attribute(:this_property_should_not_exist, 823) }.to raise_error(ArgumentError) end - it 'should not raise an error if the property does not exist and dynamic properties are allowed' do - @card.class.allow_dynamic_properties = true - expect { @card.write_attribute(:this_property_should_not_exist, 823) }.to_not raise_error(ArgumentError) - @card.class.allow_dynamic_properties = false - end - it "should let you use write_attribute on readonly properties" do lambda { @@ -116,6 +110,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