Skip to content

Commit

Permalink
MONGOID-5336 User-defined symbol field types - squashed commits
Browse files Browse the repository at this point in the history
  • Loading branch information
johnnyshields committed Jun 24, 2022
1 parent 449022f commit f28fd74
Show file tree
Hide file tree
Showing 12 changed files with 542 additions and 129 deletions.
17 changes: 15 additions & 2 deletions docs/reference/fields.txt
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,17 @@ can use in our model class as follows:
field :location, type: Point
end

You may optionally declare a mapping for the new field type in an initializer:

.. code-block:: ruby

# in /config/initializers/mongoid_custom_fields.rb

Mongoid::Fields.configure do
define_type :point, Point
end


Then make a Ruby class to represent the type. This class must define methods
used for MongoDB serialization and deserialization as follows:

Expand Down Expand Up @@ -1008,8 +1019,10 @@ specifiying its handler function as a block:

# in /config/initializers/mongoid_custom_fields.rb

Mongoid::Fields.option :required do |model, field, value|
model.validates_presence_of field if value
Mongoid::Fields.configure do
option :required do |model, field, value|
model.validates_presence_of field.name if value
end
end

Then, use it your model class:
Expand Down
18 changes: 17 additions & 1 deletion docs/release-notes/mongoid-8.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ Mongoid 8 behavior:
include Mongoid::Document

field :name, type: :bogus
# => raises Mongoid::Errors::InvalidFieldType
# => raises Mongoid::Errors::UnknownFieldType
end

Mongoid 7 behavior:
Expand All @@ -305,6 +305,22 @@ Mongoid 7 behavior:
end


Support for Defining Custom Field Type Values
---------------------------------------------

Mongoid 8.0 adds the ability to define custom ``field :type`` Symbol values as follows:

.. code-block:: ruby

# in /config/initializers/mongoid_custom_fields.rb

Mongoid::Fields.configure do
define_type :point, Point
end

Refer to the :ref:`docs <http://docs.mongodb.org/manual/reference/fields/#custom-field-types>` for details.


Removed ``:drop_dups`` Option from Indexes
------------------------------------------

