Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better handling of type casting #95

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions lib/mongoid/attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,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 @@ -352,6 +354,33 @@ def unalias_attribute(name)
end
end

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::InvalidAttributeAssignment.new(field.type, value.class)
# end
# end
# end

def lookup_attribute_presence(name, value)
if localized_fields.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 @@ -69,6 +69,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 :raise_invalid_type_assignment_error, default: false

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

# Return stored times as UTC.
option :use_utc, default: false

Expand Down
2 changes: 1 addition & 1 deletion lib/mongoid/config/encryption.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

require 'mongoid/extensions/boolean'
require 'mongoid/stringified_symbol'
require 'mongoid/extensions/stringified_symbol'

module Mongoid
module Config
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 @@ -136,7 +136,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 @@ -147,18 +147,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
24 changes: 24 additions & 0 deletions lib/mongoid/errors/invalid_type_assignment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

module Mongoid
module Errors

# This exception is raised when attempting to assign a field value
# which cannot be cast to field type.
class InvalidAttributeAssignment < MongoidError

# Create the new invalid attribute assignment error.
#
# @example Create the new invalid date error.
# InvalidAttributeAssignment.new('Integer', 'String')
#
# @param [ String | Class ] field_type The type of the field that was
# attempted to be assigned.
# @param [ String | Class ] value_class The class of the value that was
# attempted to be assigned.
def initialize(field_type, value_class)
super(compose_message('invalid_attribute_assignment', { field_type: field_type.to_s, value_class: value_class.to_s }))
end
end
end
end
2 changes: 1 addition & 1 deletion lib/mongoid/extensions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
require 'mongoid/extensions/regexp'
require 'mongoid/extensions/set'
require 'mongoid/extensions/string'
require 'mongoid/stringified_symbol'
require 'mongoid/extensions/stringified_symbol'
require 'mongoid/extensions/symbol'
require 'mongoid/extensions/time'
require 'mongoid/extensions/time_with_zone'
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 @@ -153,6 +153,8 @@ def mongoize(object)
case object
when ::Array, ::Set
object.map(&:mongoize)
else
Mongoid::RawValue(object, 'Array')
end
end

Expand Down
10 changes: 7 additions & 3 deletions lib/mongoid/extensions/big_decimal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,17 @@ 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
elsif object.is_a?(BSON::Decimal128) || object.numeric?
object.to_s
elsif !object.is_a?(String)
elsif !object.is_a?(String) && object.respond_to?(:to_d)
object.try(:to_d)&.to_s
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 @@ -33,6 +33,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_method :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 @@ -23,6 +23,8 @@ def mongoize(object)
true
elsif object.to_s&.match?(FALSY_VALUES)
false
else
Mongoid::RawValue(object, 'Boolean')
end
end
alias_method :demongoize, :mongoize
Expand Down
21 changes: 12 additions & 9 deletions lib/mongoid/extensions/date.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,19 +68,22 @@ def demongoize(object)
def mongoize(object)
return if object.blank?

begin
time = if object.is_a?(String)
# https://jira.mongodb.org/browse/MONGOID-4460
::Time.parse(object)
else
object.__mongoize_time__
end
time = begin
if object.is_a?(String)
# https://jira.mongodb.org/browse/MONGOID-4460
::Time.parse(object)
else
object.__mongoize_time__
end
rescue ArgumentError
nil
end
return unless time.acts_like?(:time)

::Time.utc(time.year, time.month, time.day)
if time&.acts_like?(:time)
return ::Time.utc(time.year, time.month, time.day)
end

Mongoid::RawValue(object, 'Date')
end
end
end
Expand Down
6 changes: 3 additions & 3 deletions lib/mongoid/extensions/float.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ module ClassMethods
def mongoize(object)
return if object.blank?

if object.is_a?(String)
object.to_f if object.numeric?
if (object.is_a?(String) && object.numeric?) || object.respond_to?(:to_f)
object.to_f
else
object.try(:to_f)
Mongoid::RawValue(object, 'Float')
end
end
alias_method :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 @@ -230,6 +230,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
15 changes: 7 additions & 8 deletions lib/mongoid/extensions/integer.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# frozen_string_literal: true
# rubocop:todo all

module Mongoid
module Extensions

# Adds type-casting behavior to Integer class.
module Integer

# Converts the integer into a time as the number of seconds since the epoch.
Expand Down Expand Up @@ -48,17 +47,17 @@ module ClassMethods
def mongoize(object)
return if object.blank?

if object.is_a?(String)
object.to_i if object.numeric?
if (object.is_a?(String) && object.numeric?) || object.respond_to?(:to_i)
object.to_i
else
object.try(:to_i)
Mongoid::RawValue(object, 'Integer')
end
end
alias_method :demongoize, :mongoize
alias :demongoize :mongoize
end
end
end
end

Integer.include Mongoid::Extensions::Integer
Integer.extend(Mongoid::Extensions::Integer::ClassMethods)
::Integer.__send__(:include, Mongoid::Extensions::Integer)
::Integer.extend(Mongoid::Extensions::Integer::ClassMethods)
1 change: 1 addition & 0 deletions lib/mongoid/extensions/range.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,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
62 changes: 56 additions & 6 deletions lib/mongoid/extensions/raw_value.rb
Original file line number Diff line number Diff line change
@@ -1,25 +1,45 @@
# 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) # rubocop:disable Naming/MethodName
RawValue.new(*args)
def RawValue(raw_value, cast_class_name = nil) # rubocop:disable Naming/MethodName
return if raw_value.nil?

RawValue.new(raw_value, cast_class_name)
end

# Represents a value which cannot be type-casted between Ruby and MongoDB.
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 @@ -29,5 +49,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
Loading