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) diff --git a/lib/couchrest/mixins/properties.rb b/lib/couchrest/mixins/properties.rb index 9a9c3a4..bb2264e 100644 --- a/lib/couchrest/mixins/properties.rb +++ b/lib/couchrest/mixins/properties.rb @@ -1,24 +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 @@ -26,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) @@ -67,28 +51,24 @@ 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| - unless value.instance_of?(klass) - value = convert_property_value(property, klass, value) - end + 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 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 == 'boolean' + if property.type.downcase == 'boolean' klass = TrueClass else klass = ::CouchRest.constantize(property.type) end - unless self[key].instance_of?(klass) - self[key] = convert_property_value(property, klass, self[property.name]) - end - associate_casted_to_parent(self[property.name], assigned) + self[key] = typecast_value(self[key], klass, property.init_method) + associate_casted_to_parent(self[key], assigned) end - end def associate_casted_to_parent(casted, assigned) @@ -96,21 +76,6 @@ module CouchRest 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} @@ -118,14 +83,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={}) @@ -141,7 +99,7 @@ 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_setter(property) unless property.read_only == true diff --git a/lib/couchrest/more/extended_document.rb b/lib/couchrest/more/extended_document.rb index c6f3c85..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 ||= [] @@ -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 @@ -84,9 +85,9 @@ module CouchRest # on the document whenever saving occurs. CouchRest uses a pretty # decent time format by default. See Time#to_json def self.timestamps! - class_eval <<-EOS, __FILE__, __LINE__ + 1 - property(:updated_at, :read_only => true, :cast_as => 'Time', :auto_validation => false) - property(:created_at, :read_only => true, :cast_as => 'Time', :auto_validation => false) + class_eval <<-EOS, __FILE__, __LINE__ + property(:updated_at, :read_only => true, :type => 'Time', :auto_validation => false) + property(:created_at, :read_only => true, :type => 'Time', :auto_validation => false) set_callback :save, :before do |object| object['updated_at'] = Time.now diff --git a/lib/couchrest/more/property.rb b/lib/couchrest/more/property.rb index efede74..88ce725 100644 --- a/lib/couchrest/more/property.rb +++ b/lib/couchrest/more/property.rb @@ -1,9 +1,9 @@ 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 +11,19 @@ module CouchRest parse_options(options) self 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,7 +34,7 @@ module CouchRest @init_method = options[:init_method] ? options.delete(:init_method) : 'new' @options = options end - + end end @@ -56,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/lib/couchrest/validation/validators/required_field_validator.rb b/lib/couchrest/validation/validators/required_field_validator.rb index d8edd81..0c64ccf 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 diff --git a/spec/couchrest/more/casted_model_spec.rb b/spec/couchrest/more/casted_model_spec.rb index a2a8a96..704b85c 100644 --- a/spec/couchrest/more/casted_model_spec.rb +++ b/spec/couchrest/more/casted_model_spec.rb @@ -12,7 +12,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 f055576..f4ac00c 100644 --- a/spec/couchrest/more/extended_doc_spec.rb +++ b/spec/couchrest/more/extended_doc_spec.rb @@ -9,11 +9,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 @@ -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 @@ -383,7 +395,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'] @@ -394,8 +406,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 @@ -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 diff --git a/spec/couchrest/more/property_spec.rb b/spec/couchrest/more/property_spec.rb index 0e25916..71adfcd 100644 --- a/spec/couchrest/more/property_spec.rb +++ b/spec/couchrest/more/property_spec.rb @@ -7,6 +7,7 @@ require File.join(FIXTURE_PATH, 'more', 'service') require File.join(FIXTURE_PATH, 'more', 'event') require File.join(FIXTURE_PATH, 'more', 'cat') require File.join(FIXTURE_PATH, 'more', 'user') +require File.join(FIXTURE_PATH, 'more', 'course') describe "ExtendedDocument properties" do @@ -42,11 +43,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 @@ -155,111 +156,449 @@ describe "ExtendedDocument properties" do end end end - + describe "casting" do - describe "cast keys to any type" do - before(:all) do - event_doc = { :subject => "Some event", :occurs_at => Time.now, :end_date => Date.today } - 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 occurs_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 - 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 - 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 + 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 + @course.ends_at = '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 "casting to a boolean value" do - class RootBeerFloat < CouchRest::ExtendedDocument - use_database DB - property :tasty, :cast_as => :boolean + 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 - it "should add an accessor with a '?' for boolean attributes that returns true or false" do - RootBeerFloat.new(:tasty => true).tasty?.should == true - RootBeerFloat.new(:tasty => 'you bet').tasty?.should == true - RootBeerFloat.new(:tasty => 123).tasty?.should == true - - RootBeerFloat.new(:tasty => false).tasty?.should == false - RootBeerFloat.new(:tasty => 'false').tasty?.should == false - RootBeerFloat.new(:tasty => 'FaLsE').tasty?.should == false - RootBeerFloat.new(:tasty => nil).tasty?.should == false + [ 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 - it "should return the real value when the default accessor is used" do - RootBeerFloat.new(:tasty => true).tasty.should == true - RootBeerFloat.new(:tasty => 'you bet').tasty.should == 'you bet' - RootBeerFloat.new(:tasty => 123).tasty.should == 123 - RootBeerFloat.new(:tasty => 'false').tasty.should == 'false' - RootBeerFloat.new(:tasty => false).tasty.should == false - RootBeerFloat.new(:tasty => nil).tasty.should == nil + [ '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 - -describe "a newly created casted model" do - before(:each) do - reset_test_db! - @cat = Cat.new(:name => 'Toonces') - @squeaky_mouse = CatToy.new(:name => 'Squeaky') - end - - describe "assigned assigned to a casted property" do - it "should have casted_by set to its parent" do - @squeaky_mouse.casted_by.should be_nil - @cat.favorite_toy = @squeaky_mouse - @squeaky_mouse.casted_by.should === @cat - end - end - - describe "appended to a casted collection" do - it "should have casted_by set to its parent" do - @squeaky_mouse.casted_by.should be_nil - @cat.toys << @squeaky_mouse - @squeaky_mouse.casted_by.should === @cat - @cat.save - @cat.toys.first.casted_by.should === @cat - end - end - - describe "list assigned to a casted collection" do - it "should have casted_by set on all elements" do - toy1 = CatToy.new(:name => 'Feather') - toy2 = CatToy.new(:name => 'Mouse') - @cat.toys = [toy1, toy2] - toy1.casted_by.should === @cat - toy2.casted_by.should === @cat - @cat.save - @cat = Cat.get(@cat.id) - @cat.toys[0].casted_by.should === @cat - @cat.toys[1].casted_by.should === @cat - end end end @@ -286,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/article.rb b/spec/fixtures/more/article.rb index dbc9e8c..0185d3b 100644 --- a/spec/fixtures/more/article.rb +++ b/spec/fixtures/more/article.rb @@ -20,10 +20,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..f340469 100644 --- a/spec/fixtures/more/course.rb +++ b/spec/fixtures/more/course.rb @@ -4,11 +4,19 @@ require File.join(FIXTURE_PATH, 'more', 'person') 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 :title, :cast_as => 'String' + 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 -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 diff --git a/spec/fixtures/more/person.rb b/spec/fixtures/more/person.rb index de9e72c..b200cb4 100644 --- a/spec/fixtures/more/person.rb +++ b/spec/fixtures/more/person.rb @@ -1,7 +1,7 @@ class Person < Hash include ::CouchRest::CastedModel - property :name property :pet, :cast_as => 'Cat' + 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