Expand Down
35 changes: 27 additions & 8 deletions lib/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -189,22 +189,25 @@ en:
resolution: "When defining the field :%{name} on '%{klass}', please provide
valid options for the field. These are currently: %{valid}. If you
meant to define a custom field option, please do so first as follows:\n\n
\_\_Mongoid::Fields.option :%{option} do |model, field, value|\n
\_\_\_\_# Your logic here...\n
\_\_Mongoid::Fields.configure do\n
\_\_\_\_option :%{option} do |model, field, value|\n
\_\_\_\_\_\_# Your logic here...\n
\_\_\_\_end\n
\_\_end\n
\_\_class %{klass}\n
\_\_\_\_include Mongoid::Document\n
\_\_\_\_field :%{name}, %{option}: true\n
\_\_end\n\n
Refer to:
https://docs.mongodb.com/mongoid/current/reference/fields/#custom-field-options"
invalid_field_type:
message: "Invalid field type %{type_inspection} for field '%{field}' on model '%{klass}'."
summary: "Model '%{klass}' defines a field '%{field}' with an unknown type value
%{type_inspection}."
resolution: "Please provide a valid type value for the field.
invalid_field_type_definition:
message: "The field type definition of %{type_inspection} to %{klass_inspection} is invalid."
summary: "In the field type definition, either field_type %{type_inspection} is not
a Symbol or String, and/or klass %{klass_inspection} is not a Class or Module."
resolution: "Please ensure you are specifying field_type as either a Symbol or String,
and klass as a Class or Module.\n\n
Refer to:
https://docs.mongodb.com/mongoid/current/reference/fields/#using-symbols-or-strings-instead-of-classes"
https://docs.mongodb.com/mongoid/current/reference/fields/#custom-field-types"
invalid_includes:
message: "Invalid includes directive: %{klass}.includes(%{args})"
summary: "Eager loading in Mongoid only supports providing arguments
Expand Down Expand Up @@ -584,6 +587,22 @@ en:
resolution: "Define the field '%{name}' in %{klass}, or include
Mongoid::Attributes::Dynamic in %{klass} if you intend to
store values in fields that are not explicitly defined."
unknown_field_type:
message: "Unknown field type %{type_inspection} for field '%{field}' on model '%{klass}'."
summary: "Model '%{klass}' declares a field '%{field}' with an unknown type value
%{type_inspection}. This value is neither present in Mongoid's default type mapping,
nor defined in a custom field type mapping."
resolution: "Please provide a known type value for the field. If you
meant to define a custom field type, please do so first as follows:\n\n
\_\_Mongoid::Fields.configure do\n
\_\_\_\_define_type %{type_inspection}, YourTypeClass
\_\_end\n
\_\_class %{klass}\n
\_\_\_\_include Mongoid::Document\n
\_\_\_\_field :%{field}, type: %{type_inspection}\n
\_\_end\n\n
Refer to:
https://docs.mongodb.com/mongoid/current/reference/fields/#custom-field-types"
unknown_model:
message: "Attempted to instantiate an object of the unknown model '%{klass}'."
summary: "A document with the value '%{value}' at the key '_type' was used to
Expand Down
3 changes: 2 additions & 1 deletion lib/mongoid/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
require "mongoid/errors/invalid_dependent_strategy"
require "mongoid/errors/invalid_field"
require "mongoid/errors/invalid_field_option"
require "mongoid/errors/invalid_field_type"
require "mongoid/errors/invalid_field_type_definition"
require "mongoid/errors/invalid_find"
require "mongoid/errors/invalid_includes"
require "mongoid/errors/invalid_index"
Expand Down Expand Up @@ -56,6 +56,7 @@
require "mongoid/errors/scope_overwrite"
require "mongoid/errors/too_many_nested_attribute_records"
require "mongoid/errors/unknown_attribute"
require "mongoid/errors/unknown_field_type"
require "mongoid/errors/unknown_model"
require "mongoid/errors/unsaved_document"
require "mongoid/errors/unsupported_javascript"
Expand Down
27 changes: 27 additions & 0 deletions lib/mongoid/errors/invalid_field_type_definition.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module Mongoid
module Errors

# This error is raised when trying to define a field type mapping with
# invalid argument types.
class InvalidFieldTypeDefinition < MongoidError

# Create the new error.
#
# @example Instantiate the error.
# InvalidFieldTypeDefinition.new('number', 123)
#
# @param [ Object ] field_type The object which is expected to a be Symbol or String.
# @param [ Object ] klass The object which is expected to be a Class or Module.
def initialize(field_type, klass)
type_inspection = field_type.try(:inspect) || field_type.class.inspect
klass_inspection = klass.try(:inspect) || klass.class.inspect
super(
compose_message('invalid_field_type_definition',
type_inspection: type_inspection, klass_inspection: klass_inspection)
)
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@ module Errors

# This error is raised when trying to define a field using a :type option value
# that is not present in the field type mapping.
class InvalidFieldType < MongoidError
class UnknownFieldType < MongoidError

