Skip to content

Commit

Permalink
WIP on Mongoid raw value for mongoize/demongoize
Browse files Browse the repository at this point in the history
  • Loading branch information
johnnyshields committed Feb 15, 2023
1 parent 67c211d commit be716a0
Show file tree
Hide file tree
Showing 23 changed files with 217 additions and 59 deletions.
27 changes: 27 additions & 0 deletions lib/mongoid/attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ def write_attribute(name, value)

if attribute_writable?(field_name)
_assigning do
# TODO: remove this
# validate_attribute_value(field_name, value)
localized = fields[field_name].try(:localized?)
attributes_before_type_cast[name.to_s] = value
typed_value = typed_value_for(field_name, value)
Expand Down Expand Up @@ -358,6 +360,31 @@ def unalias_attribute(name)

private

# Validates an attribute value as being assignable to the specified field.
#
# For now, only Hash and Array fields are validated, and the value is
# being checked to be of an appropriate type (i.e. either Hash or Array,
# respectively, or nil).
#
# This method takes the name of the field as stored in the document
# in the database, not (necessarily) the Ruby method name used to read/write
# the said field.
#
# @param [ String, Symbol ] field_name The name of the field.
# @param [ Object ] value The value to be validated.
# TODO: remove this
# def validate_attribute_value(field_name, value)
# return if value.nil?
# field = fields[field_name]
# return unless field
# validatable_types = [ Hash, Array ]
# if validatable_types.include?(field.type)
# unless value.is_a?(field.type)
# raise Mongoid::Errors::InvalidValue.new(field.type, value.class)
# end
# end
# end

def lookup_attribute_presence(name, value)
if localized_fields.has_key?(name) && value
value = localized_fields[name].send(:lookup, value)
Expand Down
8 changes: 8 additions & 0 deletions lib/mongoid/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ module Config
# existing method.
option :scope_overwrite_exception, default: false

# Indicates whether or not to raise an error when attempting
# to assign an incompatible type to a field.
option :strict_type_assignment, default: false

# Indicates whether uncastable values from the database should
# be returned wrapped by Mongoid::RawValue class.
option :wrap_uncastable_values_from_database, default: false

# Use ActiveSupport's time zone in time operations instead of the
# Ruby default time zone.
option :use_activesupport_time_zone, default: true
Expand Down
2 changes: 1 addition & 1 deletion lib/mongoid/criteria/queryable/extensions/array.rb
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def evolve(object)
when ::Array, ::Set
object.map { |obj| obj.class.evolve(obj) }
else
object
Mongoid::RawValue(object, 'Array')
end
end
end
Expand Down
28 changes: 17 additions & 11 deletions lib/mongoid/criteria/queryable/selector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,18 +150,24 @@ def evolve_multi(specs)
#
# @return [ Object ] The serialized object.
def evolve(serializer, value)
case value
when Mongoid::RawValue
value.raw_value
when Hash
evolve_hash(serializer, value)
when Array
evolve_array(serializer, value)
when Range
value.__evolve_range__(serializer: serializer)
else
(serializer || value.class).evolve(value)
_value = case value
when Mongoid::RawValue
value.raw_value
when Hash
evolve_hash(serializer, value)
when Array
evolve_array(serializer, value)
when Range
value.__evolve_range__(serializer: serializer)
else
(serializer || value.class).evolve(value)
end

while _value.is_a?(Mongoid::RawValue) do
_value = _value.raw_value
end

_value
end

# Evolve a single key selection with array values.
Expand Down
2 changes: 2 additions & 0 deletions lib/mongoid/extensions/array.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ def mongoize(object)
case object
when ::Array, ::Set
object.map(&:mongoize)
else
Mongoid::RawValue(object, 'Array')
end
end

