From 16d9e819d7fdead8d3a34564e9913444882112f4 Mon Sep 17 00:00:00 2001 From: wildchild Date: Tue, 21 Jul 2009 03:17:27 +0600 Subject: [PATCH 01/11] 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 02/11] 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 03/11] 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 04/11] 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 From bb2c7e87ad5c0b6b70e79f2afd256ca9afb21716 Mon Sep 17 00:00:00 2001 From: Sam Lown iMac Date: Mon, 28 Sep 2009 21:42:17 +0200 Subject: [PATCH 05/11] Fixing incorrectly generated document URIs with testing --- lib/couchrest/core/document.rb | 2 +- spec/couchrest/core/document_spec.rb | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/couchrest/core/document.rb b/lib/couchrest/core/document.rb index cd66285..4b79206 100644 --- a/lib/couchrest/core/document.rb +++ b/lib/couchrest/core/document.rb @@ -65,7 +65,7 @@ module CouchRest # Returns the CouchDB uri for the document def uri(append_rev = false) return nil if new? - couch_uri = "http://#{database.root}/#{CGI.escape(id)}" + couch_uri = "#{database.root}/#{CGI.escape(id)}" if append_rev == true couch_uri << "?rev=#{rev}" elsif append_rev.kind_of?(Integer) diff --git a/spec/couchrest/core/document_spec.rb b/spec/couchrest/core/document_spec.rb index 287ee79..2241598 100644 --- a/spec/couchrest/core/document_spec.rb +++ b/spec/couchrest/core/document_spec.rb @@ -83,6 +83,14 @@ describe CouchRest::Document do @doc.id.should == @resp["id"] @doc.rev.should == @resp["rev"] end + it "should generate a correct URI" do + @doc.uri.should == "#{@db.root}/#{@doc.id}" + URI.parse(@doc.uri).to_s.should == @doc.uri + end + it "should generate a correct URI with revision" do + @doc.uri(true).should == "#{@db.root}/#{@doc.id}?rev=#{@doc.rev}" + URI.parse(@doc.uri(true)).to_s.should == @doc.uri(true) + end end describe "bulk saving" do @@ -264,4 +272,4 @@ describe "dealing with attachments" do end end -end \ No newline at end of file +end From 151ea5566d872c8a4887add2c6e77f2aa79bd1f3 Mon Sep 17 00:00:00 2001 From: Tim Heighes Date: Fri, 26 Feb 2010 00:25:51 +0100 Subject: [PATCH 06/11] Fixes for 4 of the original 10 failing specs --- lib/couchrest/mixins/properties.rb | 4 ++++ spec/couchrest/more/property_spec.rb | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/couchrest/mixins/properties.rb b/lib/couchrest/mixins/properties.rb index 21e9ca2..bfd6f15 100644 --- a/lib/couchrest/mixins/properties.rb +++ b/lib/couchrest/mixins/properties.rb @@ -58,6 +58,10 @@ 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? + # Don't cast the property if it is not accessible + if self.class.respond_to? :accessible_properties + next if self.class.accessible_properties.index(key).nil? + end write_property(property, value) end end diff --git a/spec/couchrest/more/property_spec.rb b/spec/couchrest/more/property_spec.rb index a98e1a9..9156795 100644 --- a/spec/couchrest/more/property_spec.rb +++ b/spec/couchrest/more/property_spec.rb @@ -175,8 +175,9 @@ describe "ExtendedDocument properties" do @course['participants'].should eql([{}, 'q', 1]) end - it "should cast end_date to Date" do - @event['end_date'].should be_an_instance_of(Date) + it "should cast started_on to Date" do + @course.started_on = Date.today + @course['started_on'].should be_an_instance_of(Date) end end From d992f443653dbacbc9ad5e38214b20c58bbdd3c2 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Wed, 3 Mar 2010 02:18:32 +0000 Subject: [PATCH 07/11] Refactoring typecast so that it is less invasive and uses latest code base --- lib/couchrest/mixins/properties.rb | 107 ++++++---------- lib/couchrest/more/property.rb | 171 +------------------------ lib/couchrest/more/typecast.rb | 180 +++++++++++++++++++++++++++ spec/couchrest/more/property_spec.rb | 4 +- spec/fixtures/more/course.rb | 4 +- spec/fixtures/more/event.rb | 3 +- 6 files changed, 222 insertions(+), 247 deletions(-) create mode 100644 lib/couchrest/more/typecast.rb diff --git a/lib/couchrest/mixins/properties.rb b/lib/couchrest/mixins/properties.rb index 7da8169..368b3c0 100644 --- a/lib/couchrest/mixins/properties.rb +++ b/lib/couchrest/mixins/properties.rb @@ -1,23 +1,6 @@ +require 'time' require File.join(File.dirname(__FILE__), '..', 'more', 'property') - -class Time - # returns a local time value much faster than Time.parse - def self.mktime_with_offset(string) - string =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})[T|\s](\d{2}):(\d{2}):(\d{2})([\+|\s|\-])*(\d{2}):?(\d{2})/ - # $1 = year - # $2 = month - # $3 = day - # $4 = hours - # $5 = minutes - # $6 = seconds - # $7 = time zone direction - # $8 = tz difference - # utc time with wrong TZ info: - time = mktime($1, RFC2822_MONTH_NAME[$2.to_i - 1], $3, $4, $5, $6, $7) - tz_difference = ("#{$7 == '-' ? '+' : '-'}#{$8}".to_i * 3600) - time + tz_difference + zone_offset(time.zone) - end -end +require File.join(File.dirname(__FILE__), '..', 'more', 'typecast') module CouchRest module Mixins @@ -25,6 +8,8 @@ module CouchRest class IncludeError < StandardError; end + include ::CouchRest::More::Typecast + def self.included(base) base.class_eval <<-EOS, __FILE__, __LINE__ + 1 extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties) @@ -55,57 +40,41 @@ module CouchRest def cast_keys return unless self.class.properties self.class.properties.each do |property| - 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? - # Don't cast the property if it is not accessible - if self.class.respond_to? :accessible_properties - next if self.class.accessible_properties.index(key).nil? - end - write_property(property, value) + cast_property(property) end end - - protected - - def write_attribute(name, value) - unless (property = property(name)).nil? - write_property(property, value) - else - self[name] = value + + def cast_property(property, assigned=false) + return 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 + return unless self[key] + if property.type.is_a?(Array) + klass = ::CouchRest.constantize(property.type[0]) + arr = self[key].dup.collect do |value| + value = typecast_value(value, klass, property.init_method) + associate_casted_to_parent(value, assigned) + value end + self[key] = klass != String ? CastedArray.new(arr) : arr + self[key].casted_by = self if self[key].respond_to?(:casted_by) + else + if property.type.downcase == 'boolean' + klass = TrueClass + else + klass = ::CouchRest.constantize(property.type) + end + + self[key] = typecast_value(self[key], klass, property.init_method) + associate_casted_to_parent(self[key], assigned) 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 + end def associate_casted_to_parent(casted, assigned) casted.casted_by = self if casted.respond_to?(:casted_by) casted.document_saved = true if !assigned && casted.respond_to?(:document_saved) end - def convert_property_value(property, klass, value) - if ((property.init_method == 'new') && klass == Time) - # Using custom time parsing method because Ruby's default method is toooo slow - value.is_a?(String) ? Time.mktime_with_offset(value.dup) : value - # Float instances don't get initialized with #new - elsif ((property.init_method == 'new') && klass == Float) - cast_float(value) - # 'boolean' type is simply used to generate a property? accessor method - elsif ((property.init_method == 'new') && klass == TrueClass) - value - else - klass.send(property.init_method, value.dup) - end - end - def cast_property_by_name(property_name) return unless self.class.properties property = self.class.properties.detect{|property| property.name == property_name} @@ -113,14 +82,7 @@ module CouchRest cast_property(property, true) end - def cast_float(value) - begin - Float(value) - rescue - value - end - end - + module ClassMethods def property(name, options={}) @@ -136,9 +98,9 @@ module CouchRest # make sure to use a mutex. def define_property(name, options={}) # check if this property is going to casted - options[:casted] = options[:cast_as] ? options[:cast_as] : false + options[:casted] = !!(options[:cast_as] || options[:type]) 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 @@ -176,7 +138,8 @@ module CouchRest property_name = property.name class_eval <<-EOS def #{property_name}=(value) - write_attribute('#{property_name}', value) + self['#{property_name}'] = value + cast_property_by_name('#{property_name}') end EOS @@ -186,7 +149,9 @@ module CouchRest EOS end end + end # module ClassMethods + end end end diff --git a/lib/couchrest/more/property.rb b/lib/couchrest/more/property.rb index 43e9a62..88ce725 100644 --- a/lib/couchrest/more/property.rb +++ b/lib/couchrest/more/property.rb @@ -1,7 +1,3 @@ -require 'time' -require 'bigdecimal' -require 'bigdecimal/util' - module CouchRest # Basic attribute support for adding getter/setter + validation @@ -16,171 +12,6 @@ module CouchRest 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.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, - # :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) @@ -224,4 +55,4 @@ class CastedArray < Array obj.casted_by = self.casted_by if obj.respond_to?(:casted_by) super(index, obj) end -end \ No newline at end of file +end diff --git a/lib/couchrest/more/typecast.rb b/lib/couchrest/more/typecast.rb new file mode 100644 index 0000000..f866b7a --- /dev/null +++ b/lib/couchrest/more/typecast.rb @@ -0,0 +1,180 @@ +require 'time' +require 'bigdecimal' +require 'bigdecimal/util' +require File.join(File.dirname(__FILE__), '..', 'more', 'property') + +class Time + # returns a local time value much faster than Time.parse + def self.mktime_with_offset(string) + string =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})[T|\s](\d{2}):(\d{2}):(\d{2})([\+|\s|\-])*(\d{2}):?(\d{2})/ + # $1 = year + # $2 = month + # $3 = day + # $4 = hours + # $5 = minutes + # $6 = seconds + # $7 = time zone direction + # $8 = tz difference + # utc time with wrong TZ info: + time = mktime($1, RFC2822_MONTH_NAME[$2.to_i - 1], $3, $4, $5, $6, $7) + tz_difference = ("#{$7 == '-' ? '+' : '-'}#{$8}".to_i * 3600) + time + tz_difference + zone_offset(time.zone) + end +end + +module CouchRest + module More + module Typecast + + def typecast_value(value, klass, init_method) + return nil if value.nil? + + if value.instance_of?(klass) || klass.to_s == 'Object' + value + elsif ['String', 'TrueClass', 'Integer', 'Float', 'BigDecimal', 'DateTime', 'Time', 'Date', 'Class'].include?(klass.to_s) + send('typecast_to_'+klass.to_s.downcase, value) + else + # Allow the init_method to be defined as a Proc for advanced conversion + init_method.is_a?(Proc) ? init_method.call(value) : klass.send(init_method, value) + end + end + + protected + + # 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_trueclass(value) + if value.kind_of?(Integer) + return true if value == 1 + return false if value == 0 + elsif value.respond_to?(:to_s) + return true if %w[ true 1 t ].include?(value.to_s.downcase) + return false if %w[ false 0 f ].include?(value.to_s.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.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, + # :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 + + end + end +end + diff --git a/spec/couchrest/more/property_spec.rb b/spec/couchrest/more/property_spec.rb index 9156795..71adfcd 100644 --- a/spec/couchrest/more/property_spec.rb +++ b/spec/couchrest/more/property_spec.rb @@ -156,7 +156,7 @@ describe "ExtendedDocument properties" do end end end - + describe "casting" do before(:each) do @course = Course.new(:title => 'Relaxation') @@ -625,4 +625,4 @@ describe "a casted model retrieved from the database" do @cat.toys[1].casted_by.should === @cat end end -end \ No newline at end of file +end diff --git a/spec/fixtures/more/course.rb b/spec/fixtures/more/course.rb index eada30f..f340469 100644 --- a/spec/fixtures/more/course.rb +++ b/spec/fixtures/more/course.rb @@ -4,7 +4,7 @@ require File.join(FIXTURE_PATH, 'more', 'person') class Course < CouchRest::ExtendedDocument use_database TEST_SERVER.default_database - property :title + property :title, :cast_as => 'String' property :questions, :cast_as => ['Question'] property :professor, :cast_as => 'Person' property :participants, :type => ['Object'] @@ -19,4 +19,4 @@ class Course < CouchRest::ExtendedDocument view_by :title view_by :dept, :ducktype => true -end \ No newline at end of file +end diff --git a/spec/fixtures/more/event.rb b/spec/fixtures/more/event.rb index 97aa248..81a4cba 100644 --- a/spec/fixtures/more/event.rb +++ b/spec/fixtures/more/event.rb @@ -5,5 +5,4 @@ class Event < CouchRest::ExtendedDocument property :occurs_at, :cast_as => 'Time', :init_method => 'parse' property :end_date, :cast_as => 'Date', :init_method => 'parse' - -end \ No newline at end of file +end From ca23e186f4ed17fb92ed9fd1b87e386b91203d43 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Wed, 3 Mar 2010 20:01:27 +0000 Subject: [PATCH 08/11] Added ActiveRecord like after_initialize callback to ExtendedDocument --- lib/couchrest/more/extended_document.rb | 1 + spec/couchrest/more/extended_doc_spec.rb | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/couchrest/more/extended_document.rb b/lib/couchrest/more/extended_document.rb index fc266ed..28f5359 100644 --- a/lib/couchrest/more/extended_document.rb +++ b/lib/couchrest/more/extended_document.rb @@ -58,6 +58,7 @@ module CouchRest unless self['_id'] && self['_rev'] self['couchrest-type'] = self.class.to_s end + after_initialize if respond_to?(:after_initialize) end # Defines an instance and save it directly to the database diff --git a/spec/couchrest/more/extended_doc_spec.rb b/spec/couchrest/more/extended_doc_spec.rb index 6d558ce..f4ac00c 100644 --- a/spec/couchrest/more/extended_doc_spec.rb +++ b/spec/couchrest/more/extended_doc_spec.rb @@ -104,6 +104,18 @@ describe "ExtendedDocument" do self.other_arg = "foo-#{value}" end end + + class WithAfterInitializeMethod < CouchRest::ExtendedDocument + use_database TEST_SERVER.default_database + + property :some_value + + def after_initialize + self.some_value ||= "value" + end + + end + before(:each) do @obj = WithDefaultValues.new @@ -698,6 +710,13 @@ describe "ExtendedDocument" do @doc.other_arg.should == "foo-foo" end end + + describe "initialization" do + it "should call after_initialize method if available" do + @doc = WithAfterInitializeMethod.new + @doc['some_value'].should eql('value') + end + end describe "recursive validation on an extended document" do before :each do From c906be229f1b6d1e1502af8373b345fb403afbdc Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Fri, 5 Mar 2010 14:40:51 +0000 Subject: [PATCH 09/11] Small fix --- lib/couchrest/more/extended_document.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/couchrest/more/extended_document.rb b/lib/couchrest/more/extended_document.rb index 28f5359..aa1977b 100644 --- a/lib/couchrest/more/extended_document.rb +++ b/lib/couchrest/more/extended_document.rb @@ -14,7 +14,7 @@ module CouchRest include CouchRest::Mixins::ExtendedAttachments include CouchRest::Mixins::ClassProxy include CouchRest::Mixins::Collection - include CouchRest::Mixins::AttributeProtection + include CouchRest::Mixins::AttributeProtection def self.subclasses @subclasses ||= [] From de0878c84ba9eb010ff29f3dab09c85a7e43c279 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Thu, 11 Mar 2010 10:49:54 +0000 Subject: [PATCH 10/11] Adding fix for ruby1.9.1 when Array casted property is not provided an array --- lib/couchrest/mixins/properties.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/couchrest/mixins/properties.rb b/lib/couchrest/mixins/properties.rb index 368b3c0..bb2264e 100644 --- a/lib/couchrest/mixins/properties.rb +++ b/lib/couchrest/mixins/properties.rb @@ -51,7 +51,8 @@ module CouchRest return unless self[key] if property.type.is_a?(Array) klass = ::CouchRest.constantize(property.type[0]) - arr = self[key].dup.collect do |value| + self[key] = [self[key]] unless self[key].is_a?(Array) + arr = self[key].collect do |value| value = typecast_value(value, klass, property.init_method) associate_casted_to_parent(value, assigned) value From e922b81ccc846c83ae9287fd6b511cb522afc0d4 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Sat, 13 Mar 2010 15:03:38 +0000 Subject: [PATCH 11/11] Using database method call instead of using variable --- lib/couchrest/mixins/collection.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/couchrest/mixins/collection.rb b/lib/couchrest/mixins/collection.rb index 696dae7..e4ebc70 100644 --- a/lib/couchrest/mixins/collection.rb +++ b/lib/couchrest/mixins/collection.rb @@ -20,7 +20,7 @@ module CouchRest class_eval <<-END, __FILE__, __LINE__ + 1 def self.find_all_#{collection_name}(options = {}) view_options = #{view_options.inspect} || {} - CollectionProxy.new(@database, "#{design_doc}", "#{view_name}", view_options.merge(options), Kernel.const_get('#{self}')) + CollectionProxy.new(database, "#{design_doc}", "#{view_name}", view_options.merge(options), Kernel.const_get('#{self}')) end END end @@ -58,7 +58,7 @@ module CouchRest def create_collection_proxy(options) design_doc, view_name, view_options = parse_view_options(options) - CollectionProxy.new(@database, design_doc, view_name, view_options, self) + CollectionProxy.new(database, design_doc, view_name, view_options, self) end def parse_view_options(options)