From 16d9e819d7fdead8d3a34564e9913444882112f4 Mon Sep 17 00:00:00 2001 From: wildchild Date: Tue, 21 Jul 2009 03:17:27 +0600 Subject: [PATCH 1/4] Added typecasting of properties --- lib/couchrest/mixins/properties.rb | 63 ++-- lib/couchrest/more/extended_document.rb | 4 +- lib/couchrest/more/property.rb | 182 ++++++++- spec/couchrest/more/casted_model_spec.rb | 2 +- spec/couchrest/more/extended_doc_spec.rb | 14 +- spec/couchrest/more/property_spec.rb | 453 +++++++++++++++++++++-- spec/fixtures/more/article.rb | 4 +- spec/fixtures/more/course.rb | 14 +- spec/fixtures/more/person.rb | 2 +- spec/fixtures/more/question.rb | 2 +- spec/fixtures/more/service.rb | 2 +- 11 files changed, 649 insertions(+), 93 deletions(-) diff --git a/lib/couchrest/mixins/properties.rb b/lib/couchrest/mixins/properties.rb index 2982d30..0f3242a 100644 --- a/lib/couchrest/mixins/properties.rb +++ b/lib/couchrest/mixins/properties.rb @@ -1,4 +1,3 @@ -require 'time' require File.join(File.dirname(__FILE__), '..', 'more', 'property') class Time @@ -56,46 +55,34 @@ module CouchRest def cast_keys return unless self.class.properties self.class.properties.each do |property| - next unless property.casted key = self.has_key?(property.name) ? property.name : property.name.to_sym # Don't cast the property unless it has a value - next unless self[key] - target = property.type - if target.is_a?(Array) - klass = ::CouchRest.constantize(target[0]) - self[property.name] = self[key].collect do |value| - # Auto parse Time objects - obj = ( (property.init_method == 'new') && klass == Time) ? Time.parse(value) : klass.send(property.init_method, value) - obj.casted_by = self if obj.respond_to?(:casted_by) - obj + next if (value = self[key]).nil? + obj = property.typecast(value) + if obj.respond_to?(:casted_by) + obj.casted_by = self + end + self[property.name] = obj + end + end + + protected + + def write_attribute(name, value) + unless (property = property(name)).nil? + if property.casted + self[name] = value + else + self[name] = property.typecast(value) end else - # Auto parse Time objects - self[property.name] = if ((property.init_method == 'new') && target == 'Time') - # Using custom time parsing method because Ruby's default method is toooo slow - self[key].is_a?(String) ? Time.mktime_with_offset(self[key].dup) : self[key] - # Float instances don't get initialized with #new - elsif ((property.init_method == 'new') && target == 'Float') - cast_float(self[key]) - else - # Let people use :send as a Time parse arg - klass = ::CouchRest.constantize(target) - klass.send(property.init_method, self[key].dup) - end - self[property.name].casted_by = self if self[property.name].respond_to?(:casted_by) - end - - end - - def cast_float(value) - begin - Float(value) - rescue - value + self[name] = value end end - - end + + def property(name) + properties.find {|p| p.name == name.to_s} + end module ClassMethods @@ -114,7 +101,7 @@ module CouchRest # check if this property is going to casted options[:casted] = options[:cast_as] ? options[:cast_as] : false property = CouchRest::Property.new(name, (options.delete(:cast_as) || options.delete(:type)), options) - create_property_getter(property) + create_property_getter(property) create_property_setter(property) unless property.read_only == true properties << property end @@ -140,7 +127,7 @@ module CouchRest meth = property.name class_eval <<-EOS def #{meth}=(value) - self['#{meth}'] = value + write_attribute('#{meth}', value) end EOS @@ -150,9 +137,7 @@ module CouchRest EOS end end - end # module ClassMethods - 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 7fd067a..650c32c 100644 --- a/lib/couchrest/more/extended_document.rb +++ b/lib/couchrest/more/extended_document.rb @@ -77,8 +77,8 @@ module CouchRest # decent time format by default. See Time#to_json def self.timestamps! class_eval <<-EOS, __FILE__, __LINE__ - property(:updated_at, :read_only => true, :cast_as => 'Time', :auto_validation => false) - property(:created_at, :read_only => true, :cast_as => 'Time', :auto_validation => false) + property(:updated_at, :read_only => true, :type => 'Time', :auto_validation => false) + property(:created_at, :read_only => true, :type => 'Time', :auto_validation => false) save_callback :before do |object| object['updated_at'] = Time.now diff --git a/lib/couchrest/more/property.rb b/lib/couchrest/more/property.rb index 77e2b90..7b8a5fe 100644 --- a/lib/couchrest/more/property.rb +++ b/lib/couchrest/more/property.rb @@ -1,9 +1,13 @@ +require 'time' +require 'bigdecimal' +require 'bigdecimal/util' + module CouchRest - + # Basic attribute support for adding getter/setter + validation class Property 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 @@ -11,20 +15,182 @@ module CouchRest parse_options(options) self end - - + + def typecast(value) + do_typecast(value, type, init_method) + end + + protected + + def do_typecast(value, target, init_method) + return nil if value.nil? + + if target == 'String' then typecast_to_string(value) + elsif target == 'Boolean' then typecast_to_boolean(value) + elsif target == 'Integer' then typecast_to_integer(value) + elsif target == 'Float' then typecast_to_float(value) + elsif target == 'BigDecimal' then typecast_to_bigdecimal(value) + elsif target == 'DateTime' then typecast_to_datetime(value) + elsif target == 'Time' then typecast_to_time(value) + elsif target == 'Date' then typecast_to_date(value) + elsif target == 'Class' then typecast_to_class(value) + elsif target.is_a?(Array) then typecast_array(value, target, init_method) + else + @klass ||= ::CouchRest.constantize(target) + value.kind_of?(@klass) ? value : @klass.send(init_method, value.dup) + end + end + + def typecast_array(value, target, init_method) + value.map { |v| do_typecast(v, target[0], init_method) } + end + + # Typecast a value to an Integer + def typecast_to_integer(value) + value.kind_of?(Integer) ? value : typecast_to_numeric(value, :to_i) + end + + # Typecast a value to a String + def typecast_to_string(value) + value.to_s + end + + # Typecast a value to a true or false + def typecast_to_boolean(value) + return value if value == true || value == false + + if value.kind_of?(Integer) + return true if value == 1 + return false if value == 0 + elsif value.respond_to?(:to_str) + return true if %w[ true 1 t ].include?(value.to_str.downcase) + return false if %w[ false 0 f ].include?(value.to_str.downcase) + end + + value + end + + # Typecast a value to a BigDecimal + def typecast_to_bigdecimal(value) + return value if value.kind_of?(BigDecimal) + + if value.kind_of?(Integer) + value.to_s.to_d + else + typecast_to_numeric(value, :to_d) + end + end + + # Typecast a value to a Float + def typecast_to_float(value) + return value if value.kind_of?(Float) + typecast_to_numeric(value, :to_f) + end + + # 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/ + $1.send(method) + else + value + end + elsif value.respond_to?(method) + value.send(method) + else + value + end + end + + # Typecasts an arbitrary value to a DateTime. + # Handles both Hashes and DateTime instances. + def typecast_to_datetime(value) + return value if value.kind_of?(DateTime) + + if value.is_a?(Hash) + typecast_hash_to_datetime(value) + else + DateTime.parse(value.to_s) + end + rescue ArgumentError + value + end + + # Typecasts an arbitrary value to a Date + # Handles both Hashes and Date instances. + def typecast_to_date(value) + return value if value.kind_of?(Date) + + if value.is_a?(Hash) + typecast_hash_to_date(value) + else + Date.parse(value.to_s) + end + rescue ArgumentError + value + end + + # Typecasts an arbitrary value to a Time + # Handles both Hashes and Time instances. + def typecast_to_time(value) + return value if value.kind_of?(Time) + + if value.is_a?(Hash) + typecast_hash_to_time(value) + else + Time.parse(value.to_s) + end + rescue ArgumentError + value + end + + # Creates a DateTime instance from a Hash with keys :year, :month, :day, + # :hour, :min, :sec + def typecast_hash_to_datetime(value) + DateTime.new(*extract_time(value)) + end + + # Creates a Date instance from a Hash with keys :year, :month, :day + def typecast_hash_to_date(value) + Date.new(*extract_time(value)[0, 3]) + end + + # Creates a Time instance from a Hash with keys :year, :month, :day, + # :hour, :min, :sec + def typecast_hash_to_time(value) + Time.local(*extract_time(value)) + end + + # Extracts the given args from the hash. If a value does not exist, it + # uses the value of Time.now. + def extract_time(value) + now = Time.now + + [:year, :month, :day, :hour, :min, :sec].map do |segment| + typecast_to_numeric(value.fetch(segment, now.send(segment)), :to_i) + end + end + + # Typecast a value to a Class + def typecast_to_class(value) + return value if value.kind_of?(Class) + ::CouchRest.constantize(value.to_s) + rescue NameError + value + end + private - + def parse_type(type) if type.nil? @type = 'String' elsif type.is_a?(Array) && type.empty? - @type = 'Array' + @type = ['Object'] else @type = type.is_a?(Array) ? [type.first.to_s] : type.to_s end end - + def parse_options(options) return if options.empty? @validation_format = options.delete(:format) if options[:format] @@ -35,6 +201,6 @@ module CouchRest @init_method = options[:send] ? options.delete(:send) : 'new' @options = options end - + end end diff --git a/spec/couchrest/more/casted_model_spec.rb b/spec/couchrest/more/casted_model_spec.rb index 75b0c57..5d88f2f 100644 --- a/spec/couchrest/more/casted_model_spec.rb +++ b/spec/couchrest/more/casted_model_spec.rb @@ -10,7 +10,7 @@ class WithCastedModelMixin < Hash include CouchRest::CastedModel property :name property :no_value - property :details, :default => {} + property :details, :type => 'Object', :default => {} property :casted_attribute, :cast_as => 'WithCastedModelMixin' end diff --git a/spec/couchrest/more/extended_doc_spec.rb b/spec/couchrest/more/extended_doc_spec.rb index 5d71d7f..ea614ed 100644 --- a/spec/couchrest/more/extended_doc_spec.rb +++ b/spec/couchrest/more/extended_doc_spec.rb @@ -7,11 +7,11 @@ describe "ExtendedDocument" do class WithDefaultValues < CouchRest::ExtendedDocument use_database TEST_SERVER.default_database - property :preset, :default => {:right => 10, :top_align => false} - property :set_by_proc, :default => Proc.new{Time.now}, :cast_as => 'Time' - property :tags, :default => [] + property :preset, :type => 'Object', :default => {:right => 10, :top_align => false} + property :set_by_proc, :default => Proc.new{Time.now}, :cast_as => 'Time' + property :tags, :type => ['String'], :default => [] property :read_only_with_default, :default => 'generic', :read_only => true - property :default_false, :default => false + property :default_false, :type => 'Boolean', :default => false property :name timestamps! end @@ -314,7 +314,7 @@ describe "ExtendedDocument" do "professor" => { "name" => ["Mark", "Hinchliff"] }, - "final_test_at" => "2008/12/19 13:00:00 +0800" + "ends_at" => "2008/12/19 13:00:00 +0800" } r = Course.database.save_doc course_doc @course = Course.get r['id'] @@ -325,8 +325,8 @@ describe "ExtendedDocument" do it "should instantiate the professor as a person" do @course['professor'].last_name.should == "Hinchliff" end - it "should instantiate the final_test_at as a Time" do - @course['final_test_at'].should == Time.parse("2008/12/19 13:00:00 +0800") + it "should instantiate the ends_at as a Time" do + @course['ends_at'].should == Time.parse("2008/12/19 13:00:00 +0800") end end diff --git a/spec/couchrest/more/property_spec.rb b/spec/couchrest/more/property_spec.rb index 6782789..bf22aba 100644 --- a/spec/couchrest/more/property_spec.rb +++ b/spec/couchrest/more/property_spec.rb @@ -3,7 +3,7 @@ require File.join(FIXTURE_PATH, 'more', 'person') 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', 'event') +require File.join(FIXTURE_PATH, 'more', 'course') describe "ExtendedDocument properties" do @@ -39,11 +39,11 @@ describe "ExtendedDocument properties" do it "should let you use an alias for a casted attribute" do @card.cast_alias = Person.new(:name => "Aimonetti") - @card.cast_alias.name.should == "Aimonetti" - @card.calias.name.should == "Aimonetti" + @card.cast_alias.name.should == ["Aimonetti"] + @card.calias.name.should == ["Aimonetti"] card = Card.new(:first_name => "matt", :cast_alias => {:name => "Aimonetti"}) - card.cast_alias.name.should == "Aimonetti" - card.calias.name.should == "Aimonetti" + card.cast_alias.name.should == ["Aimonetti"] + card.calias.name.should == ["Aimonetti"] end it "should be auto timestamped" do @@ -130,40 +130,437 @@ describe "ExtendedDocument properties" do end describe "casting" do - describe "cast keys to any type" do - before(:all) do - event_doc = { :subject => "Some event", :occurs_at => Time.now } - e = Event.database.save_doc event_doc + before(:each) do + @course = Course.new(:title => 'Relaxation') + end - @event = Event.get e['id'] + describe "when value is nil" do + it "leaves the value unchanged" do + @course.title = nil + @course['title'].should == nil end - it "should cast created_at to Time" do - @event['occurs_at'].should be_an_instance_of(Time) + end + + describe "when type primitive is an Object" do + it "it should not cast given value" do + @course.participants = [{}, 'q', 1] + @course['participants'].should eql([{}, 'q', 1]) end end - describe "casting to Float object" do - class RootBeerFloat < CouchRest::ExtendedDocument - use_database DB - property :price, :cast_as => 'Float' + describe "when type primitive is a String" do + it "keeps string value unchanged" do + value = "1.0" + @course.title = value + @course['title'].should equal(value) end - it "should convert a string into a float if casted as so" do - RootBeerFloat.new(:price => '12.50').price.should == 12.50 - RootBeerFloat.new(:price => '9').price.should == 9.0 - RootBeerFloat.new(:price => '-9').price.should == -9.0 + it "it casts to string representation of the value" do + @course.title = 1.0 + @course['title'].should eql("1.0") end - - it "should not convert a string if it's not a string that can be cast as a float" do - RootBeerFloat.new(:price => 'test').price.should == 'test' + end + + describe 'when type primitive is a Float' do + it 'returns same value if a float' do + value = 24.0 + @course.estimate = value + @course['estimate'].should equal(value) end - - it "should work fine when a float is being passed" do - RootBeerFloat.new(:price => 9.99).price.should == 9.99 + + it 'returns float representation of a zero string integer' do + @course.estimate = '0' + @course['estimate'].should eql(0.0) + end + + it 'returns float representation of a positive string integer' do + @course.estimate = '24' + @course['estimate'].should eql(24.0) + end + + it 'returns float representation of a negative string integer' do + @course.estimate = '-24' + @course['estimate'].should eql(-24.0) + end + + it 'returns float representation of a zero string float' do + @course.estimate = '0.0' + @course['estimate'].should eql(0.0) + end + + it 'returns float representation of a positive string float' do + @course.estimate = '24.35' + @course['estimate'].should eql(24.35) + end + + it 'returns float representation of a negative string float' do + @course.estimate = '-24.35' + @course['estimate'].should eql(-24.35) + end + + it 'returns float representation of a zero string float, with no leading digits' do + @course.estimate = '.0' + @course['estimate'].should eql(0.0) + end + + it 'returns float representation of a positive string float, with no leading digits' do + @course.estimate = '.41' + @course['estimate'].should eql(0.41) + end + + it 'returns float representation of a zero integer' do + @course.estimate = 0 + @course['estimate'].should eql(0.0) + end + + it 'returns float representation of a positive integer' do + @course.estimate = 24 + @course['estimate'].should eql(24.0) + end + + it 'returns float representation of a negative integer' do + @course.estimate = -24 + @course['estimate'].should eql(-24.0) + end + + it 'returns float representation of a zero decimal' do + @course.estimate = BigDecimal('0.0') + @course['estimate'].should eql(0.0) + end + + it 'returns float representation of a positive decimal' do + @course.estimate = BigDecimal('24.35') + @course['estimate'].should eql(24.35) + end + + it 'returns float representation of a negative decimal' do + @course.estimate = BigDecimal('-24.35') + @course['estimate'].should eql(-24.35) + 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 + it 'returns same value if an integer' do + value = 24 + @course.hours = value + @course['hours'].should equal(value) + end + + it 'returns integer representation of a zero string integer' do + @course.hours = '0' + @course['hours'].should eql(0) + end + + it 'returns integer representation of a positive string integer' do + @course.hours = '24' + @course['hours'].should eql(24) + end + + it 'returns integer representation of a negative string integer' do + @course.hours = '-24' + @course['hours'].should eql(-24) + end + + it 'returns integer representation of a zero string float' do + @course.hours = '0.0' + @course['hours'].should eql(0) + end + + it 'returns integer representation of a positive string float' do + @course.hours = '24.35' + @course['hours'].should eql(24) + end + + it 'returns integer representation of a negative string float' do + @course.hours = '-24.35' + @course['hours'].should eql(-24) + end + + it 'returns integer representation of a zero string float, with no leading digits' do + @course.hours = '.0' + @course['hours'].should eql(0) + end + + it 'returns integer representation of a positive string float, with no leading digits' do + @course.hours = '.41' + @course['hours'].should eql(0) + end + + it 'returns integer representation of a zero float' do + @course.hours = 0.0 + @course['hours'].should eql(0) + end + + it 'returns integer representation of a positive float' do + @course.hours = 24.35 + @course['hours'].should eql(24) + end + + it 'returns integer representation of a negative float' do + @course.hours = -24.35 + @course['hours'].should eql(-24) + end + + it 'returns integer representation of a zero decimal' do + @course.hours = '0.0' + @course['hours'].should eql(0) + end + + it 'returns integer representation of a positive decimal' do + @course.hours = '24.35' + @course['hours'].should eql(24) + end + + it 'returns integer representation of a negative decimal' do + @course.hours = '-24.35' + @course['hours'].should eql(-24) + end + + [ Object.new, true, '00.0', '0.', '-.0', 'string' ].each do |value| + it "does not typecast non-numeric value #{value.inspect}" do + @course.hours = value + @course['hours'].should equal(value) + end + end + end + + describe 'when type primitive is a BigDecimal' do + it 'returns same value if a decimal' do + value = BigDecimal('24.0') + @course.profit = value + @course['profit'].should equal(value) + end + + it 'returns decimal representation of a zero string integer' do + @course.profit = '0' + @course['profit'].should eql(BigDecimal('0.0')) + end + + it 'returns decimal representation of a positive string integer' do + @course.profit = '24' + @course['profit'].should eql(BigDecimal('24.0')) + end + + it 'returns decimal representation of a negative string integer' do + @course.profit = '-24' + @course['profit'].should eql(BigDecimal('-24.0')) + end + + it 'returns decimal representation of a zero string float' do + @course.profit = '0.0' + @course['profit'].should eql(BigDecimal('0.0')) + end + + it 'returns decimal representation of a positive string float' do + @course.profit = '24.35' + @course['profit'].should eql(BigDecimal('24.35')) + end + + it 'returns decimal representation of a negative string float' do + @course.profit = '-24.35' + @course['profit'].should eql(BigDecimal('-24.35')) + end + + it 'returns decimal representation of a zero string float, with no leading digits' do + @course.profit = '.0' + @course['profit'].should eql(BigDecimal('0.0')) + end + + it 'returns decimal representation of a positive string float, with no leading digits' do + @course.profit = '.41' + @course['profit'].should eql(BigDecimal('0.41')) + end + + it 'returns decimal representation of a zero integer' do + @course.profit = 0 + @course['profit'].should eql(BigDecimal('0.0')) + end + + it 'returns decimal representation of a positive integer' do + @course.profit = 24 + @course['profit'].should eql(BigDecimal('24.0')) + end + + it 'returns decimal representation of a negative integer' do + @course.profit = -24 + @course['profit'].should eql(BigDecimal('-24.0')) + end + + it 'returns decimal representation of a zero float' do + @course.profit = 0.0 + @course['profit'].should eql(BigDecimal('0.0')) + end + + it 'returns decimal representation of a positive float' do + @course.profit = 24.35 + @course['profit'].should eql(BigDecimal('24.35')) + end + + it 'returns decimal representation of a negative float' do + @course.profit = -24.35 + @course['profit'].should eql(BigDecimal('-24.35')) + end + + [ Object.new, true, '00.0', '0.', '-.0', 'string' ].each do |value| + it "does not typecast non-numeric value #{value.inspect}" do + @course.profit = value + @course['profit'].should equal(value) + end + end + end + + describe 'when type primitive is a DateTime' do + describe 'and value given as a hash with keys like :year, :month, etc' do + it 'builds a DateTime instance from hash values' do + @course.updated_at = { + :year => '2006', + :month => '11', + :day => '23', + :hour => '12', + :min => '0', + :sec => '0' + } + result = @course['updated_at'] + + result.should be_kind_of(DateTime) + result.year.should eql(2006) + result.month.should eql(11) + result.day.should eql(23) + result.hour.should eql(12) + result.min.should eql(0) + result.sec.should eql(0) + end + end + + describe 'and value is a string' do + it 'parses the string' do + @course.updated_at = 'Dec, 2006' + @course['updated_at'].month.should == 12 + end + end + + it 'does not typecast non-datetime values' do + @course.updated_at = 'not-datetime' + @course['updated_at'].should eql('not-datetime') + end + end + + describe 'when type primitive is a Date' do + describe 'and value given as a hash with keys like :year, :month, etc' do + it 'builds a Date instance from hash values' do + @course.started_on = { + :year => '2007', + :month => '3', + :day => '25' + } + result = @course['started_on'] + + result.should be_kind_of(Date) + result.year.should eql(2007) + result.month.should eql(3) + result.day.should eql(25) + end + end + + describe 'and value is a string' do + it 'parses the string' do + @course.started_on = 'Dec 20th, 2006' + @course.started_on.month.should == 12 + @course.started_on.day.should == 20 + @course.started_on.year.should == 2006 + end + end + + it 'does not typecast non-date values' do + @course.started_on = 'not-date' + @course['started_on'].should eql('not-date') + end + end + + describe 'when type primitive is a Time' do + describe 'and value given as a hash with keys like :year, :month, etc' do + it 'builds a Time instance from hash values' do + @course.ends_at = { + :year => '2006', + :month => '11', + :day => '23', + :hour => '12', + :min => '0', + :sec => '0' + } + result = @course['ends_at'] + + result.should be_kind_of(Time) + result.year.should eql(2006) + result.month.should eql(11) + result.day.should eql(23) + result.hour.should eql(12) + result.min.should eql(0) + result.sec.should eql(0) + end + end + + describe 'and value is a string' do + it 'parses the string' do + @course.ends_at = '22:24' + @course['ends_at'].hour.should eql(22) + @course['ends_at'].min.should eql(24) + end + end + + it 'does not typecast non-time values' do + pending 'Time#parse is too permissive' + @course.started_on = 'not-time' + @course['ends_at'].should eql('not-time') + end + end + + describe 'when type primitive is a Class' do + it 'returns same value if a class' do + value = Course + @course.klass = value + @course['klass'].should equal(value) + end + + it 'returns the class if found' do + @course.klass = 'Course' + @course['klass'].should eql(Course) + end + + it 'does not typecast non-class values' do + @course.klass = 'NoClass' + @course['klass'].should eql('NoClass') end - end + describe 'when type primitive is a Boolean' do + [ true, 'true', 'TRUE', '1', 1, 't', 'T' ].each do |value| + it "returns true when value is #{value.inspect}" do + @course.active = value + @course['active'].should be_true + end + end + + [ false, 'false', 'FALSE', '0', 0, 'f', 'F' ].each do |value| + it "returns false when value is #{value.inspect}" do + @course.active = value + @course['active'].should be_false + end + end + + [ 'string', 2, 1.0, BigDecimal('1.0'), DateTime.now, Time.now, Date.today, Class, Object.new, ].each do |value| + it "does not typecast value #{value.inspect}" do + @course.active = value + @course['active'].should equal(value) + end + end + end end - end diff --git a/spec/fixtures/more/article.rb b/spec/fixtures/more/article.rb index e0a6393..be90de6 100644 --- a/spec/fixtures/more/article.rb +++ b/spec/fixtures/more/article.rb @@ -19,10 +19,10 @@ class Article < CouchRest::ExtendedDocument return sum(values); }" - property :date + property :date, :type => 'Date' property :slug, :read_only => true property :title - property :tags + property :tags, :type => ['String'] timestamps! diff --git a/spec/fixtures/more/course.rb b/spec/fixtures/more/course.rb index b639d34..eada30f 100644 --- a/spec/fixtures/more/course.rb +++ b/spec/fixtures/more/course.rb @@ -5,9 +5,17 @@ class Course < CouchRest::ExtendedDocument use_database TEST_SERVER.default_database property :title - property :questions, :cast_as => ['Question'] - property :professor, :cast_as => 'Person' - property :final_test_at, :cast_as => 'Time' + property :questions, :cast_as => ['Question'] + property :professor, :cast_as => 'Person' + property :participants, :type => ['Object'] + property :ends_at, :type => 'Time' + property :estimate, :type => 'Float' + property :hours, :type => 'Integer' + property :profit, :type => 'BigDecimal' + property :started_on, :type => 'Date' + property :updated_at, :type => 'DateTime' + property :active, :type => 'Boolean' + property :klass, :type => 'Class' view_by :title view_by :dept, :ducktype => true diff --git a/spec/fixtures/more/person.rb b/spec/fixtures/more/person.rb index ddc1bfd..e8da1d7 100644 --- a/spec/fixtures/more/person.rb +++ b/spec/fixtures/more/person.rb @@ -1,6 +1,6 @@ class Person < Hash include ::CouchRest::CastedModel - property :name + property :name, :type => ['String'] def last_name name.last diff --git a/spec/fixtures/more/question.rb b/spec/fixtures/more/question.rb index bec7803..0d29e21 100644 --- a/spec/fixtures/more/question.rb +++ b/spec/fixtures/more/question.rb @@ -2,5 +2,5 @@ class Question < Hash include ::CouchRest::CastedModel property :q - property :a + property :a, :type => 'Object' end \ No newline at end of file diff --git a/spec/fixtures/more/service.rb b/spec/fixtures/more/service.rb index 6ecf276..77b28ef 100644 --- a/spec/fixtures/more/service.rb +++ b/spec/fixtures/more/service.rb @@ -7,6 +7,6 @@ class Service < CouchRest::ExtendedDocument # Official Schema property :name, :length => 4...20 - property :price, :type => Integer + property :price, :type => 'Integer' end \ No newline at end of file From e27135cb1ee68f632e0bc1909114486d1d70bffc Mon Sep 17 00:00:00 2001 From: wildchild Date: Tue, 21 Jul 2009 03:18:07 +0600 Subject: [PATCH 2/4] Fixed required_field_validator to behave correctly with boolean fields --- .../validation/validators/required_field_validator.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/couchrest/validation/validators/required_field_validator.rb b/lib/couchrest/validation/validators/required_field_validator.rb index 17e3132..ee18b2a 100644 --- a/lib/couchrest/validation/validators/required_field_validator.rb +++ b/lib/couchrest/validation/validators/required_field_validator.rb @@ -37,7 +37,7 @@ module CouchRest def call(target) value = target.validation_property_value(field_name) - property = target.validation_property(field_name) + property = target.validation_property(field_name.to_s) return true if present?(value, property) error_message = @options[:message] || default_error(property) @@ -66,7 +66,7 @@ module CouchRest # Returns false for other property types. # Returns false for non-properties. def boolean_type?(property) - property ? property.type == TrueClass : false + property ? property.type == 'Boolean' : false end end # class RequiredFieldValidator From f65d8bbbcc8a474927fb758479bcef7b1b2ba30d Mon Sep 17 00:00:00 2001 From: wildchild Date: Tue, 21 Jul 2009 05:01:34 +0600 Subject: [PATCH 3/4] Should cast casted attribute on direct assignment --- lib/couchrest/mixins/properties.rb | 18 ++++++++---------- .../couchrest/more/casted_extended_doc_spec.rb | 4 ++-- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/couchrest/mixins/properties.rb b/lib/couchrest/mixins/properties.rb index 0f3242a..1ba6342 100644 --- a/lib/couchrest/mixins/properties.rb +++ b/lib/couchrest/mixins/properties.rb @@ -58,11 +58,7 @@ module CouchRest key = self.has_key?(property.name) ? property.name : property.name.to_sym # Don't cast the property unless it has a value next if (value = self[key]).nil? - obj = property.typecast(value) - if obj.respond_to?(:casted_by) - obj.casted_by = self - end - self[property.name] = obj + write_property(property, value) end end @@ -70,16 +66,18 @@ module CouchRest def write_attribute(name, value) unless (property = property(name)).nil? - if property.casted - self[name] = value - else - self[name] = property.typecast(value) - end + write_property(property, value) else self[name] = value end end + def write_property(property, value) + value = property.typecast(value) + value.casted_by = self if value.respond_to?(:casted_by) + self[property.name] = value + end + def property(name) properties.find {|p| p.name == name.to_s} end diff --git a/spec/couchrest/more/casted_extended_doc_spec.rb b/spec/couchrest/more/casted_extended_doc_spec.rb index 51afd77..a42d155 100644 --- a/spec/couchrest/more/casted_extended_doc_spec.rb +++ b/spec/couchrest/more/casted_extended_doc_spec.rb @@ -50,9 +50,9 @@ describe "assigning a value to casted attribute after initializing an object" do @car.driver.name.should == 'Matt' end - it "should not cast attribute" do + it "should cast attribute" do @car.driver = JSON.parse(JSON.generate(@driver)) - @car.driver.should_not be_instance_of(Driver) + @car.driver.should be_instance_of(Driver) end end From e4ad16b77c0e16b14150999d8a26b9cfa6478b4f Mon Sep 17 00:00:00 2001 From: wildchild Date: Tue, 21 Jul 2009 09:31:18 +0600 Subject: [PATCH 4/4] Use Time#mktime_with_offset --- lib/couchrest/more/property.rb | 4 +++- spec/couchrest/more/property_spec.rb | 14 +++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/couchrest/more/property.rb b/lib/couchrest/more/property.rb index 7b8a5fe..7fb903b 100644 --- a/lib/couchrest/more/property.rb +++ b/lib/couchrest/more/property.rb @@ -138,10 +138,12 @@ module CouchRest if value.is_a?(Hash) typecast_hash_to_time(value) else - Time.parse(value.to_s) + Time.mktime_with_offset(value.to_s) end rescue ArgumentError value + rescue TypeError + value end # Creates a DateTime instance from a Hash with keys :year, :month, :day, diff --git a/spec/couchrest/more/property_spec.rb b/spec/couchrest/more/property_spec.rb index bf22aba..5bd5275 100644 --- a/spec/couchrest/more/property_spec.rb +++ b/spec/couchrest/more/property_spec.rb @@ -509,15 +509,19 @@ describe "ExtendedDocument properties" do describe 'and value is a string' do it 'parses the string' do - @course.ends_at = '22:24' - @course['ends_at'].hour.should eql(22) - @course['ends_at'].min.should eql(24) + t = Time.now + @course.ends_at = t.strftime('%Y/%m/%d %H:%M:%S %z') + @course['ends_at'].year.should eql(t.year) + @course['ends_at'].month.should eql(t.month) + @course['ends_at'].day.should eql(t.day) + @course['ends_at'].hour.should eql(t.hour) + @course['ends_at'].min.should eql(t.min) + @course['ends_at'].sec.should eql(t.sec) end end it 'does not typecast non-time values' do - pending 'Time#parse is too permissive' - @course.started_on = 'not-time' + @course.ends_at = 'not-time' @course['ends_at'].should eql('not-time') end end