Adding support for mass_assign_any_attribute config option and refactoring non-api methods into private areas of modules

This commit is contained in:
Sam Lown 2010-09-18 15:19:15 +02:00
parent 97347e70e3
commit 1d1d815435
10 changed files with 124 additions and 42 deletions

View file

@ -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'.

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
included do
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?(:[]=))
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)

View file

@ -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"

View file

@ -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')

View file

@ -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