Skip to content

Commit a8c2217

Browse files
committed
MONGOID-5336 User-defined symbol field types - squashed commits
1 parent 604a407 commit a8c2217

14 files changed

+601
-140
lines changed

docs/reference/fields.txt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,6 +1057,17 @@ can use in our model class as follows:
10571057
field :location, type: Point
10581058
end
10591059

1060+
You may optionally declare a mapping for the new field type in an initializer:
1061+
1062+
.. code-block:: ruby
1063+
1064+
# in /config/initializers/mongoid_custom_fields.rb
1065+
1066+
Mongoid.configure do |config|
1067+
config.field_type :point, Point
1068+
end
1069+
1070+
10601071
Then make a Ruby class to represent the type. This class must define methods
10611072
used for MongoDB serialization and deserialization as follows:
10621073

@@ -1235,8 +1246,10 @@ specifiying its handler function as a block:
12351246

12361247
# in /config/initializers/mongoid_custom_fields.rb
12371248

1238-
Mongoid::Fields.option :max_length do |model, field, value|
1239-
model.validates_length_of field.name, maximum: value
1249+
Mongoid.configure do |config|
1250+
config.field_option :max_length do |model, field, value|
1251+
model.validates_length_of field.name, maximum: value
1252+
end
12401253
end
12411254

12421255
Then, use it your model class:

docs/release-notes/mongoid-9.0.txt

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,64 @@ Mongoid 9.0 flips the default of this flag from ``true`` => ``false``.
120120

121121
This means that, by default, Mongoid 9 will update the existing document and
122122
will not replace it.
123+
124+
125+
Support for Defining Custom Field Type Values
126+
---------------------------------------------
127+
128+
Mongoid 9.0 adds the ability to define custom ``field :type`` Symbol values as follows:
129+
130+
.. code-block:: ruby
131+
132+
# in /config/initializers/mongoid.rb
133+
134+
Mongoid.configure do |config|
135+
config.field_type :point, Point
136+
end
137+
138+
Refer to the :ref:`docs <http://docs.mongodb.org/manual/reference/fields/#custom-field-types>` for details.
139+
140+
141+
Rename error InvalidFieldType to UnknownFieldType
142+
-------------------------------------------------
143+
144+
The error class InvalidFieldType has been renamed to UnknownFieldType
145+
to improve clarity. This error occurs when attempting using the
146+
``field`` macro in a Document definition with a ``:type`` Symbol that
147+
does not correspond to any built-in or custom-defined field type.
148+
149+
.. code-block:: ruby
150+
151+
class User
152+
include Mongoid::Document
153+
154+
field :name, type: :bogus
155+
#=> raises Mongoid::Errors::UnknownFieldType
156+
end
157+
158+
159+
Support for Defining Custom Field Options via Top-Level Config
160+
--------------------------------------------------------------
161+
162+
Mongoid 9.0 adds the ability to define custom ``field`` options as follows:
163+
164+
.. code-block:: ruby
165+
166+
# in /config/initializers/mongoid.rb
167+
168+
Mongoid.configure do |config|
169+
config.field_option :max_length do |model, field, value|
170+
model.validates_length_of field.name, maximum: value
171+
end
172+
end
173+
174+
In Mongoid 8, this was possible with the following legacy syntax. Users are
175+
recommended to migrate to the Mongoid 9.0 syntax above.
176+
177+
.. code-block:: ruby
178+
179+
Mongoid::Fields.option :max_length do |model, field, value|
180+
model.validates_length_of field.name, maximum: value
181+
end
182+
183+
Refer to the :ref:`docs <http://docs.mongodb.org/manual/reference/fields/#custom-field-options>` for details.

