Skip to content

Commit be716a0

Browse files
committed
WIP on Mongoid raw value for mongoize/demongoize
1 parent 67c211d commit be716a0

23 files changed

+217
-59
lines changed

lib/mongoid/attributes.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ def write_attribute(name, value)
170170

171171
if attribute_writable?(field_name)
172172
_assigning do
173+
# TODO: remove this
174+
# validate_attribute_value(field_name, value)
173175
localized = fields[field_name].try(:localized?)
174176
attributes_before_type_cast[name.to_s] = value
175177
typed_value = typed_value_for(field_name, value)
@@ -358,6 +360,31 @@ def unalias_attribute(name)
358360

359361
private
360362

363+
# Validates an attribute value as being assignable to the specified field.
364+
#
365+
# For now, only Hash and Array fields are validated, and the value is
366+
# being checked to be of an appropriate type (i.e. either Hash or Array,
367+
# respectively, or nil).
368+
#
369+
# This method takes the name of the field as stored in the document
370+
# in the database, not (necessarily) the Ruby method name used to read/write
371+
# the said field.
372+
#
373+
# @param [ String, Symbol ] field_name The name of the field.
374+
# @param [ Object ] value The value to be validated.
375+
# TODO: remove this
376+
# def validate_attribute_value(field_name, value)
377+
# return if value.nil?
378+
# field = fields[field_name]
379+
# return unless field
380+
# validatable_types = [ Hash, Array ]
381+
# if validatable_types.include?(field.type)
382+
# unless value.is_a?(field.type)
383+
# raise Mongoid::Errors::InvalidValue.new(field.type, value.class)
384+
# end
385+
# end
386+
# end
387+
361388
def lookup_attribute_presence(name, value)
362389
if localized_fields.has_key?(name) && value
363390
value = localized_fields[name].send(:lookup, value)

lib/mongoid/config.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,14 @@ module Config
7171
# existing method.
7272
option :scope_overwrite_exception, default: false
7373

74+
# Indicates whether or not to raise an error when attempting
75+
# to assign an incompatible type to a field.
76+
option :strict_type_assignment, default: false
77+
78+
# Indicates whether uncastable values from the database should
79+
# be returned wrapped by Mongoid::RawValue class.
80+
option :wrap_uncastable_values_from_database, default: false
81+
7482
# Use ActiveSupport's time zone in time operations instead of the
7583
# Ruby default time zone.
7684
option :use_activesupport_time_zone, default: true

lib/mongoid/criteria/queryable/extensions/array.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ def evolve(object)
137137
when ::Array, ::Set
138138
object.map { |obj| obj.class.evolve(obj) }
139139
else
140-
object
140+
Mongoid::RawValue(object, 'Array')
141141
end
142142
end
143143
end

lib/mongoid/criteria/queryable/selector.rb

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -150,18 +150,24 @@ def evolve_multi(specs)
150150
#
151151
# @return [ Object ] The serialized object.
152152
def evolve(serializer, value)
153-
case value
154-
when Mongoid::RawValue
155-
value.raw_value
156-
when Hash
157-
evolve_hash(serializer, value)
158-
when Array
159-
evolve_array(serializer, value)
160-
when Range
161-
value.__evolve_range__(serializer: serializer)
162-
else
163-
(serializer || value.class).evolve(value)
153+
_value = case value
154+
when Mongoid::RawValue
155+
value.raw_value
156+
when Hash
157+
evolve_hash(serializer, value)
158+
when Array
159+
evolve_array(serializer, value)
160+
when Range
161+
value.__evolve_range__(serializer: serializer)
162+
else
163+
(serializer || value.class).evolve(value)
164+
end
165+
166+
while _value.is_a?(Mongoid::RawValue) do
167+
_value = _value.raw_value
164168
end
169+
170+
_value
165171
end
166172

167173
# Evolve a single key selection with array values.

lib/mongoid/extensions/array.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ def mongoize(object)
150150
case object
151151
when ::Array, ::Set
152152
object.map(&:mongoize)
153+
else
154+
Mongoid::RawValue(object, 'Array')
153155
end
154156
end
155157

lib/mongoid/extensions/big_decimal.rb

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,18 @@ def mongoize(object)
7070
BSON::Decimal128.new(object)
7171
elsif object.numeric?
7272
BSON::Decimal128.new(object.to_s)
73-
elsif !object.is_a?(String)
74-
object.try(:to_d)
73+
elsif !object.is_a?(String) && object.respond_to?(:to_d)
74+
object.to_d
75+
else
76+
Mongoid::RawValue(object, 'BigDecimal')
7577
end
7678
else
7779
if object.is_a?(BSON::Decimal128) || object.numeric?
7880
object.to_s
79-
elsif !object.is_a?(String)
80-
object.try(:to_d)&.to_s
81+
elsif !object.is_a?(String) && object.respond_to?(:to_d)
82+
object.to_d
83+
else
84+
Mongoid::RawValue(object, 'BigDecimal')
8185
end
8286
end
8387
end