# Create the new error.
#
# @example Instantiate the error.
# InvalidFieldType.new('Person', 'first_name', 'stringgy')
# UnknownFieldType.new('Person', 'first_name', 'stringgy')
#
# @param [ String ] klass The model class.
# @param [ String ] field The field on which the invalid type is used.
# @param [ Symbol | String ] type The value of the field :type option.
def initialize(klass, field, type)
super(
compose_message('invalid_field_type',
compose_message('unknown_field_type',
klass: klass, field: field, type_inspection: type.inspect)
)
end
Expand Down
88 changes: 48 additions & 40 deletions lib/mongoid/fields.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require "mongoid/fields/foreign_key"
require "mongoid/fields/localized"
require "mongoid/fields/validators"
require "mongoid/fields/field_types"

module Mongoid

Expand All @@ -14,26 +15,8 @@ module Fields
StringifiedSymbol = Mongoid::StringifiedSymbol
Boolean = Mongoid::Boolean

# For fields defined with symbols use the correct class.
TYPE_MAPPINGS = {
array: Array,
big_decimal: BigDecimal,
binary: BSON::Binary,
boolean: Mongoid::Boolean,
date: Date,
date_time: DateTime,
float: Float,
hash: Hash,
integer: Integer,
object_id: BSON::ObjectId,
range: Range,
regexp: Regexp,
set: Set,
string: String,
stringified_symbol: StringifiedSymbol,
symbol: Symbol,
time: Time
}.with_indifferent_access
# @deprecated
TYPE_MAPPINGS = ::Mongoid::Fields::FieldTypes::DEFAULT_MAPPING

# Constant for all names of the _id field in a document.
#
Expand All @@ -45,7 +28,7 @@ module Fields
# BSON classes that are not supported as field types
#
# @api private
INVALID_BSON_CLASSES = [ BSON::Decimal128, BSON::Int32, BSON::Int64 ].freeze
UNSUPPORTED_BSON_TYPES = [ BSON::Decimal128, BSON::Int32, BSON::Int64 ].freeze

module ClassMethods
# Returns the list of id fields for this model class, as both strings
Expand Down Expand Up @@ -274,6 +257,33 @@ def validate_writable_field_name!(name)

class << self

# DSL method used for configuration readability, typically in
# an initializer.
#
# @example
# Mongoid::Fields.configure do
# # do configuration
# end
def configure(&block)
instance_exec(&block)
end

# Defines a field type mapping, for later use in field :type option.
#
# @example
# Mongoid::Fields.configure do
# define_type :point, Point
# end
#
# @param [ Symbol | String ] field_type the identifier of the
# defined type. This identifier will be accessible as either a
# string or a symbol regardless of the type passed to this method.
# @param [ Module ] klass the class of the defined type, which must
# include mongoize, demongoize, and evolve methods.
def define_type(field_type, klass)
Fields::FieldTypes.define_type(field_type, klass)
end

# Stores the provided block to be run when the option name specified is
# defined on a field.
#
Expand All @@ -282,8 +292,10 @@ class << self
# provided in the field definition -- even if it is false or nil.
#
# @example
# Mongoid::Fields.option :required do |model, field, value|
# model.validates_presence_of field if value
# Mongoid::Fields.configure do
# option :required do |model, field, value|
# model.validates_presence_of field.name if value
# end
# end
#
# @param [ Symbol ] option_name the option name to match against
Expand Down Expand Up @@ -767,32 +779,28 @@ def remove_defaults(name)

def field_for(name, options)
opts = options.merge(klass: self)
type_mapping = TYPE_MAPPINGS[options[:type]]
opts[:type] = type_mapping || unmapped_type(options)
if !opts[:type].is_a?(Class)
raise Errors::InvalidFieldType.new(self, name, options[:type])
else
if INVALID_BSON_CLASSES.include?(opts[:type])
warn_message = "Using #{opts[:type]} as the field type is not supported. "
if opts[:type] == BSON::Decimal128
warn_message += "In BSON <= 4, the BSON::Decimal128 type will work as expected for both storing and querying, but will return a BigDecimal on query in BSON 5+."
else
warn_message += "Saving values of this type to the database will work as expected, however, querying them will return a value of the native Ruby Integer type."
end
Mongoid.logger.warn(warn_message)
if type = options[:type]
type = Fields::FieldTypes.get(type)
unless type
raise Mongoid::Errors::UnknownFieldType.new(self.name, name, type)
end
opts[:type] = type
warn_unsupported_bson_type(type)
end
return Fields::Localized.new(name, opts) if options[:localize]
return Fields::ForeignKey.new(name, opts) if options[:identity]
Fields::Standard.new(name, opts)
end

def unmapped_type(options)
if "Boolean" == options[:type].to_s
Mongoid::Boolean
def warn_unsupported_bson_type(type)
return unless UNSUPPORTED_BSON_TYPES.include?(type)
warn_message = "Using #{type} as the field type is not supported. "
if type == BSON::Decimal128
warn_message += "In BSON <= 4, the BSON::Decimal128 type will work as expected for both storing and querying, but will return a BigDecimal on query in BSON 5+."
else
options[:type] || Object
warn_message += "Saving values of this type to the database will work as expected, however, querying them will return a value of the native Ruby Integer type."
end
Mongoid.logger.warn(warn_message)
end
end
end
Expand Down
Loading

0 comments on commit f28fd74

Please sign in to comment.