lib/config/locales/en.yml

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -230,22 +230,25 @@ en:
230230
resolution: "When defining the field :%{name} on '%{klass}', please provide
231231
valid options for the field. These are currently: %{valid}. If you
232232
meant to define a custom field option, please do so first as follows:\n\n
233-
\_\_Mongoid::Fields.option :%{option} do |model, field, value|\n
234-
\_\_\_\_# Your logic here...\n
233+
\_\_Mongoid.configure do |config|\n
234+
\_\_\_\_config.field_option :%{option} do |model, field, value|\n
235+
\_\_\_\_\_\_# Your logic here...\n
236+
\_\_\_\_end\n
235237
\_\_end\n
236238
\_\_class %{klass}\n
237239
\_\_\_\_include Mongoid::Document\n
238240
\_\_\_\_field :%{name}, %{option}: true\n
239241
\_\_end\n\n
240242
Refer to:
241-
https://www.mongodb.com/docs/mongoid/current/reference/fields/#custom-field-options"
242-
invalid_field_type:
243-
message: "Invalid field type %{type_inspection} for field '%{field}' on model '%{klass}'."
244-
summary: "Model '%{klass}' defines a field '%{field}' with an unknown type value
245-
%{type_inspection}."
246-
resolution: "Please provide a valid type value for the field.
243+
https://docs.mongodb.com/mongoid/current/reference/fields/#custom-field-options"
244+
invalid_field_type_definition:
245+
message: "The field type definition of %{type_inspection} to %{klass_inspection} is invalid."
246+
summary: "In the field type definition, either field_type %{type_inspection} is not
247+
a Symbol or String, and/or klass %{klass_inspection} is not a Class or Module."
248+
resolution: "Please ensure you are specifying field_type as either a Symbol or String,
249+
and klass as a Class or Module.\n\n
247250
Refer to:
248-
https://www.mongodb.com/docs/mongoid/current/reference/fields/#using-symbols-or-strings-instead-of-classes"
251+
https://www.mongodb.com/docs/mongoid/current/reference/fields/#custom-field-types"
249252
invalid_global_executor_concurrency:
250253
message: "Invalid global_executor_concurrency option."
251254
summary: "You set global_executor_concurrency while async_query_executor
@@ -628,6 +631,22 @@ en:
628631
resolution: "Define the field '%{name}' in %{klass}, or include
629632
Mongoid::Attributes::Dynamic in %{klass} if you intend to
630633
store values in fields that are not explicitly defined."
634+
unknown_field_type:
635+
message: "Unknown field type %{type_inspection} for field '%{field}' on model '%{klass}'."
636+
summary: "Model '%{klass}' declares a field '%{field}' with an unknown type value
637+
%{type_inspection}. This value is neither present in Mongoid's default type mapping,
638+
nor defined in a custom field type mapping."
639+
resolution: "Please provide a known type value for the field. If you
640+
meant to define a custom field type, please do so first as follows:\n\n
641+
\_\_Mongoid.configure do |config|\n
642+
\_\_\_\_config.field_type %{type_inspection}, YourTypeClass
643+
\_\_end\n
644+
\_\_class %{klass}\n
645+
\_\_\_\_include Mongoid::Document\n
646+
\_\_\_\_field :%{field}, type: %{type_inspection}\n
647+
\_\_end\n\n
648+
Refer to:
649+
https://docs.mongodb.com/mongoid/current/reference/fields/#custom-field-types"
631650
unknown_model:
632651
message: "Attempted to instantiate an object of the unknown model '%{klass}'."
633652
summary: "A document with the value '%{value}' at the key '_type' was used to

lib/mongoid/config.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,43 @@ def running_with_passenger?
359359
@running_with_passenger ||= defined?(PhusionPassenger)
360360
end
361361

