From 85cd1308bc87206d28f4822b5cbee9e712a230d8 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Fri, 17 Sep 2010 23:00:55 +0200 Subject: [PATCH 01/15] Adding configuration support and changing 'couchrest-type' key to 'model' along with config options --- history.txt | 2 + lib/couchrest/model/base.rb | 5 +- lib/couchrest/model/configuration.rb | 49 ++++++++++++ lib/couchrest/model/design_doc.rb | 2 +- lib/couchrest/model/document_queries.rb | 6 +- lib/couchrest/model/persistence.rb | 2 +- lib/couchrest/model/views.rb | 6 +- lib/couchrest_model.rb | 1 + spec/couchrest/attribute_protection_spec.rb | 2 +- spec/couchrest/base_spec.rb | 2 +- spec/couchrest/configuration_spec.rb | 87 +++++++++++++++++++++ spec/couchrest/persistence_spec.rb | 4 +- spec/couchrest/subclass_spec.rb | 4 +- spec/fixtures/more/article.rb | 2 +- 14 files changed, 157 insertions(+), 17 deletions(-) create mode 100644 lib/couchrest/model/configuration.rb create mode 100644 spec/couchrest/configuration_spec.rb diff --git a/history.txt b/history.txt index d7e157f..13199f0 100644 --- a/history.txt +++ b/history.txt @@ -1,6 +1,8 @@ == Next Version * 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 * Minor enhancements * Fixing find("") issue (thanks epochwolf) diff --git a/lib/couchrest/model/base.rb b/lib/couchrest/model/base.rb index 2a5c602..8a7350b 100644 --- a/lib/couchrest/model/base.rb +++ b/lib/couchrest/model/base.rb @@ -4,6 +4,7 @@ module CouchRest extend ActiveModel::Naming + include CouchRest::Model::Configuration include CouchRest::Model::Persistence include CouchRest::Model::Callbacks include CouchRest::Model::DocumentQueries @@ -37,7 +38,7 @@ module CouchRest # Accessors attr_accessor :casted_by - + # Instantiate a new CouchRest::Model::Base by preparing all properties # using the provided document hash. @@ -50,7 +51,7 @@ module CouchRest 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 diff --git a/lib/couchrest/model/configuration.rb b/lib/couchrest/model/configuration.rb new file mode 100644 index 0000000..c468a02 --- /dev/null +++ b/lib/couchrest/model/configuration.rb @@ -0,0 +1,49 @@ +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 + + configure do |config| + config.model_type_key = 'model' + 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/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/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_model.rb b/lib/couchrest_model.rb index 0a3d657..d161368 100644 --- a/lib/couchrest_model.rb +++ b/lib/couchrest_model.rb @@ -48,6 +48,7 @@ require "couchrest/model/collection" require "couchrest/model/attribute_protection" require "couchrest/model/attributes" require "couchrest/model/associations" +require "couchrest/model/configuration" # Monkey patches applied to couchrest require "couchrest/model/support/couchrest" diff --git a/spec/couchrest/attribute_protection_spec.rb b/spec/couchrest/attribute_protection_spec.rb index c940b93..47e50a8 100644 --- a/spec/couchrest/attribute_protection_spec.rb +++ b/spec/couchrest/attribute_protection_spec.rb @@ -150,7 +150,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/base_spec.rb b/spec/couchrest/base_spec.rb index cd5fd4d..e10d9be 100644 --- a/spec/couchrest/base_spec.rb +++ b/spec/couchrest/base_spec.rb @@ -36,7 +36,7 @@ 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 diff --git a/spec/couchrest/configuration_spec.rb b/spec/couchrest/configuration_spec.rb new file mode 100644 index 0000000..d0e681b --- /dev/null +++ b/spec/couchrest/configuration_spec.rb @@ -0,0 +1,87 @@ +# 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 set default configuration options on Model::Base" do + CouchRest::Model::Base.model_type_key.should eql(@default_model_key) + end + + it "should provide options from instance" do + cat = Cat.new + cat.model_type_key.should eql(@default_model_key) + end + + 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 + 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/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/subclass_spec.rb b/spec/couchrest/subclass_spec.rb index 8344b90..333c383 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\['model'\] == '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); }); From 97347e70e346520b95d6b050f9131c08a321d0f5 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Fri, 17 Sep 2010 23:25:56 +0200 Subject: [PATCH 02/15] Working on adding support for allowing dynamic properties --- README.md | 33 ++++++++++++++++++++++++---- lib/couchrest/model/casted_model.rb | 1 + lib/couchrest/model/configuration.rb | 2 ++ lib/couchrest/model/properties.rb | 7 +++--- spec/couchrest/property_spec.rb | 7 ++++++ 5 files changed, 43 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a4669b5..01081e9 100644 --- a/README.md +++ b/README.md @@ -71,11 +71,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, if at all, and other options such as the default value. These replace your typical +`add_column` methods typically found in 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. In its simplest form, a property will only create a getter and setter passing all attribute data directly to the database. Assuming the attribute @@ -290,6 +293,28 @@ 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.allow_dynamic_properties = true + config.model_type_key = 'couchrest-type' + end + +To set for a specific model: + + class Cat < CouchRest::Model::Base + allow_dynamic_properties 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. + * `model_type_key` - 'model' by default, useful for migrating from an older CouchRest ExtendedDocument when the default used to be 'couchrest-type'. + ## Notable Issues diff --git a/lib/couchrest/model/casted_model.rb b/lib/couchrest/model/casted_model.rb index 9ef1603..8dc2f9e 100644 --- a/lib/couchrest/model/casted_model.rb +++ b/lib/couchrest/model/casted_model.rb @@ -4,6 +4,7 @@ module CouchRest::Model extend ActiveSupport::Concern included do + include CouchRest::Model::Configuration include CouchRest::Model::AttributeProtection include CouchRest::Model::Attributes include CouchRest::Model::Callbacks diff --git a/lib/couchrest/model/configuration.rb b/lib/couchrest/model/configuration.rb index c468a02..2dd9c88 100644 --- a/lib/couchrest/model/configuration.rb +++ b/lib/couchrest/model/configuration.rb @@ -9,9 +9,11 @@ module CouchRest included do add_config :model_type_key + add_config :allow_dynamic_properties configure do |config| config.model_type_key = 'model' + config.allow_dynamic_properties = false end end diff --git a/lib/couchrest/model/properties.rb b/lib/couchrest/model/properties.rb index 399673e..d6981a3 100644 --- a/lib/couchrest/model/properties.rb +++ b/lib/couchrest/model/properties.rb @@ -29,7 +29,7 @@ module CouchRest def write_attribute(property, value) prop = find_property!(property) - self[prop.to_s] = prop.cast(self, value) + self[prop.to_s] = prop.is_a?(String) ? value : prop.cast(self, value) end def apply_all_property_defaults @@ -41,10 +41,11 @@ module CouchRest end private + 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 - prop + raise ArgumentError, "Missing property definition for #{property.to_s}" unless allow_dynamic_properties or !prop.nil? + prop || property end module ClassMethods diff --git a/spec/couchrest/property_spec.rb b/spec/couchrest/property_spec.rb index 95352a9..cbcc00f 100644 --- a/spec/couchrest/property_spec.rb +++ b/spec/couchrest/property_spec.rb @@ -88,6 +88,13 @@ 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 { @card.read_only_value = "foo" From 1d1d8154358f2adca2f1a50cf1af2192d1a442e0 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Sat, 18 Sep 2010 15:19:15 +0200 Subject: [PATCH 03/15] Adding support for mass_assign_any_attribute config option and refactoring non-api methods into private areas of modules --- README.md | 38 ++++++---- history.txt | 1 + lib/couchrest/model/attribute_protection.rb | 2 + lib/couchrest/model/base.rb | 1 - lib/couchrest/model/casted_model.rb | 3 +- lib/couchrest/model/configuration.rb | 4 +- lib/couchrest/model/properties.rb | 78 +++++++++++++++++---- lib/couchrest_model.rb | 1 - spec/couchrest/configuration_spec.rb | 4 +- spec/couchrest/property_spec.rb | 34 +++++++-- 10 files changed, 124 insertions(+), 42 deletions(-) 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 From d0ed97ed8b1b22e4f463a6cb428afb27e20890bc Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Fri, 22 Oct 2010 15:39:12 +0200 Subject: [PATCH 04/15] Renaming Attribute Protection and solving problem modifying the provided hash to the #attributes= method --- README.md | 6 +- Rakefile | 2 +- lib/couchrest/model/attributes.rb | 68 ------------------- lib/couchrest/model/base.rb | 4 +- lib/couchrest/model/casted_model.rb | 2 +- lib/couchrest/model/configuration.rb | 2 +- lib/couchrest/model/properties.rb | 12 +++- ...e_protection.rb => property_protection.rb} | 38 +++++------ lib/couchrest_model.rb | 2 +- spec/couchrest/configuration_spec.rb | 11 +-- ...on_spec.rb => property_protection_spec.rb} | 28 ++++++++ spec/couchrest/subclass_spec.rb | 2 +- 12 files changed, 65 insertions(+), 112 deletions(-) delete mode 100644 lib/couchrest/model/attributes.rb rename lib/couchrest/model/{attribute_protection.rb => property_protection.rb} (66%) rename spec/couchrest/{attribute_protection_spec.rb => property_protection_spec.rb} (81%) diff --git a/README.md b/README.md index e96a296..42936aa 100644 --- a/README.md +++ b/README.md @@ -192,14 +192,14 @@ documents and retrieve them using the CastedModel module. Simply include the mod a Hash (or other model that responds to the [] and []= methods) and set any properties you'd like to use. For example: - class CatToy << Hash + class CatToy < Hash include CouchRest::Model::CastedModel property :name, String property :purchased, Date end - class Cat << CouchRest::Model::Base + class Cat < CouchRest::Model::Base property :name, String property :toys, [CatToy] end @@ -218,7 +218,7 @@ Ruby will bring up a missing constant error. To avoid this, or if you have a rea you'd like to model, the latest version of CouchRest Model (> 1.0.0) supports creating anonymous classes: - class Cat << CouchRest::Model::Base + class Cat < CouchRest::Model::Base property :name, String property :toys do |toy| 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/lib/couchrest/model/attributes.rb b/lib/couchrest/model/attributes.rb deleted file mode 100644 index 391cdb5..0000000 --- a/lib/couchrest/model/attributes.rb +++ /dev/null @@ -1,68 +0,0 @@ -module CouchRest - module Model - module Attributes - - ## 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 - - - private - - 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)) - 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 - - def check_properties_exist(attrs) - property_list = self.properties.map{|p| p.name} - 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 bef793f..b52ad59 100644 --- a/lib/couchrest/model/base.rb +++ b/lib/couchrest/model/base.rb @@ -13,7 +13,7 @@ module CouchRest include CouchRest::Model::ExtendedAttachments include CouchRest::Model::ClassProxy include CouchRest::Model::Collection - include CouchRest::Model::AttributeProtection + include CouchRest::Model::PropertyProtection include CouchRest::Model::Associations include CouchRest::Model::Validations @@ -47,7 +47,7 @@ module CouchRest # * :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[self.model_type_key] = self.class.to_s diff --git a/lib/couchrest/model/casted_model.rb b/lib/couchrest/model/casted_model.rb index ea74c02..4d931fc 100644 --- a/lib/couchrest/model/casted_model.rb +++ b/lib/couchrest/model/casted_model.rb @@ -7,7 +7,7 @@ module CouchRest::Model include CouchRest::Model::Configuration include CouchRest::Model::Callbacks include CouchRest::Model::Properties - include CouchRest::Model::AttributeProtection + include CouchRest::Model::PropertyProtection 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 2c4919a..765a4b1 100644 --- a/lib/couchrest/model/configuration.rb +++ b/lib/couchrest/model/configuration.rb @@ -12,7 +12,7 @@ module CouchRest add_config :mass_assign_any_attribute configure do |config| - config.model_type_key = 'model' + config.model_type_key = 'couchrest-type' # 'model'? config.mass_assign_any_attribute = false end end diff --git a/lib/couchrest/model/properties.rb b/lib/couchrest/model/properties.rb index f431c21..97b4c53 100644 --- a/lib/couchrest/model/properties.rb +++ b/lib/couchrest/model/properties.rb @@ -61,7 +61,7 @@ module CouchRest if options[:directly_set_attributes] directly_set_read_only_attributes(doc) else - remove_protected_attributes(doc) + doc = remove_protected_attributes(doc) end directly_set_attributes(doc) unless doc.nil? end @@ -72,12 +72,18 @@ module CouchRest prop end + # Set all the attributes and return a hash with the attributes + # that have not been accepted. def directly_set_attributes(hash) - hash.each do |attribute_name, attribute_value| + hash.reject do |attribute_name, attribute_value| if self.respond_to?("#{attribute_name}=") - self.send("#{attribute_name}=", hash.delete(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 diff --git a/lib/couchrest/model/attribute_protection.rb b/lib/couchrest/model/property_protection.rb similarity index 66% rename from lib/couchrest/model/attribute_protection.rb rename to lib/couchrest/model/property_protection.rb index ed852a3..73a2e74 100644 --- a/lib/couchrest/model/attribute_protection.rb +++ b/lib/couchrest/model/property_protection.rb @@ -1,9 +1,9 @@ module CouchRest module Model - module AttributeProtection + module PropertyProtection extend ActiveSupport::Concern - # Attribute protection from mass assignment to CouchRest::Model properties + # Property protection from mass assignment to CouchRest::Model properties # # Protected methods will be removed from # * new @@ -22,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 @@ -34,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 @@ -50,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.rb b/lib/couchrest_model.rb index 8abb75b..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,7 +46,6 @@ 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/associations" require "couchrest/model/configuration" diff --git a/spec/couchrest/configuration_spec.rb b/spec/couchrest/configuration_spec.rb index 8e4ff35..46681c0 100644 --- a/spec/couchrest/configuration_spec.rb +++ b/spec/couchrest/configuration_spec.rb @@ -62,20 +62,13 @@ describe CouchRest::Model::Base do @default_model_key = 'model' end - it "should set default configuration options on Model::Base" do - CouchRest::Model::Base.model_type_key.should eql(@default_model_key) - end - - it "should provide options from instance" do - cat = Cat.new - cat.model_type_key.should eql(@default_model_key) - 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) + 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') 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 47e50a8..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 diff --git a/spec/couchrest/subclass_spec.rb b/spec/couchrest/subclass_spec.rb index 333c383..78a8ed8 100644 --- a/spec/couchrest/subclass_spec.rb +++ b/spec/couchrest/subclass_spec.rb @@ -93,7 +93,7 @@ describe "Subclassing a Model" do end 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\['model'\] == 'OnlineCourse'\)/ + OnlineCourse.design_doc['views']['all']['map'].should =~ /if \(doc\['#{OnlineCourse.model_type_key}'\] == 'OnlineCourse'\)/ end end From 9419622c13456671a49f65c797c5d0f39def9df6 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Fri, 22 Oct 2010 15:42:48 +0200 Subject: [PATCH 05/15] updating history --- history.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/history.txt b/history.txt index 1b25d35..9614922 100644 --- a/history.txt +++ b/history.txt @@ -1,12 +1,12 @@ == Next Version * 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) + * Altered protected attributes so that hash provided to #attributes= is not modified == CouchRest Model 1.0.0.beta8 From ef6f54d9661edac39aceeba67d7cfc0877940a20 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Fri, 22 Oct 2010 15:46:07 +0200 Subject: [PATCH 06/15] history update --- history.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/history.txt b/history.txt index 9614922..9562cc0 100644 --- a/history.txt +++ b/history.txt @@ -8,6 +8,13 @@ * Fixing find("") issue (thanks epochwolf) * Altered protected attributes so that hash provided to #attributes= is not modified +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 * Major enhancements From e8d7af989686ba40a2832aaaf3355f296824297c Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Sat, 23 Oct 2010 20:59:24 +0200 Subject: [PATCH 07/15] Handling cases when , used instead of . more elegantly --- history.txt | 1 + lib/couchrest/model/typecast.rb | 2 +- spec/couchrest/property_spec.rb | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/history.txt b/history.txt index 9562cc0..becff4d 100644 --- a/history.txt +++ b/history.txt @@ -7,6 +7,7 @@ * 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 Notes: 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/spec/couchrest/property_spec.rb b/spec/couchrest/property_spec.rb index 363da5b..98c0fb5 100644 --- a/spec/couchrest/property_spec.rb +++ b/spec/couchrest/property_spec.rb @@ -320,12 +320,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 From dd284fe045b46a7406035acf8c7368e96d73ca7e Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Sat, 23 Oct 2010 21:06:43 +0200 Subject: [PATCH 08/15] Updated gemspec --- couchrest_model.gemspec | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) 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"]) From 3bd72b8a29a688eb399ff4a57678ff42cd0d5aec Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Fri, 19 Nov 2010 01:03:40 +0100 Subject: [PATCH 09/15] Modifying railtie to use couchrest_model name --- lib/couchrest/railtie.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 + From 1f615ce5686dac03ce57e4e4116bd98d5e0822ba Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Tue, 23 Nov 2010 02:55:29 +0100 Subject: [PATCH 10/15] Removing wierd marshalling thing --- lib/couchrest/model/property.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From 7e8bdf2855e7112953a192834de7d5ca57b07003 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Thu, 2 Dec 2010 01:42:52 +0100 Subject: [PATCH 11/15] Adding the initial workings of the view class --- lib/couchrest/model/view.rb | 190 ++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 lib/couchrest/model/view.rb 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 From e1d140d8ae7e4a79c653cd457c0e36077e94e908 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Thu, 2 Dec 2010 01:53:45 +0100 Subject: [PATCH 12/15] Fixing the database in view bug --- history.txt | 1 + lib/couchrest/model/collection.rb | 3 +++ 2 files changed, 4 insertions(+) diff --git a/history.txt b/history.txt index becff4d..4f06656 100644 --- a/history.txt +++ b/history.txt @@ -8,6 +8,7 @@ * 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: 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 From 2ee92a533102cf87c6ef435f6bda25464ab18623 Mon Sep 17 00:00:00 2001 From: Will Leinweber Date: Thu, 2 Dec 2010 15:06:50 -0600 Subject: [PATCH 13/15] Removes suppport for ActiveModel::Dirty and ::AttributeMethods for performance reasons Removes commit d3331333194ba31ce1d26448cd542086af6ab662 --- history.txt | 2 - lib/couchrest/model/attributes.rb | 86 +++---------------- lib/couchrest/model/base.rb | 31 ++++--- lib/couchrest/model/casted_model.rb | 16 ++-- lib/couchrest/model/dirty.rb | 44 ---------- lib/couchrest/model/properties.rb | 54 ++++++++++-- lib/couchrest_model.rb | 1 - spec/couchrest/base_spec.rb | 80 +++++++++--------- spec/couchrest/dirty_spec.rb | 126 ---------------------------- spec/couchrest/property_spec.rb | 14 ---- 10 files changed, 123 insertions(+), 331 deletions(-) delete mode 100644 lib/couchrest/model/dirty.rb delete mode 100644 spec/couchrest/dirty_spec.rb diff --git a/history.txt b/history.txt index f05d44f..d7e157f 100644 --- a/history.txt +++ b/history.txt @@ -1,8 +1,6 @@ == Next Version * Major enhancements - * Dirty Tracking via ActiveModel - * ActiveModel Attribute Methods support * Minor enhancements * Fixing find("") issue (thanks epochwolf) diff --git a/lib/couchrest/model/attributes.rb b/lib/couchrest/model/attributes.rb index c2f2484..391cdb5 100644 --- a/lib/couchrest/model/attributes.rb +++ b/lib/couchrest/model/attributes.rb @@ -1,66 +1,17 @@ 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) + directly_set_read_only_attributes(doc) else remove_protected_attributes(doc) end @@ -69,7 +20,7 @@ module CouchRest # 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. + # 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. @@ -78,26 +29,11 @@ module CouchRest 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 @@ -105,27 +41,27 @@ module CouchRest end def directly_set_read_only_attributes(hash) - r_o_a = read_only_attributes - property_list = attributes + property_list = self.properties.map{|p| p.name} hash.each do |attribute_name, attribute_value| - next unless r_o_a.include? attribute_name + 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 def check_properties_exist(attrs) - property_list = attributes + property_list = self.properties.map{|p| p.name} 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 end diff --git a/lib/couchrest/model/base.rb b/lib/couchrest/model/base.rb index 391f3b0..2a5c602 100644 --- a/lib/couchrest/model/base.rb +++ b/lib/couchrest/model/base.rb @@ -6,7 +6,7 @@ module CouchRest 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 @@ -16,12 +16,11 @@ module CouchRest include CouchRest::Model::Attributes 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,16 +34,16 @@ module CouchRest EOS subclasses << subklass end - + # Accessors attr_accessor :casted_by - + # Instantiate a new CouchRest::Model::Base by preparing all properties # using the provided document hash. # # Options supported: - # + # # * :directly_set_attributes: true when data comes directly from database # def initialize(doc = {}, options = {}) @@ -55,8 +54,8 @@ module CouchRest 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..9ef1603 100644 --- a/lib/couchrest/model/casted_model.rb +++ b/lib/couchrest/model/casted_model.rb @@ -1,6 +1,6 @@ module CouchRest::Model module CastedModel - + extend ActiveSupport::Concern included do @@ -12,28 +12,28 @@ module CouchRest::Model 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/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/properties.rb b/lib/couchrest/model/properties.rb index 4f5701f..399673e 100644 --- a/lib/couchrest/model/properties.rb +++ b/lib/couchrest/model/properties.rb @@ -1,5 +1,4 @@ # encoding: utf-8 -require 'set' module CouchRest module Model module Properties @@ -23,6 +22,16 @@ module CouchRest self.class.properties end + 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 + def apply_all_property_defaults return if self.respond_to?(:new?) && (new? == false) # TODO: cache the default object @@ -85,7 +94,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 @@ -93,15 +103,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.rb b/lib/couchrest_model.rb index e69bbf4..0a3d657 100644 --- a/lib/couchrest_model.rb +++ b/lib/couchrest_model.rb @@ -48,7 +48,6 @@ require "couchrest/model/collection" require "couchrest/model/attribute_protection" require "couchrest/model/attributes" require "couchrest/model/associations" -require "couchrest/model/dirty" # 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..cd5fd4d 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 @@ -39,10 +39,10 @@ describe "Model Base" do @obj.should == { 'couchrest-type' => 'Basic' } 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/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/property_spec.rb b/spec/couchrest/property_spec.rb index 7e59be1..95352a9 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 From 958616025db74bdea252e3108112c0afa439b439 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Fri, 3 Dec 2010 01:17:26 +0100 Subject: [PATCH 14/15] Documentation alterations --- README.md | 127 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 79 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 42936aa..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 @@ -73,13 +70,10 @@ but no guarantees! 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 typically found in relational database migrations. +`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 -you'd like to set can be done using the regular CouchRest Document, in the same way you'd update a 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. +can be set in the same way you'd update a Hash, this funcionality is inherited from CouchRest Documents. Here are a few examples of the way properties are used: @@ -106,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 @@ -121,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 @@ -131,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 @@ -151,22 +143,21 @@ attribute using the `write_attribute` method: Mass assigning attributes is also possible in a similar fashion to ActiveRecord: - @cat.attributes = {:name => "Felix"} + @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 +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 @@ -208,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 @@ -231,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 @@ -242,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: @@ -266,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: @@ -279,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 @@ -325,28 +365,21 @@ To set for a specific model: 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` - 'model' by default, useful for migrating from an older CouchRest ExtendedDocument when the default used to be 'couchrest-type'. + * `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 @@ -358,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 From 1d0df87e34a1a01b41adecea9538fe143d95bd91 Mon Sep 17 00:00:00 2001 From: Aidan Feldman Date: Wed, 29 Sep 2010 18:11:59 -0400 Subject: [PATCH 15/15] use the COUCHHOST for the test db MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcos Tapajós --- spec/spec_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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