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

[READY FOR REVIEW] MONGOID-5336: User-defined symbol field types #5269

Open
wants to merge 3 commits 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
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 @@ -241,6 +241,67 @@ defaults to ``true``.
When set to false, the older, inconsistent behavior is restored.


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.


Bug Fixes and Improvements
--------------------------

Expand Down
37 changes: 28 additions & 9 deletions lib/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -237,22 +237,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 @@ -652,6 +655,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 @@ -367,6 +367,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 @@ -18,7 +18,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 @@ -65,6 +65,7 @@
require "mongoid/errors/transaction_error"
require "mongoid/errors/transactions_not_supported"
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 = {
johnnyshields marked this conversation as resolved.
Show resolved Hide resolved
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