lib/mongoid/extensions/binary.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def mongoize(object)
3030
case object
3131
when BSON::Binary then object
3232
when String, Symbol then BSON::Binary.new(object.to_s)
33+
else Mongoid::RawValue(object, 'BSON::Binary')
3334
end
3435
end
3536
alias :demongoize :mongoize

lib/mongoid/extensions/boolean.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ def mongoize(object)
1818
true
1919
elsif object.to_s =~ (/\A(false|f|no|n|off|0|0.0)\z/i)
2020
false
21+
else
22+
Mongoid::RawValue(object, 'Boolean')
2123
end
2224
end
2325
alias :demongoize :mongoize

lib/mongoid/extensions/date.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,13 @@ def mongoize(object)
7171
else
7272
time = object.__mongoize_time__
7373
end
74+
75+
if time.acts_like?(:time)
76+
return ::Time.utc(time.year, time.month, time.day)
77+
end
7478
rescue ArgumentError
75-
nil
76-
end
77-
if time.acts_like?(:time)
78-
::Time.utc(time.year, time.month, time.day)
7979
end
80+
Mongoid::RawValue(object, 'Date')
8081
end
8182
end
8283
end

lib/mongoid/extensions/float.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@ def mongoize(object)
4141
if object.numeric?
4242
object.to_f
4343
end
44+
elsif object.respond_to?(:to_f)
45+
object.to_f
4446
else
45-
object.try(:to_f)
47+
Mongoid::RawValue(object, 'Float')
4648
end
4749
end
4850
alias :demongoize :mongoize

lib/mongoid/extensions/hash.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,8 @@ def mongoize(object)
225225
object.dup.transform_values!(&:mongoize)
226226
when Hash
227227
BSON::Document.new(object.transform_values(&:mongoize))
228+
else
229+
Mongoid::RawValue(object, 'Hash')
228230
end
229231
end
230232

lib/mongoid/extensions/integer.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,10 @@ def mongoize(object)
4949
if object.numeric?
5050
object.to_i
5151
end
52+
elsif object.respond_to?(:to_i)
53+
object.to_i
5254
else
53-
object.try(:to_i)
55+
Mongoid::RawValue(object, 'Integer')
5456
end
5557
end
5658
alias :demongoize :mongoize

lib/mongoid/extensions/range.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def mongoize(object)
7777
case object
7878
when Hash then __mongoize_hash__(object)
7979
when Range then __mongoize_range__(object)
80+
else Mongoid::RawValue(object, 'Range')
8081
end
8182
end
8283

lib/mongoid/extensions/raw_value.rb

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,42 @@
11
# frozen_string_literal: true
22

3-
# Wrapper class used when a value cannot be casted in evolve method.
3+
# Wrapper class used when a value cannot be casted by the
4+
# mongoize, demongoize, and evolve methods.
45
module Mongoid
56

6-
# Instantiates a new Mongoid::RawValue object. Used as a syntax shortcut.
7+
# Instantiates a new Mongoid::RawValue object. Used as a
8+
# syntax shortcut.
79
#
810
# @example Create a Mongoid::RawValue object.
911
# Mongoid::RawValue("Beagle")
1012
#
13+
# @param [ Object ] raw_value The underlying raw object.
14+
# @param [ String ] cast_class_name The name of the class
15+
# to which the raw value is intended to be cast.
16+
#
1117
# @return [ Mongoid::RawValue ] The object.
12-
def RawValue(*args)
13-
RawValue.new(*args)
18+
def RawValue(raw_value, cast_class_name = nil)
19+
RawValue.new(raw_value, cast_class_name)
1420
end
1521

1622
class RawValue
1723

18-
attr_reader :raw_value
24+
attr_reader :raw_value,
25+
:cast_class_name
1926

20-
def initialize(raw_value)
27+
# Instantiates a new Mongoid::RawValue object.
28+
#
29+
# @example Create a Mongoid::RawValue object.
30+
# Mongoid::RawValue.new("Beagle", "String")
31+
#
32+
# @param [ Object ] raw_value The underlying raw object.
33+
# @param [ String ] cast_class_name The name of the class
34+
# to which the raw value is intended to be cast.
35+
#
36+
# @return [ Mongoid::RawValue ] The object.
37+
def initialize(raw_value, cast_class_name = nil)
2138
@raw_value = raw_value
39+
@cast_class_name = cast_class_name
2240
end
2341