Expand Down
12 changes: 8 additions & 4 deletions lib/mongoid/extensions/big_decimal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,18 @@ def mongoize(object)
BSON::Decimal128.new(object)
elsif object.numeric?
BSON::Decimal128.new(object.to_s)
elsif !object.is_a?(String)
object.try(:to_d)
elsif !object.is_a?(String) && object.respond_to?(:to_d)
object.to_d
else
Mongoid::RawValue(object, 'BigDecimal')
end
else
if object.is_a?(BSON::Decimal128) || object.numeric?
object.to_s
elsif !object.is_a?(String)
object.try(:to_d)&.to_s
elsif !object.is_a?(String) && object.respond_to?(:to_d)
object.to_d
else
Mongoid::RawValue(object, 'BigDecimal')
end
end
end
Expand Down
1 change: 1 addition & 0 deletions lib/mongoid/extensions/binary.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def mongoize(object)
case object
when BSON::Binary then object
when String, Symbol then BSON::Binary.new(object.to_s)
else Mongoid::RawValue(object, 'BSON::Binary')
end
end
alias :demongoize :mongoize
Expand Down
2 changes: 2 additions & 0 deletions lib/mongoid/extensions/boolean.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ def mongoize(object)
true
elsif object.to_s =~ (/\A(false|f|no|n|off|0|0.0)\z/i)
false
else
Mongoid::RawValue(object, 'Boolean')
end
end
alias :demongoize :mongoize
Expand Down
9 changes: 5 additions & 4 deletions lib/mongoid/extensions/date.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,13 @@ def mongoize(object)
else
time = object.__mongoize_time__
end

if time.acts_like?(:time)
return ::Time.utc(time.year, time.month, time.day)
end
rescue ArgumentError
nil
end
if time.acts_like?(:time)
::Time.utc(time.year, time.month, time.day)
end
Mongoid::RawValue(object, 'Date')
end
end
end
Expand Down
4 changes: 3 additions & 1 deletion lib/mongoid/extensions/float.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ def mongoize(object)
if object.numeric?
object.to_f
end
elsif object.respond_to?(:to_f)
object.to_f
else
object.try(:to_f)
Mongoid::RawValue(object, 'Float')
end
end
alias :demongoize :mongoize
Expand Down
2 changes: 2 additions & 0 deletions lib/mongoid/extensions/hash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ def mongoize(object)
object.dup.transform_values!(&:mongoize)
when Hash
BSON::Document.new(object.transform_values(&:mongoize))
else
Mongoid::RawValue(object, 'Hash')
end
end

Expand Down
4 changes: 3 additions & 1 deletion lib/mongoid/extensions/integer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ def mongoize(object)
if object.numeric?
object.to_i
end
elsif object.respond_to?(:to_i)
object.to_i
else
object.try(:to_i)
Mongoid::RawValue(object, 'Integer')
end
end
alias :demongoize :mongoize
Expand Down
1 change: 1 addition & 0 deletions lib/mongoid/extensions/range.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def mongoize(object)
case object
when Hash then __mongoize_hash__(object)
when Range then __mongoize_range__(object)
else Mongoid::RawValue(object, 'Range')
end
end

Expand Down
60 changes: 54 additions & 6 deletions lib/mongoid/extensions/raw_value.rb
Original file line number Diff line number Diff line change
@@ -1,24 +1,42 @@
# frozen_string_literal: true

# Wrapper class used when a value cannot be casted in evolve method.
# Wrapper class used when a value cannot be casted by the
# mongoize, demongoize, and evolve methods.
module Mongoid

# Instantiates a new Mongoid::RawValue object. Used as a syntax shortcut.
# Instantiates a new Mongoid::RawValue object. Used as a
# syntax shortcut.
#
# @example Create a Mongoid::RawValue object.
# Mongoid::RawValue("Beagle")
#
# @param [ Object ] raw_value The underlying raw object.
# @param [ String ] cast_class_name The name of the class
# to which the raw value is intended to be cast.
#
# @return [ Mongoid::RawValue ] The object.
def RawValue(*args)
RawValue.new(*args)
def RawValue(raw_value, cast_class_name = nil)
RawValue.new(raw_value, cast_class_name)
end

class RawValue

attr_reader :raw_value
attr_reader :raw_value,
:cast_class_name

def initialize(raw_value)
# Instantiates a new Mongoid::RawValue object.
#
# @example Create a Mongoid::RawValue object.
# Mongoid::RawValue.new("Beagle", "String")
#
# @param [ Object ] raw_value The underlying raw object.
# @param [ String ] cast_class_name The name of the class
# to which the raw value is intended to be cast.
#
# @return [ Mongoid::RawValue ] The object.
def initialize(raw_value, cast_class_name = nil)
@raw_value = raw_value
@cast_class_name = cast_class_name
end

