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