diff --git a/lib/couchrest.rb b/lib/couchrest.rb index cffde30..7a08989 100644 --- a/lib/couchrest.rb +++ b/lib/couchrest.rb @@ -39,6 +39,7 @@ module CouchRest autoload :Streamer, 'couchrest/helper/streamer' autoload :ExtendedDocument, 'couchrest/more/extended_document' + autoload :CastedModel, 'couchrest/more/casted_model' require File.join(File.dirname(__FILE__), 'couchrest', 'mixins') @@ -47,6 +48,23 @@ module CouchRest # some helpers for tasks like instantiating a new Database or Server instance. class << self + # extracted from Extlib + # + # Constantize tries to find a declared constant with the name specified + # in the string. It raises a NameError when the name is not in CamelCase + # or is not initialized. + # + # @example + # "Module".constantize #=> Module + # "Class".constantize #=> Class + def constantize(camel_cased_word) + unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ camel_cased_word + raise NameError, "#{camel_cased_word.inspect} is not a valid constant name!" + end + + Object.module_eval("::#{$1}", __FILE__, __LINE__) + end + # todo, make this parse the url and instantiate a Server or Database instance # depending on the specificity. def new(*opts) diff --git a/lib/couchrest/mixins/properties.rb b/lib/couchrest/mixins/properties.rb index 400608b..1801361 100644 --- a/lib/couchrest/mixins/properties.rb +++ b/lib/couchrest/mixins/properties.rb @@ -1,18 +1,24 @@ +require File.join(File.dirname(__FILE__), '..', 'more', 'property') + module CouchRest module Mixins - module DocumentProperties + module Properties class IncludeError < StandardError; end def self.included(base) + base.cattr_accessor(:properties) + base.class_eval <<-EOS, __FILE__, __LINE__ + @@properties = [] + EOS base.extend(ClassMethods) - raise CouchRest::Mixins::DocumentProporties::InludeError, "You can only mixin Properties in a class responding to [] and []=" unless (base.new.respond_to?(:[]) && base.new.respond_to?(:[]=)) + raise CouchRest::Mixins::Properties::IncludeError, "You can only mixin Properties in a class responding to [] and []=, if you tried to mixin CastedModel, make sure your class inherits from Hash or responds to the proper methods" unless (base.new.respond_to?(:[]) && base.new.respond_to?(:[]=)) end def apply_defaults - return unless new_document? + return unless self.respond_to?(:new_document?) && new_document? + return unless self.class.respond_to?(:properties) return if self.class.properties.empty? - # TODO: cache the default object self.class.properties.each do |property| key = property.name.to_s @@ -25,16 +31,38 @@ module CouchRest end end end - + end + + def cast_keys + return unless self.class.properties + # TODO move the argument checking to the cast method for early crashes + self.class.properties.each do |property| + next unless property.casted + key = self.has_key?(property.name) ? property.name : property.name.to_sym + target = property.type + if target.is_a?(Array) + klass = ::CouchRest.constantize(target[0]) + + self[property.name] = self[key].collect do |value| + obj = ( (property.init_method == 'send') && klass == Time) ? Time.parse(value) : klass.send(property.init_method, value) + obj.casted_by = self if obj.respond_to?(:casted_by) + obj + end + else + # Let people use :send as a Time parse arg + self[property.name] = if ((property.init_method != 'send') && target == 'Time') + Time.parse(self[property.init_method]) + else + klass = ::CouchRest.constantize(target) + klass.send(property.init_method, self[property.name]) + end + self[key].casted_by = self if self[key].respond_to?(:casted_by) + end + end end module ClassMethods - # Stores the class properties - def properties - @@properties ||= [] - end - def property(name, options={}) define_property(name, options) unless properties.map{|p| p.name}.include?(name.to_s) end @@ -44,29 +72,31 @@ module CouchRest # This is not a thread safe operation, if you have to set new properties at runtime # make sure to use a mutex. def define_property(name, options={}) - property = CouchRest::Property.new(name, options.delete(:type), options) + # check if this property is going to casted + options[:casted] = true if options[:cast_as] + property = CouchRest::Property.new(name, (options.delete(:cast_as) || options.delete(:type)), options) create_property_getter(property) create_property_setter(property) unless property.read_only == true properties << property end - # defines the getter for the property + # defines the getter for the property (and optional aliases) def create_property_getter(property) - meth = property.name - class_eval <<-EOS - def #{meth} - self['#{meth}'] + # meth = property.name + class_eval <<-EOS, __FILE__, __LINE__ + def #{property.name} + self['#{property.name}'] end EOS if property.alias - class_eval <<-EOS - alias #{property.alias.to_sym} #{meth.to_sym} + class_eval <<-EOS, __FILE__, __LINE__ + alias #{property.alias.to_sym} #{property.name.to_sym} EOS end end - # defines the setter for the property + # defines the setter for the property (and optional aliases) def create_property_setter(property) meth = property.name class_eval <<-EOS diff --git a/lib/couchrest/mixins/validation.rb b/lib/couchrest/mixins/validation.rb index 3cdb279..02c2df5 100644 --- a/lib/couchrest/mixins/validation.rb +++ b/lib/couchrest/mixins/validation.rb @@ -126,10 +126,7 @@ module CouchRest self.respond_to?(name, true) ? self.send(name) : nil end - # Get the corresponding Resource property, if it exists. - # - # Note: CouchRest validations can be used on non-CouchRest resources. - # In such cases, the return value will be nil. + # Get the corresponding Object property, if it exists. def validation_property(field_name) properties.find{|p| p.name == field_name} end diff --git a/lib/couchrest/more/casted_model.rb b/lib/couchrest/more/casted_model.rb new file mode 100644 index 0000000..ab0bbf7 --- /dev/null +++ b/lib/couchrest/more/casted_model.rb @@ -0,0 +1,28 @@ +require File.join(File.dirname(__FILE__), '..', 'mixins', 'properties') + +module CouchRest + module CastedModel + + def self.included(base) + base.send(:include, CouchRest::Mixins::Properties) + base.send(:attr_accessor, :casted_by) + end + + def initialize(keys={}) + super + keys.each do |k,v| + self[k.to_s] = v + end if keys + apply_defaults # defined in CouchRest::Mixins::Properties + # cast_keys # defined in CouchRest::Mixins::Properties + end + + def []= key, value + super(key.to_s, value) + end + + def [] key + super(key.to_s) + end + end +end \ No newline at end of file diff --git a/lib/couchrest/more/extended_document.rb b/lib/couchrest/more/extended_document.rb index dbe3f5c..e846340 100644 --- a/lib/couchrest/more/extended_document.rb +++ b/lib/couchrest/more/extended_document.rb @@ -15,7 +15,7 @@ module CouchRest class ExtendedDocument < Document include CouchRest::Callbacks include CouchRest::Mixins::DocumentQueries - include CouchRest::Mixins::DocumentProperties + include CouchRest::Mixins::Properties include CouchRest::Mixins::Views include CouchRest::Mixins::DesignDoc @@ -25,8 +25,8 @@ module CouchRest def initialize(keys={}) super - apply_defaults # defined in CouchRest::Mixins::DocumentProperties - # cast_keys + apply_defaults # defined in CouchRest::Mixins::Properties + cast_keys # defined in CouchRest::Mixins::Properties unless self['_id'] && self['_rev'] self['couchrest-type'] = self.class.to_s end diff --git a/lib/couchrest/more/property.rb b/lib/couchrest/more/property.rb index e3c86d5..06628de 100644 --- a/lib/couchrest/more/property.rb +++ b/lib/couchrest/more/property.rb @@ -2,12 +2,12 @@ module CouchRest # Basic attribute support for adding getter/setter + validation class Property - attr_reader :name, :type, :read_only, :alias, :default, :options + attr_reader :name, :type, :read_only, :alias, :default, :casted, :init_method, :options # attribute to define def initialize(name, type = nil, options = {}) - @name = name.to_s - @type = type || String + @name = name.to_s + @type = type.nil? ? 'String' : type.to_s parse_options(options) self end @@ -20,6 +20,8 @@ module CouchRest @read_only = options.delete(:read_only) if options[:read_only] @alias = options.delete(:alias) if options[:alias] @default = options.delete(:default) if options[:default] + @casted = options[:casted] ? true : false + @init_method = options[:send] ? options.delete[:send] : 'new' @options = options end diff --git a/lib/couchrest/validation/auto_validate.rb b/lib/couchrest/validation/auto_validate.rb index 6ccb3aa..87d18c0 100644 --- a/lib/couchrest/validation/auto_validate.rb +++ b/lib/couchrest/validation/auto_validate.rb @@ -107,7 +107,7 @@ module CouchRest end # length - if property.type == String + if property.type == "String" # XXX: maybe length should always return a Range, with the min defaulting to 1 # 52 being the max set len = property.options.fetch(:length, property.options.fetch(:size, 52)) @@ -145,7 +145,7 @@ module CouchRest end # numeric validator - if Integer == property.type + if "Integer" == property.type opts[:integer_only] = true # validates_is_number property.name, opts validates_is_number property.name, options_with_message(opts, property, :is_number) diff --git a/spec/couchrest/more/casted_model_spec.rb b/spec/couchrest/more/casted_model_spec.rb new file mode 100644 index 0000000..f9ddeb3 --- /dev/null +++ b/spec/couchrest/more/casted_model_spec.rb @@ -0,0 +1,60 @@ +require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper') +require File.join(FIXTURE_PATH, 'more', 'card') + +describe CouchRest::CastedModel do + + class WithCastedModelMixin < Hash + include CouchRest::CastedModel + property :name + end + + class DummyModel < CouchRest::ExtendedDocument + property :casted_attribute, :cast_as => 'WithCastedModelMixin' + end + + describe "A non hash class including CastedModel" do + + it "should fail raising and include error" do + lambda do + class NotAHashButWithCastedModelMixin + include CouchRest::CastedModel + property :name + end + + end.should raise_error + end + + end + + describe "isolated" do + before(:each) do + @obj = WithCastedModelMixin.new + end + it "should automatically include the property mixin and define getters and setters" do + @obj.name = 'Matt' + @obj.name.should == 'Matt' + end + end + + describe "casted as attribute" do + + before(:each) do + @obj = DummyModel.new(:casted_attribute => {:name => 'whatever'}) + @casted_obj = @obj.casted_attribute + end + + it "should be available from its parent" do + @casted_obj.should be_an_instance_of(WithCastedModelMixin) + end + + it "should have the getters defined" do + @casted_obj.name.should == 'whatever' + end + + it "should know who casted it" do + @casted_obj.casted_by.should == @obj + end + + end + +end \ No newline at end of file diff --git a/spec/couchrest/more/extended_doc_spec.rb b/spec/couchrest/more/extended_doc_spec.rb index eca0dfc..0b10ba6 100644 --- a/spec/couchrest/more/extended_doc_spec.rb +++ b/spec/couchrest/more/extended_doc_spec.rb @@ -1,10 +1,5 @@ require File.dirname(__FILE__) + '/../../spec_helper' -# require File.join(FIXTURE_PATH, 'more', 'card') -# require File.join(FIXTURE_PATH, 'more', 'invoice') -# require File.join(FIXTURE_PATH, 'more', 'service') - - class WithDefaultValues < CouchRest::ExtendedDocument use_database TEST_SERVER.default_database property :preset, :default => {:right => 10, :top_align => false} diff --git a/spec/couchrest/more/property_spec.rb b/spec/couchrest/more/property_spec.rb index 9a341ed..ef44141 100644 --- a/spec/couchrest/more/property_spec.rb +++ b/spec/couchrest/more/property_spec.rb @@ -1,7 +1,8 @@ require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper') require File.join(FIXTURE_PATH, 'more', 'card') require File.join(FIXTURE_PATH, 'more', 'invoice') -require File.join(FIXTURE_PATH, 'more', 'service') +require File.join(FIXTURE_PATH, 'more', 'service.rb') + describe "ExtendedDocument properties" do @@ -36,7 +37,7 @@ describe "ExtendedDocument properties" do it "should be auto timestamped" do @card.created_at.should be_nil @card.updated_at.should be_nil - # :emo:hack for autospec + # :emo: hack for autospec Card.use_database(TEST_SERVER.default_database) if @card.database.nil? @card.save @card.created_at.should_not be_nil @@ -52,14 +53,14 @@ describe "ExtendedDocument properties" do it "should be able to be validated" do @card.valid?.should == true end - + it "should let you validate the presence of an attribute" do @card.first_name = nil @card.should_not be_valid @card.errors.should_not be_empty @card.errors.on(:first_name).should == ["First name must not be blank"] end - + it "should validate the presence of 2 attributes" do @invoice.clear @invoice.should_not be_valid @@ -84,6 +85,7 @@ describe "ExtendedDocument properties" do end describe "autovalidation" do + before(:each) do @service = Service.new(:name => "Coumpound analysis", :price => 3_000) end @@ -92,7 +94,12 @@ describe "ExtendedDocument properties" do @service.should be_valid end + it "should not respond to properties not setup" do + @service.respond_to?(:client_name).should be_false + end + describe "property :name, :length => 4...20" do + it "should autovalidate the presence when length is set" do @service.name = nil @service.should_not be_valid