2442
# Returns a string containing a human-readable representation of
@@ -28,5 +46,35 @@ def initialize(raw_value)
2846
def inspect
2947
"RawValue: #{raw_value.inspect}"
3048
end
49+
50+
# Raises a Mongoid::Errors::InvalidValue error.
51+
def raise_error!
52+
raise Mongoid::Errors::InvalidValue.new(raw_value.class.name, cast_class_name)
53+
end
54+
55+
# Logs a warning that a value cannot be cast.
56+
def warn
57+
Mongoid.logger.warn("Cannot cast #{raw_value.class.name} to #{cast_class_name}; returning nil")
58+
end
59+
60+
# Delegate all missing methods to the raw value.
61+
#
62+
# @param [ String, Symbol ] method_name The name of the method.
63+
# @param [ Array ] args The arguments passed to the method.
64+
#
65+
# @return [ Object ] The method response.
66+
ruby2_keywords def method_missing(method_name, *args, &block)
67+
raw_value.send(method_name, *args, &block)
68+
end
69+
70+
# Delegate all missing methods to the raw value.
71+
#
72+
# @param [ String, Symbol ] method_name The name of the method.
73+
# @param [ true | false ] include_private Whether to check private methods.
74+
#
75+
# @return [ true | false ] Whether the raw value object responds to the method.
76+
def respond_to_missing?(method_name, include_private = false)
77+
raw_value.respond_to?(method_name, include_private)
78+
end
3179
end
3280
end

lib/mongoid/extensions/regexp.rb

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@ module ClassMethods
1717
# @return [ Regexp | nil ] The object mongoized or nil.
1818
def mongoize(object)
1919
return if object.nil?
20-
case object
21-
when String then ::Regexp.new(object)
22-
when ::Regexp then object
23-
when BSON::Regexp::Raw then object.compile
20+
begin
21+
_object = case object
22+
when String then ::Regexp.new(object)
23+
when ::Regexp then object
24+
when BSON::Regexp::Raw then object.compile
25+
end
26+
return _object if _object
27+
rescue RegexpError
2428
end
25-
rescue RegexpError
26-
nil
29+
Mongoid::RawValue(object, 'Regexp')
2730
end
2831
alias :demongoize :mongoize
2932
end

lib/mongoid/extensions/set.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def mongoize(object)
4646
case object
4747
when ::Set then ::Array.mongoize(object.to_a).uniq
4848
when ::Array then ::Array.mongoize(object).uniq
49+
else Mongoid::RawValue(object, 'Set')
4950
end
5051
end
5152
end

lib/mongoid/extensions/string.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,9 @@ module ClassMethods
159159
#
160160
# @return [ String ] The object mongoized.
161161
def mongoize(object)
162-
object.try(:to_s)
162+
return if object.nil?
163+
return object.to_s if object.respond_to?(:to_s)
164+
Mongoid::RawValue.new(object, 'String')
163165
end
164166
alias :demongoize :mongoize
165167
end

lib/mongoid/extensions/symbol.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ module ClassMethods
2626
#
2727
# @return [ Symbol | nil ] The object mongoized or nil.
2828
def mongoize(object)
29-
object.try(:to_sym)
29+
return if object.nil?
30+
return object.to_sym if object.respond_to?(:to_sym)
31+
Mongoid::RawValue.new(object, 'Symbol')
3032
end
3133
alias :demongoize :mongoize
3234
end

lib/mongoid/extensions/time.rb

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -84,19 +84,18 @@ def mongoize(object)
8484
return if object.blank?
8585
begin
8686
time = object.__mongoize_time__
87-
rescue ArgumentError
88-
return
89-
end
90-
91-
if time.acts_like?(:time)
92-
if object.respond_to?(:sec_fraction)
93-
::Time.at(time.to_i, object.sec_fraction * 10**6).utc
94-
elsif time.respond_to?(:subsec)
95-
::Time.at(time.to_i, time.subsec * 10**6).utc
96-
else
97-
::Time.at(time.to_i, time.usec).utc
87+
if time.acts_like?(:time)
88+
if object.respond_to?(:sec_fraction)
89+
return ::Time.at(time.to_i, object.sec_fraction * 10**6).utc
90+
elsif time.respond_to?(:subsec)
91+
return ::Time.at(time.to_i, time.subsec * 10**6).utc
92+
else
93+
return ::Time.at(time.to_i, time.usec).utc
94+
end
9895
end
96+
rescue ArgumentError
9997
end
98+
Mongoid::RawValue.new(object, 'Time')
10099
end
101100
end
102101
end

lib/mongoid/fields/localized.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def localize_present?
5050
#
5151
# @return [ Hash ] The locale with string translation.
5252
def mongoize(object)
53-
{ ::I18n.locale.to_s => type.mongoize(object) }
53+
{ ::I18n.locale.to_s => super(object) }
5454
end
5555

5656
private

0 commit comments

Comments
 (0)