# Returns a string containing a human-readable representation of
Expand All @@ -28,5 +46,35 @@ def initialize(raw_value)
def inspect
"RawValue: #{raw_value.inspect}"
end

# Raises a Mongoid::Errors::InvalidValue error.
def raise_error!
raise Mongoid::Errors::InvalidValue.new(raw_value.class.name, cast_class_name)
end

# Logs a warning that a value cannot be cast.
def warn
Mongoid.logger.warn("Cannot cast #{raw_value.class.name} to #{cast_class_name}; returning nil")
end

# Delegate all missing methods to the raw value.
#
# @param [ String, Symbol ] method_name The name of the method.
# @param [ Array ] args The arguments passed to the method.
#
# @return [ Object ] The method response.
ruby2_keywords def method_missing(method_name, *args, &block)
raw_value.send(method_name, *args, &block)
end

# Delegate all missing methods to the raw value.
#
# @param [ String, Symbol ] method_name The name of the method.
# @param [ true | false ] include_private Whether to check private methods.
#
# @return [ true | false ] Whether the raw value object responds to the method.
def respond_to_missing?(method_name, include_private = false)
raw_value.respond_to?(method_name, include_private)
end
end
end
15 changes: 9 additions & 6 deletions lib/mongoid/extensions/regexp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@ module ClassMethods
# @return [ Regexp | nil ] The object mongoized or nil.
def mongoize(object)
return if object.nil?
case object
when String then ::Regexp.new(object)
when ::Regexp then object
when BSON::Regexp::Raw then object.compile
begin
_object = case object
when String then ::Regexp.new(object)
when ::Regexp then object
when BSON::Regexp::Raw then object.compile
end
return _object if _object
rescue RegexpError
end
rescue RegexpError
nil
Mongoid::RawValue(object, 'Regexp')
end
alias :demongoize :mongoize
end
Expand Down
1 change: 1 addition & 0 deletions lib/mongoid/extensions/set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def mongoize(object)
case object
when ::Set then ::Array.mongoize(object.to_a).uniq
when ::Array then ::Array.mongoize(object).uniq
else Mongoid::RawValue(object, 'Set')
end
end
end
Expand Down
4 changes: 3 additions & 1 deletion lib/mongoid/extensions/string.rb
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,9 @@ module ClassMethods
#
# @return [ String ] The object mongoized.
def mongoize(object)
object.try(:to_s)
return if object.nil?
return object.to_s if object.respond_to?(:to_s)
Mongoid::RawValue.new(object, 'String')
end
alias :demongoize :mongoize
end
Expand Down
4 changes: 3 additions & 1 deletion lib/mongoid/extensions/symbol.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ module ClassMethods
#
# @return [ Symbol | nil ] The object mongoized or nil.
def mongoize(object)
object.try(:to_sym)
return if object.nil?
return object.to_sym if object.respond_to?(:to_sym)
Mongoid::RawValue.new(object, 'Symbol')
end
alias :demongoize :mongoize
end
Expand Down
21 changes: 10 additions & 11 deletions lib/mongoid/extensions/time.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,18 @@ def mongoize(object)
return if object.blank?
begin
time = object.__mongoize_time__
rescue ArgumentError
return
end

if time.acts_like?(:time)
if object.respond_to?(:sec_fraction)
::Time.at(time.to_i, object.sec_fraction * 10**6).utc
elsif time.respond_to?(:subsec)
::Time.at(time.to_i, time.subsec * 10**6).utc
else
::Time.at(time.to_i, time.usec).utc
if time.acts_like?(:time)
if object.respond_to?(:sec_fraction)
return ::Time.at(time.to_i, object.sec_fraction * 10**6).utc
elsif time.respond_to?(:subsec)
return ::Time.at(time.to_i, time.subsec * 10**6).utc
else
return ::Time.at(time.to_i, time.usec).utc
end
end
rescue ArgumentError
end
Mongoid::RawValue.new(object, 'Time')
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/mongoid/fields/localized.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def localize_present?
#
# @return [ Hash ] The locale with string translation.
def mongoize(object)
{ ::I18n.locale.to_s => type.mongoize(object) }
{ ::I18n.locale.to_s => super(object) }
end

private
Expand Down
Loading

0 comments on commit be716a0

Please sign in to comment.