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 Oct 4, 2022
1 parent 604a407 commit a8c2217
Show file tree
Hide file tree
Showing 14 changed files with 601 additions and 140 deletions.
17 changes: 15 additions & 2 deletions docs/reference/fields.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1057,6 +1057,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.configure do |config|
config.field_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 @@ -1235,8 +1246,10 @@ specifiying its handler function as a block:

# in /config/initializers/mongoid_custom_fields.rb

Mongoid::Fields.option :max_length do |model, field, value|
model.validates_length_of field.name, maximum: value
Mongoid.configure do |config|
config.field_option :max_length do |model, field, value|
model.validates_length_of field.name, maximum: value
end
end

Then, use it your model class:
Expand Down
61 changes: 61 additions & 0 deletions docs/release-notes/mongoid-9.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,64 @@ Mongoid 9.0 flips the default of this flag from ``true`` => ``false``.

This means that, by default, Mongoid 9 will update the existing document and
will not replace it.


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

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

.. code-block:: ruby

# in /config/initializers/mongoid.rb

Mongoid.configure do |config|
config.field_type :point, Point
end

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


Rename error InvalidFieldType to UnknownFieldType
-------------------------------------------------

The error class InvalidFieldType has been renamed to UnknownFieldType
to improve clarity. This error occurs when attempting using the
``field`` macro in a Document definition with a ``:type`` Symbol that
does not correspond to any built-in or custom-defined field type.

.. code-block:: ruby

class User
include Mongoid::Document

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


Support for Defining Custom Field Options via Top-Level Config
--------------------------------------------------------------

Mongoid 9.0 adds the ability to define custom ``field`` options as follows:

.. code-block:: ruby

# in /config/initializers/mongoid.rb

Mongoid.configure do |config|
config.field_option :max_length do |model, field, value|
model.validates_length_of field.name, maximum: value
end
end

In Mongoid 8, this was possible with the following legacy syntax. Users are
recommended to migrate to the Mongoid 9.0 syntax above.

.. code-block:: ruby

Mongoid::Fields.option :max_length do |model, field, value|
model.validates_length_of field.name, maximum: value
end

Refer to the :ref:`docs <http://docs.mongodb.org/manual/reference/fields/#custom-field-options>` for details.
37 changes: 28 additions & 9 deletions lib/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -230,22 +230,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.configure do |config|\n
\_\_\_\_config.field_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://www.mongodb.com/docs/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.
https://docs.mongodb.com/mongoid/current/reference/fields/#custom-field-options"
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://www.mongodb.com/docs/mongoid/current/reference/fields/#using-symbols-or-strings-instead-of-classes"
https://www.mongodb.com/docs/mongoid/current/reference/fields/#custom-field-types"
invalid_global_executor_concurrency:
message: "Invalid global_executor_concurrency option."
summary: "You set global_executor_concurrency while async_query_executor
Expand Down Expand Up @@ -628,6 +631,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.configure do |config|\n
\_\_\_\_config.field_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
37 changes: 37 additions & 0 deletions lib/mongoid/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,43 @@ def running_with_passenger?
@running_with_passenger ||= defined?(PhusionPassenger)
end

# Defines a field type mapping, for later use in field :type option.
#
# @example
# Mongoid.configure do |config|
# config.field_type :point, Point
# end
#
# @param [ Symbol | String ] type_name The identifier of the
# defined type. This identifier may be accessible as either a
# Symbol or a String 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 field_type(type_name, klass)
Mongoid::Fields::FieldTypes.define_type(type_name, klass)
end

# Defines an option for the field macro, which runs the handler
# provided as a block.
#
# No assumptions are made about what functionality the handler might
# perform, so it will always be called if the `option_name` key is
# provided in the field definition -- even if it is false or nil.
#
# @example
# Mongoid.configure do |config|
# config.field_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
# @param [ Proc ] block the handler to execute when the option is
# provided.
def field_option(option_name, &block)
Mongoid::Fields.option(option_name, &block)
end

private

def set_log_levels
Expand Down
3 changes: 2 additions & 1 deletion lib/mongoid/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,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_global_executor_concurrency"
require "mongoid/errors/invalid_includes"
Expand Down Expand Up @@ -59,6 +59,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
74 changes: 23 additions & 51 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 @@ -283,7 +266,7 @@ class << self
#
# @example
# Mongoid::Fields.option :required do |model, field, value|
# model.validates_presence_of field if value
# model.validates_presence_of field.name if value
# end
#
# @param [ Symbol ] option_name the option name to match against
Expand Down Expand Up @@ -807,48 +790,37 @@ def field_for(name, options)

# Get the class for the given type.
#
# @param [ Symbol ] name The name of the field.
# @param [ Symbol | Class ] type The type of the field.
# @param [ Symbol ] field_name The name of the field.
# @param [ Symbol | Class ] raw_type The type of the field.
#
# @return [ Class ] The type of the field.
#
# @raises [ Mongoid::Errors::InvalidFieldType ] if given an invalid field
# @raises [ Mongoid::Errors::UnknownFieldType ] if given an invalid field
# type.
#
# @api private
def retrieve_and_validate_type(name, type)
type_mapping = TYPE_MAPPINGS[type]
result = type_mapping || unmapped_type(type)
if !result.is_a?(Class)
raise Errors::InvalidFieldType.new(self, name, type)
else
if INVALID_BSON_CLASSES.include?(result)
warn_message = "Using #{result} as the field type is not supported. "
if result == 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)
end
end
result
def get_field_type(field_name, raw_type)
type = raw_type ? Fields::FieldTypes.get(raw_type) : Object
raise Mongoid::Errors::UnknownFieldType.new(self.name, field_name, raw_type) unless type
warn_if_unsupported_bson_type(type)
type
end

# Returns the type of the field if the type was not in the TYPE_MAPPINGS
# hash.
# Logs a warning message if the given type cannot be represented
# by BSON.
#
# @param [ Symbol | Class ] type The type of the field.
#
# @return [ Class ] The type of the field.
# @param [ Class ] type The type of the field.
#
# @api private
def unmapped_type(type)
if "Boolean" == type.to_s
Mongoid::Boolean
def warn_if_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
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 a8c2217

Please sign in to comment.