From be716a04558afb245078e03baac6e1a5ffbaac92 Mon Sep 17 00:00:00 2001 From: johnnyshields Date: Thu, 16 Feb 2023 08:36:33 +0900 Subject: [PATCH] WIP on Mongoid raw value for mongoize/demongoize --- lib/mongoid/attributes.rb | 27 +++++++++ lib/mongoid/config.rb | 8 +++ .../criteria/queryable/extensions/array.rb | 2 +- lib/mongoid/criteria/queryable/selector.rb | 28 +++++---- lib/mongoid/extensions/array.rb | 2 + lib/mongoid/extensions/big_decimal.rb | 12 ++-- lib/mongoid/extensions/binary.rb | 1 + lib/mongoid/extensions/boolean.rb | 2 + lib/mongoid/extensions/date.rb | 9 +-- lib/mongoid/extensions/float.rb | 4 +- lib/mongoid/extensions/hash.rb | 2 + lib/mongoid/extensions/integer.rb | 4 +- lib/mongoid/extensions/range.rb | 1 + lib/mongoid/extensions/raw_value.rb | 60 +++++++++++++++++-- lib/mongoid/extensions/regexp.rb | 15 +++-- lib/mongoid/extensions/set.rb | 1 + lib/mongoid/extensions/string.rb | 4 +- lib/mongoid/extensions/symbol.rb | 4 +- lib/mongoid/extensions/time.rb | 21 ++++--- lib/mongoid/fields/localized.rb | 2 +- lib/mongoid/fields/standard.rb | 50 +++++++++++++++- lib/mongoid/fields/validators/macro.rb | 1 + lib/mongoid/stringified_symbol.rb | 16 ++--- 23 files changed, 217 insertions(+), 59 deletions(-) diff --git a/lib/mongoid/attributes.rb b/lib/mongoid/attributes.rb index 4484750958..e9dff09a35 100644 --- a/lib/mongoid/attributes.rb +++ b/lib/mongoid/attributes.rb @@ -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) @@ -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) diff --git a/lib/mongoid/config.rb b/lib/mongoid/config.rb index a236836bcb..188a9ec361 100644 --- a/lib/mongoid/config.rb +++ b/lib/mongoid/config.rb @@ -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 diff --git a/lib/mongoid/criteria/queryable/extensions/array.rb b/lib/mongoid/criteria/queryable/extensions/array.rb index 2012c34781..ba39cc4321 100644 --- a/lib/mongoid/criteria/queryable/extensions/array.rb +++ b/lib/mongoid/criteria/queryable/extensions/array.rb @@ -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 diff --git a/lib/mongoid/criteria/queryable/selector.rb b/lib/mongoid/criteria/queryable/selector.rb index 8d1e12dec4..af17152a26 100644 --- a/lib/mongoid/criteria/queryable/selector.rb +++ b/lib/mongoid/criteria/queryable/selector.rb @@ -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. diff --git a/lib/mongoid/extensions/array.rb b/lib/mongoid/extensions/array.rb index cae37730e4..02cd2469de 100644 --- a/lib/mongoid/extensions/array.rb +++ b/lib/mongoid/extensions/array.rb @@ -150,6 +150,8 @@ def mongoize(object) case object when ::Array, ::Set object.map(&:mongoize) + else + Mongoid::RawValue(object, 'Array') end end diff --git a/lib/mongoid/extensions/big_decimal.rb b/lib/mongoid/extensions/big_decimal.rb index 2013c26668..4390cfb255 100644 --- a/lib/mongoid/extensions/big_decimal.rb +++ b/lib/mongoid/extensions/big_decimal.rb @@ -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 diff --git a/lib/mongoid/extensions/binary.rb b/lib/mongoid/extensions/binary.rb index cfbc5318bc..459f0ff6be 100644 --- a/lib/mongoid/extensions/binary.rb +++ b/lib/mongoid/extensions/binary.rb @@ -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 diff --git a/lib/mongoid/extensions/boolean.rb b/lib/mongoid/extensions/boolean.rb index 7674ec9c8e..5b3d33405a 100644 --- a/lib/mongoid/extensions/boolean.rb +++ b/lib/mongoid/extensions/boolean.rb @@ -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 diff --git a/lib/mongoid/extensions/date.rb b/lib/mongoid/extensions/date.rb index 52d90cc9ff..76d5b89092 100644 --- a/lib/mongoid/extensions/date.rb +++ b/lib/mongoid/extensions/date.rb @@ -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 diff --git a/lib/mongoid/extensions/float.rb b/lib/mongoid/extensions/float.rb index a2491e11ed..ac38019ba3 100644 --- a/lib/mongoid/extensions/float.rb +++ b/lib/mongoid/extensions/float.rb @@ -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 diff --git a/lib/mongoid/extensions/hash.rb b/lib/mongoid/extensions/hash.rb index f1b067e7ab..d9740e6a23 100644 --- a/lib/mongoid/extensions/hash.rb +++ b/lib/mongoid/extensions/hash.rb @@ -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 diff --git a/lib/mongoid/extensions/integer.rb b/lib/mongoid/extensions/integer.rb index b9c4ce1877..2026a8ebcf 100644 --- a/lib/mongoid/extensions/integer.rb +++ b/lib/mongoid/extensions/integer.rb @@ -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 diff --git a/lib/mongoid/extensions/range.rb b/lib/mongoid/extensions/range.rb index b39a61ddb0..54ff775094 100644 --- a/lib/mongoid/extensions/range.rb +++ b/lib/mongoid/extensions/range.rb @@ -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 diff --git a/lib/mongoid/extensions/raw_value.rb b/lib/mongoid/extensions/raw_value.rb index 9343ae52bf..c50a91c533 100644 --- a/lib/mongoid/extensions/raw_value.rb +++ b/lib/mongoid/extensions/raw_value.rb @@ -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 @@ -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 diff --git a/lib/mongoid/extensions/regexp.rb b/lib/mongoid/extensions/regexp.rb index 1208fe0ba3..eedf96a7e1 100644 --- a/lib/mongoid/extensions/regexp.rb +++ b/lib/mongoid/extensions/regexp.rb @@ -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 diff --git a/lib/mongoid/extensions/set.rb b/lib/mongoid/extensions/set.rb index 7bd08a6ce3..0a8ddae778 100644 --- a/lib/mongoid/extensions/set.rb +++ b/lib/mongoid/extensions/set.rb @@ -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 diff --git a/lib/mongoid/extensions/string.rb b/lib/mongoid/extensions/string.rb index 5fb256c785..53cc5a4857 100644 --- a/lib/mongoid/extensions/string.rb +++ b/lib/mongoid/extensions/string.rb @@ -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 diff --git a/lib/mongoid/extensions/symbol.rb b/lib/mongoid/extensions/symbol.rb index 159b813e5c..d316f56595 100644 --- a/lib/mongoid/extensions/symbol.rb +++ b/lib/mongoid/extensions/symbol.rb @@ -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 diff --git a/lib/mongoid/extensions/time.rb b/lib/mongoid/extensions/time.rb index afa59ba72f..4c55c0971b 100644 --- a/lib/mongoid/extensions/time.rb +++ b/lib/mongoid/extensions/time.rb @@ -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 diff --git a/lib/mongoid/fields/localized.rb b/lib/mongoid/fields/localized.rb index e0a0ac9f49..9f7cbc5bc1 100644 --- a/lib/mongoid/fields/localized.rb +++ b/lib/mongoid/fields/localized.rb @@ -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 diff --git a/lib/mongoid/fields/standard.rb b/lib/mongoid/fields/standard.rb index a727f3a2c1..99f53b12dd 100644 --- a/lib/mongoid/fields/standard.rb +++ b/lib/mongoid/fields/standard.rb @@ -9,7 +9,32 @@ class Standard # Set readers for the instance variables. attr_accessor :default_val, :label, :name, :options - def_delegators :type, :demongoize, :evolve, :mongoize + # If type.mongoize returns Mongoid::RawValue, + # handle according to field or global strict setting + def mongoize(object) + value = type.mongoize(object) + if value.is_a?(Mongoid::RawValue) + case strict + when :error then value.raise_error! + when :warn then value.warn and return nil + when :suppress then return nil + # when :defer, assign the Mongoid::RawValue and fail when trying to persist. + end + end + value + end + + # If type.demongoize returns Mongoid::RawValue, + # return the inner value according to Mongoid.wrap_uncastable_values_from_database + def demongoize(object) + value = type.demongoize(object) + if value.is_a?(Mongoid::RawValue) && !Mongoid.wrap_uncastable_values_from_database + return value.raw_value + end + value + end + + def_delegators :type, :evolve # Adds the atomic changes for this type of resizable field. # @@ -107,6 +132,29 @@ def localize_present? false end + # Whether or not the field raises an error if a non-castable + # type is assignment. + # + # @example Get the type. + # field.type + # + # @return [ :error | :warn | :suppress ] The value. True means raise + # an error. False means handle as nil. + def strict + return @strict if defined?(@strict) + if options.key?(:strict) + @strict = case options[:strict] + when true, :error then :error + when false, :suppress then :suppress + when :warn then :warn + end + end + # TODO: add default. Array/Hash should be strict. + # Also consider global + # Don't memoize default? + # Support warn option + end + # Get the metadata for the field if its a foreign key. # # @example Get the metadata. diff --git a/lib/mongoid/fields/validators/macro.rb b/lib/mongoid/fields/validators/macro.rb index 13f6437105..38df499712 100644 --- a/lib/mongoid/fields/validators/macro.rb +++ b/lib/mongoid/fields/validators/macro.rb @@ -17,6 +17,7 @@ module Macro :fallbacks, :association, :pre_processed, + :strict, :subtype, :type, :overwrite diff --git a/lib/mongoid/stringified_symbol.rb b/lib/mongoid/stringified_symbol.rb index 0614582ebf..558831dfe5 100644 --- a/lib/mongoid/stringified_symbol.rb +++ b/lib/mongoid/stringified_symbol.rb @@ -17,11 +17,9 @@ class << self # # @api private def demongoize(object) - if object.nil? - object - else - object.to_s.to_sym - end + return if object.nil? + return object.to_s.to_sym if object.respond_to?(:to_s) + Mongoid::RawValue.new(object, 'String') end # Turn the object from the ruby type we deal with to a Mongo friendly @@ -36,11 +34,9 @@ def demongoize(object) # # @api private def mongoize(object) - if object.nil? - object - else - object.to_s - end + return if object.nil? + return object.to_s if object.respond_to?(:to_s) + Mongoid::RawValue.new(object, 'String') end # @api private