Skip to content

Commit e9d314b

Browse files
committed
Proof of concept of evolve raw value and "strict" field option
1 parent 94fdc7d commit e9d314b

25 files changed

+219
-58
lines changed

docs/release-notes/mongoid-8.1.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,21 @@ from the ``_translations`` hash:
116116

117117
See the section on :ref:`Localize :present Field Option <present-fields>` for
118118
more details on how these are used.
119+
120+
121+
Support for Passing Raw Values into Queries
122+
-------------------------------------------
123+
124+
When performing queries, it is now possible skip Mongoid's type coercion logic
125+
using the ``Mongoid::RawValue`` wrapper class. This can be useful when legacy
126+
data in the database is of a different type than the field definition.
127+
128+
.. code-block:: ruby
129+
130+
class Person
131+
include Mongoid::Document
132+
field :age, type: Integer
133+
end
134+
135+
# Query for the string "42", not the integer 42
136+
Person.where(age: Mongoid::RawValue("42"))

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
@@ -70,6 +70,14 @@ module Config
7070
# existing method.
7171
option :scope_overwrite_exception, default: false
7272

73+
# Indicates whether or not to raise an error when attempting
74+
# to assign an incompatible type to a field.
75+
option :strict_type_assignment, default: false
76+
77+
# Indicates whether uncastable values from the database should
78+
# be returned wrapped by Mongoid::RawValue class.
79+
option :wrap_uncastable_values_from_database, default: false
80+
7381
# Use ActiveSupport's time zone in time operations instead of the
7482
# Ruby default time zone.
7583
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: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -150,16 +150,20 @@ def evolve_multi(specs)
150150
#
151151
# @return [ Object ] The serialized object.
152152
def evolve(serializer, value)
153-
case value
154-
when Hash
155-
evolve_hash(serializer, value)
156-
when Array
157-
evolve_array(serializer, value)
158-
when Range
159-
value.__evolve_range__(serializer: serializer)
160-
else
161-
(serializer || value.class).evolve(value)
162-
end
153+
_value = case value
154+
when Hash
155+
evolve_hash(serializer, value)
156+
when Array
157+
evolve_array(serializer, value)
158+
when Range
159+
value.__evolve_range__(serializer: serializer)
160+
when Mongoid::RawValue
161+
value
162+
else
163+
(serializer || value.class).evolve(value)
164+
end
165+
_value = _value.raw_value if _value.is_a?(Mongoid::RawValue)
166+
_value
163167
end
164168

165169
# Evolve a single key selection with array values.

lib/mongoid/extensions.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def transform_keys
4848
require "mongoid/extensions/object"
4949
require "mongoid/extensions/object_id"
5050
require "mongoid/extensions/range"
51+
require "mongoid/raw_value"
5152
require "mongoid/extensions/regexp"
5253
require "mongoid/extensions/set"
5354
require "mongoid/extensions/string"

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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,14 @@ def mongoize(object)
7272
BSON::Decimal128.new(object)
7373
elsif object.numeric?
7474
BSON::Decimal128.new(object.to_s)
75+
else
76+
Mongoid::RawValue(object, 'BigDecimal')
7577
end
7678
else
7779
if object.is_a?(BSON::Decimal128) || object.numeric?
7880
object.to_s
81+
else
82+
Mongoid::RawValue(object, 'BigDecimal')
7983
end
8084
end
8185
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: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,8 @@ module ClassMethods
3737
# @return [ Float | nil ] The object mongoized or nil.
3838
def mongoize(object)
3939
return if object.blank?
40-
if object.numeric?
41-
object.to_f
42-
end
40+
return object.to_f if object.numeric?
41+
Mongoid::RawValue(object, 'Date')
4342
end
4443
alias :demongoize :mongoize
4544
end

lib/mongoid/extensions/hash.rb

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -220,12 +220,11 @@ module ClassMethods
220220
# @return [ Hash | nil ] The object mongoized or nil.
221221
def mongoize(object)
222222
return if object.nil?
223-
if object.is_a?(Hash)
224-
# Need to use transform_values! which maintains the BSON::Document
225-
# instead of transform_values which always returns a hash. To do this,
226-
# we first need to dup the hash.
227-
object.dup.transform_values!(&:mongoize)
228-
end
223+
# Need to use transform_values! which maintains the BSON::Document
224+
# instead of transform_values which always returns a hash. To do this,
225+
# we first need to dup the hash.
226+
return object.dup.transform_values!(&:mongoize) if object.is_a?(Hash)
227+
Mongoid::RawValue(object, 'Hash')
229228
end
230229