362+
# Defines a field type mapping, for later use in field :type option.
363+
#
364+
# @example
365+
# Mongoid.configure do |config|
366+
# config.field_type :point, Point
367+
# end
368+
#
369+
# @param [ Symbol | String ] type_name The identifier of the
370+
# defined type. This identifier may be accessible as either a
371+
# Symbol or a String regardless of the type passed to this method.
372+
# @param [ Module ] klass the class of the defined type, which must
373+
# include mongoize, demongoize, and evolve methods.
374+
def field_type(type_name, klass)
375+
Mongoid::Fields::FieldTypes.define_type(type_name, klass)
376+
end
377+
378+
# Defines an option for the field macro, which runs the handler
379+
# provided as a block.
380+
#
381+
# No assumptions are made about what functionality the handler might
382+
# perform, so it will always be called if the `option_name` key is
383+
# provided in the field definition -- even if it is false or nil.
384+
#
385+
# @example
386+
# Mongoid.configure do |config|
387+
# config.field_option :required do |model, field, value|
388+
# model.validates_presence_of field.name if value
389+
# end
390+
# end
391+
#
392+
# @param [ Symbol ] option_name the option name to match against
393+
# @param [ Proc ] block the handler to execute when the option is
394+
# provided.
395+
def field_option(option_name, &block)
396+
Mongoid::Fields.option(option_name, &block)
397+
end
398+
362399
private
363400

364401
def set_log_levels

lib/mongoid/errors.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
require "mongoid/errors/invalid_dependent_strategy"
1818
require "mongoid/errors/invalid_field"
1919
require "mongoid/errors/invalid_field_option"
20-
require "mongoid/errors/invalid_field_type"
20+
require "mongoid/errors/invalid_field_type_definition"
2121
require "mongoid/errors/invalid_find"
2222
require "mongoid/errors/invalid_global_executor_concurrency"
2323
require "mongoid/errors/invalid_includes"
@@ -59,6 +59,7 @@
5959
require "mongoid/errors/scope_overwrite"
6060
require "mongoid/errors/too_many_nested_attribute_records"
6161
require "mongoid/errors/unknown_attribute"
62+
require "mongoid/errors/unknown_field_type"
6263
require "mongoid/errors/unknown_model"
6364
require "mongoid/errors/unsaved_document"
6465
require "mongoid/errors/unsupported_javascript"
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# frozen_string_literal: true
2+
3+
module Mongoid
4+
module Errors
5+
6+
# This error is raised when trying to define a field type mapping with
7+
# invalid argument types.
8+
class InvalidFieldTypeDefinition < MongoidError
9+
10+
# Create the new error.
11+
#
12+
# @example Instantiate the error.
13+
# InvalidFieldTypeDefinition.new('number', 123)
14+
#
15+
# @param [ Object ] field_type The object which is expected to a be Symbol or String.
16+
# @param [ Object ] klass The object which is expected to be a Class or Module.
17+
def initialize(field_type, klass)
18+
type_inspection = field_type.try(:inspect) || field_type.class.inspect
19+
klass_inspection = klass.try(:inspect) || klass.class.inspect
20+
super(
21+
compose_message('invalid_field_type_definition',
22+
type_inspection: type_inspection, klass_inspection: klass_inspection)
23+
)
24+
end
25+
end
26+
end
27+
end

lib/mongoid/errors/invalid_field_type.rb renamed to lib/mongoid/errors/unknown_field_type.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,19 @@ module Errors
55

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

