Skip to content

Commit

Permalink
Improved field type handling
Browse files Browse the repository at this point in the history
  • Loading branch information
johnnyshields committed May 24, 2024
1 parent 37ef7b3 commit eb200a9
Show file tree
Hide file tree
Showing 23 changed files with 544 additions and 357 deletions.
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ Naming/PredicateName:
- has_attribute?
- has_attribute_before_type_cast?

RSpec/ImplicitSubject:
Enabled: false

RSpec/IndexedLet:
Enabled: false

Expand Down
19 changes: 16 additions & 3 deletions docs/reference/fields.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1016,13 +1016,24 @@ First, declare the new field type mapping in an initializer:

.. code-block:: ruby

# in /config/initializers/mongoid_custom_fields.rb
# in /config/initializers/active_document_custom_fields.rb

ActiveDocument::Fields.configure do
type :point, Point
end


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

.. code-block:: ruby

# in /config/initializers/active_document_custom_fields.rb

ActiveDocument.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 @@ -1201,8 +1212,10 @@ specifying its handler function as a block:

# in /config/initializers/active_document_custom_fields.rb

ActiveDocument::Fields.option :max_length do |model, field, value|
model.validates_length_of field.name, maximum: value
ActiveDocument.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
63 changes: 62 additions & 1 deletion docs/release-notes/migrating-from-mongoid.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,71 @@ Mongoid 8.0 adds the ability to define custom ``field :type`` Symbol values as f

.. code-block:: ruby

# in /config/initializers/mongoid_custom_fields.rb
# in /config/initializers/active_document_custom_fields.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.


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/active_document.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/active_document.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: 37 additions & 0 deletions lib/active_document/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,43 @@ def running_with_passenger?
@running_with_passenger ||= defined?(PhusionPassenger)
end

# Defines a field type mapping, for later use in field :type option.
#
# @example
# ActiveDocument.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)
ActiveDocument::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
# ActiveDocument.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)
ActiveDocument::Fields.option(option_name, &block)
end

ActiveDocument.deprecate(self, :running_with_passenger?)

private
Expand Down
3 changes: 2 additions & 1 deletion lib/active_document/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
require 'active_document/errors/invalid_dependent_strategy'
require 'active_document/errors/invalid_field'
require 'active_document/errors/invalid_field_option'
require 'active_document/errors/invalid_field_type'
require 'active_document/errors/invalid_field_type_definition'
require 'active_document/errors/invalid_find'
require 'active_document/errors/invalid_global_executor_concurrency'
require 'active_document/errors/invalid_includes'
Expand Down Expand Up @@ -66,6 +66,7 @@
require 'active_document/errors/transaction_error'
require 'active_document/errors/transactions_not_supported'
require 'active_document/errors/unknown_attribute'
require 'active_document/errors/unknown_field_type'
require 'active_document/errors/unknown_model'
require 'active_document/errors/unsaved_document'
require 'active_document/errors/unsupported_javascript'
Expand Down
27 changes: 27 additions & 0 deletions lib/active_document/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 ActiveDocument
module Errors

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

# 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 < BaseError
class UnknownFieldType < BaseError

# 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
80 changes: 24 additions & 56 deletions lib/active_document/fields.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,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 @@ -282,10 +282,8 @@ def type(symbol, klass)
# provided in the field definition -- even if it is false or nil.
#
# @example
# ActiveDocument::Fields.configure do
# option :required do |model, field, value|
# model.validates_presence_of field.name if value
# end
# ActiveDocument::Fields.option :required do |model, field, value|
# model.validates_presence_of field.name if value
# end
#
# @param [ Symbol ] option_name the option name to match against
Expand Down Expand Up @@ -805,69 +803,39 @@ 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.
#
# @raise [ ActiveDocument::Errors::InvalidFieldType ] if given an invalid field
# @raises [ ActiveDocument::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)
elsif unsupported_type?(result)
warn_message = "Using #{result} as the field type is not supported. "
warn_message += if result == BSON::Decimal128
'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
'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
ActiveDocument.logger.warn(warn_message)
end
def get_field_type(field_name, raw_type)
type = raw_type ? Fields::FieldTypes.get(raw_type) : Object
raise ActiveDocument::Errors::UnknownFieldType.new(name, field_name, raw_type) unless type

result
warn_if_unsupported_bson_type(type)
type
end

def field_type_klass_for(field, type)
klass = Fields::FieldTypes.get(type)
return klass if klass

raise ActiveDocument::Errors::InvalidFieldType.new(name, field, type)
end

# Returns the type of the field if the type was not in the TYPE_MAPPINGS
# hash.
#
# @param [ Symbol | Class ] type The type of the field.
# Logs a warning message if the given type cannot be represented
# by BSON.
#
# @return [ Class ] The type of the field.
# @param [ Class ] type The type of the field.
#
# @api private
def unmapped_type(type)
if type.to_s == 'Boolean'
ActiveDocument::Boolean
else
type || Object
end
end

# Queries whether or not the given type is permitted as a declared field
# type.
#
# @param [ Class ] type The type to query
#
# @return [ true | false ] whether or not the type is supported
#
# @api private
def unsupported_type?(type)
return !ActiveDocument::Config.allow_bson5_decimal128? if type == BSON::Decimal128

INVALID_BSON_CLASSES.include?(type)
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. "
warn_message += if type == BSON::Decimal128
'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
'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
ActiveDocument.logger.warn(warn_message)
end
end
end
Expand Down
Loading

0 comments on commit eb200a9

Please sign in to comment.