231230
# Can the size of this object change?

lib/mongoid/extensions/integer.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,8 @@ module ClassMethods
4545
# @return [ Integer | nil ] The object mongoized or nil.
4646
def mongoize(object)
4747
return if object.blank?
48-
if object.numeric?
49-
object.to_i
50-
end
48+
return object.to_i if object.numeric?
49+
Mongoid::RawValue(object, 'Integer')
5150
end
5251
alias :demongoize :mongoize
5352
end

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/regexp.rb

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ 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+
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+
rescue RegexpError
2427
end
25-
rescue RegexpError
26-
nil
28+
Mongoid::RawValue(object, 'Regexp')
2729
end
2830
alias :demongoize :mongoize
2931
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
@@ -82,19 +82,18 @@ def mongoize(object)
8282
return if object.blank?
8383
begin
8484
time = object.__mongoize_time__
85-
rescue ArgumentError
86-
return
87-
end
88-
89-
if time.acts_like?(:time)
90-
if object.respond_to?(:sec_fraction)
91-
::Time.at(time.to_i, object.sec_fraction * 10**6).utc
92-
elsif time.respond_to?(:subsec)
93-
::Time.at(time.to_i, time.subsec * 10**6).utc
94-
else
95-
::Time.at(time.to_i, time.usec).utc
85+
if time.acts_like?(:time)
86+
if object.respond_to?(:sec_fraction)
87+
return ::Time.at(time.to_i, object.sec_fraction * 10**6).utc
88+
elsif time.respond_to?(:subsec)
89+
return ::Time.at(time.to_i, time.subsec * 10**6).utc
90+
else
91+
return ::Time.at(time.to_i, time.usec).utc
92+
end
9693
end
94+
rescue ArgumentError
9795
end
96+
Mongoid::RawValue.new(object, 'Time')
9897
end
9998
end
10099
end

lib/mongoid/fields/localized.rb

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

5454
private

lib/mongoid/fields/standard.rb

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,32 @@ class Standard
99
# Set readers for the instance variables.
1010
attr_accessor :default_val, :label, :name, :options
1111

12-
def_delegators :type, :demongoize, :evolve, :mongoize
12+
# If type.mongoize returns Mongoid::RawValue,
13+
# handle according to field or global strict setting
14+
def mongoize(object)
15+
value = type.mongoize(object)
16+
if value.is_a?(Mongoid::RawValue)
17+
case strict
18+
when :error then value.raise_error!
19+
when :warn then value.warn and return nil
20+
when :suppress then return nil
21+
# when :defer, assign the Mongoid::RawValue and fail when trying to persist.
22+
end
23+
end
24+
value
25+
end
26+
27+
# If type.demongoize returns Mongoid::RawValue,
28+
# return the inner value according to Mongoid.wrap_uncastable_values_from_database
29+
def demongoize(object)
30+
value = type.demongoize(object)
31+
if value.is_a?(Mongoid::RawValue) && !Mongoid.wrap_uncastable_values_from_database
32+
return value.raw_value
33+
end
34+
value
35+
end
36+
37+
def_delegators :type, :evolve
1338

1439
# Adds the atomic changes for this type of resizable field.
1540
#
@@ -107,6 +132,29 @@ def localize_present?
107132
false
108133
end
109134

135+
# Whether or not the field raises an error if a non-castable
136+
# type is assignment.
137+
#
138+
# @example Get the type.
139+
# field.type
140+
#
141+
# @return [ :error | :warn | :suppress ] The value. True means raise
142+
# an error. False means handle as nil.
143+
def strict
144+
return @strict if defined?(@strict)
145+
if options.key?(:strict)
146+
@strict = case options[:strict]
147+
when true, :error then :error
148+
when false, :suppress then :suppress
149+
when :warn then :warn
150+
end
151+
end
152+
# TODO: add default. Array/Hash should be strict.
153+
# Also consider global
154+
# Don't memoize default?
155+
# Support warn option
156+
end
157+
110158
# Get the metadata for the field if its a foreign key.
111159
#
112160
# @example Get the metadata.

lib/mongoid/fields/validators/macro.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ module Macro
1717
:fallbacks,
1818
:association,
1919
:pre_processed,
20+
:strict,
2021
:subtype,
2122
:type,
2223
:overwrite

0 commit comments

Comments
 (0)