1010
# Create the new error.
1111
#
1212
# @example Instantiate the error.
13-
# InvalidFieldType.new('Person', 'first_name', 'stringgy')
13+
# UnknownFieldType.new('Person', 'first_name', 'stringgy')
1414
#
1515
# @param [ String ] klass The model class.
1616
# @param [ String ] field The field on which the invalid type is used.
1717
# @param [ Symbol | String ] type The value of the field :type option.
1818
def initialize(klass, field, type)
1919
super(
20-
compose_message('invalid_field_type',
20+
compose_message('unknown_field_type',
2121
klass: klass, field: field, type_inspection: type.inspect)
2222
)
2323
end

lib/mongoid/fields.rb

Lines changed: 23 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require "mongoid/fields/foreign_key"
55
require "mongoid/fields/localized"
66
require "mongoid/fields/validators"
7+
require "mongoid/fields/field_types"
78

89
module Mongoid
910

@@ -14,26 +15,8 @@ module Fields
1415
StringifiedSymbol = Mongoid::StringifiedSymbol
1516
Boolean = Mongoid::Boolean
1617

17-
# For fields defined with symbols use the correct class.
18-
TYPE_MAPPINGS = {
19-
array: Array,
20-
big_decimal: BigDecimal,
21-
binary: BSON::Binary,
22-
boolean: Mongoid::Boolean,
23-
date: Date,
24-
date_time: DateTime,
25-
float: Float,
26-
hash: Hash,
27-
integer: Integer,
28-
object_id: BSON::ObjectId,
29-
range: Range,
30-
regexp: Regexp,
31-
set: Set,
32-
string: String,
33-
stringified_symbol: StringifiedSymbol,
34-
symbol: Symbol,
35-
time: Time
36-
}.with_indifferent_access
18+
# @deprecated
19+
TYPE_MAPPINGS = ::Mongoid::Fields::FieldTypes::DEFAULT_MAPPING
3720

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

5033
module ClassMethods
5134
# Returns the list of id fields for this model class, as both strings
@@ -283,7 +266,7 @@ class << self
283266
#
284267
# @example
285268
# Mongoid::Fields.option :required do |model, field, value|
286-
# model.validates_presence_of field if value
269+
# model.validates_presence_of field.name if value
287270
# end
288271
#
289272
# @param [ Symbol ] option_name the option name to match against
@@ -807,48 +790,37 @@ def field_for(name, options)
807790

808791
# Get the class for the given type.
809792
#
810-
# @param [ Symbol ] name The name of the field.
811-
# @param [ Symbol | Class ] type The type of the field.
793+
# @param [ Symbol ] field_name The name of the field.
794+
# @param [ Symbol | Class ] raw_type The type of the field.
812795
#
813796
# @return [ Class ] The type of the field.
814797
#
815-
# @raises [ Mongoid::Errors::InvalidFieldType ] if given an invalid field
798+
# @raises [ Mongoid::Errors::UnknownFieldType ] if given an invalid field
816799
# type.
817800
#
818801
# @api private
819-
def retrieve_and_validate_type(name, type)
820-
type_mapping = TYPE_MAPPINGS[type]
821-
result = type_mapping || unmapped_type(type)
822-
if !result.is_a?(Class)
823-
raise Errors::InvalidFieldType.new(self, name, type)
824-
else
825-
if INVALID_BSON_CLASSES.include?(result)
826-
warn_message = "Using #{result} as the field type is not supported. "
827-
if result == BSON::Decimal128
828-
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+."
829-
else
830-
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."
831-
end
832-
Mongoid.logger.warn(warn_message)
833-
end
834-
end
835-
result
802+
def get_field_type(field_name, raw_type)
803+
type = raw_type ? Fields::FieldTypes.get(raw_type) : Object
804+
raise Mongoid::Errors::UnknownFieldType.new(self.name, field_name, raw_type) unless type
805+
warn_if_unsupported_bson_type(type)
806+
type
836807
end
837808

838-
# Returns the type of the field if the type was not in the TYPE_MAPPINGS
839-
# hash.
809+
# Logs a warning message if the given type cannot be represented
810+
# by BSON.
840811
#
841-
# @param [ Symbol | Class ] type The type of the field.
842-
#
843-
# @return [ Class ] The type of the field.
812+
# @param [ Class ] type The type of the field.
844813
#
845814
# @api private
846-
def unmapped_type(type)
847-
if "Boolean" == type.to_s
848-
Mongoid::Boolean
815+
def warn_if_unsupported_bson_type(type)
816+
return unless UNSUPPORTED_BSON_TYPES.include?(type)
817+
warn_message = "Using #{type} as the field type is not supported. "
818+
if type == BSON::Decimal128
819+
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+."
849820
else
850-
type || Object
821+
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."
851822
end
823+
Mongoid.logger.warn(warn_message)
852824
end
853825
end
854826
end

0 commit comments

Comments
 (0)