From 22222c62737b07bd5922ac8c1464b17209b0f5e8 Mon Sep 17 00:00:00 2001 From: shields Date: Tue, 5 Apr 2022 22:37:05 +0900 Subject: [PATCH] - Prefer defining field type value as a Symbol rather than a Class. (Symbol is already supported today.) - Deprecate using field type as a Class. - Add ability to define custom field types using a mini DSL (Mongoid::Fields.configure) - Fix Mongoid::Fields.option documentation --- docs/reference/fields.txt | 125 ++++++++------- docs/release-notes/mongoid-8.0.txt | 37 +++++ lib/config/locales/en.yml | 22 ++- lib/mongoid/errors.rb | 1 + lib/mongoid/errors/invalid_field_type.rb | 25 +++ lib/mongoid/fields.rb | 63 ++++---- lib/mongoid/fields/field_types.rb | 83 ++++++++++ .../mongoid/errors/invalid_field_type_spec.rb | 31 ++++ spec/mongoid/fields/field_types_spec.rb | 148 ++++++++++++++++++ spec/mongoid/fields_spec.rb | 148 ++++++++++-------- 10 files changed, 535 insertions(+), 148 deletions(-) create mode 100644 lib/mongoid/errors/invalid_field_type.rb create mode 100644 lib/mongoid/fields/field_types.rb create mode 100644 spec/mongoid/errors/invalid_field_type_spec.rb create mode 100644 spec/mongoid/fields/field_types_spec.rb diff --git a/docs/reference/fields.txt b/docs/reference/fields.txt index 0c96e7b988..3160b672cb 100644 --- a/docs/reference/fields.txt +++ b/docs/reference/fields.txt @@ -55,34 +55,40 @@ on a person by using the ``field`` macro. class Person include Mongoid::Document - field :name, type: String - field :date_of_birth, type: Date - field :weight, type: Float + field :name, type: :string + field :date_of_birth, type: :date + field :weight, type: :float end Below is a list of valid types for fields. -- ``Array`` -- ``BigDecimal`` -- ``Boolean`` -- ``Date`` -- ``DateTime`` -- ``Float`` -- ``Hash`` -- ``Integer`` -- ``BSON::ObjectId`` -- ``BSON::Binary`` -- ``Range`` -- ``Regexp`` -- ``Set`` -- ``String`` -- ``StringifiedSymbol`` -- ``Symbol`` -- ``Time`` -- ``TimeWithZone`` +- ``:array`` +- ``:big_decimal`` +- ``:boolean`` +- ``:date`` +- ``:date_time`` +- ``:decimal128`` (uses ``BSON::Decimal128``) +- ``:float`` +- ``:hash`` +- ``:integer`` +- ``:object_id`` (uses ``BSON::ObjectID``) +- ``:binary`` (uses ``BSON::Binary``) +- ``:range`` +- ``:regexp`` +- ``:set`` +- ``:string`` +- ``:stringified_symbol`` (see below) +- ``:symbol`` +- ``:time`` +- ``:time_with_zone`` To define custom field types, refer to :ref:`Custom Field Types ` below. +As of Mongoid 8.0, ``field :type`` should be specified as a ``Symbol``. +Specifying as a ``Class`` is deprecated and will be no longer supported in a +future major version of Mongoid. Unrecognized field type symbols will result +in an `InvalidFieldType` error when the model class is loaded. + .. _omitting-field-type-definition: @@ -116,16 +122,16 @@ Types that are not supported as dynamic attributes since they cannot be cast are .. _field-type-stringified-symbol: -Field Type: StringifiedSymbol ------------------------------ +Field Type :stringified_symbol +------------------------------ -The ``StringifiedSymbol`` field type is the recommended field type for storing -values that should be exposed as symbols to Ruby applications. When using the ``Symbol`` field type, +The ``:stringified_symbol`` field type is the recommended field type for storing +values that should be exposed as symbols to Ruby applications. When using the ``:symbol`` field type, Mongoid defaults to storing values as BSON symbols. For more information on the BSON symbol type, see :ref:`here `. However, the BSON symbol type is deprecated and is difficult to work with in programming languages -without native symbol types, so the ``StringifiedSymbol`` type allows the use of symbols -while ensuring interoperability with other drivers. The ``StringifiedSymbol`` type stores all data +without native symbol types, so the ``:stringified_symbol`` type allows the use of symbols +while ensuring interoperability with other drivers. The ``:stringified_symbol`` type stores all data on the database as strings, while exposing values to the application as symbols. An example usage is shown below: @@ -170,15 +176,15 @@ migration from fields that currently store either strings or BSON symbols in the .. _field-type-symbol: -Field Type: Symbol +Field Type :symbol ------------------ -New applications should use the :ref:`StringifiedSymbol field type ` -to store Ruby symbols in the database. The ``StringifiedSymbol`` field type +New applications should use the :ref:`:stringified_symbol field type ` +to store Ruby symbols in the database. The ``:stringified_symbol`` field type provides maximum compatibility with other applications and programming languages and has the same behavior in all circumstances. -Mongoid also provides the deprecated ``Symbol`` field type for serializing +Mongoid also provides the deprecated ``:symbol`` field type for serializing Ruby symbols to BSON symbols. Because the BSON specification deprecated the BSON symbol type, the `bson` gem will serialize Ruby symbols into BSON strings when used on its own. However, in order to maintain backwards compatibility @@ -201,10 +207,10 @@ snippet in your project: .. _field-type-hash: -Field Type: Hash +Field Type :hash ---------------- -When using a field of type Hash, be wary of adhering to the +When using a field of type ``:hash``, be wary of adhering to the `legal key names for mongoDB `_, or else the values will not store properly. @@ -213,7 +219,7 @@ or else the values will not store properly. class Person include Mongoid::Document field :first_name - field :url, type: Hash + field :url, type: :hash # will update the fields properly and save the values def set_vals @@ -233,21 +239,21 @@ or else the values will not store properly. .. _field-type-time: -Field Type: Time +Field Type :time ---------------- -``Time`` fields store values as ``Time`` instances in the :ref:`configured +``:time`` fields store values as ``Time`` instances in the :ref:`configured time zone `. ``Date`` and ``DateTime`` instances are converted to ``Time`` instances upon -assignment to a ``Time`` field: +assignment to a ``:time`` field: .. code-block:: ruby class Voter include Mongoid::Document - - field :registered_at, type: Time + + field :registered_at, type: :time end Voter.new(registered_at: Date.today) @@ -259,10 +265,10 @@ local time, because the application was not configured to use UTC times. .. _field-type-date: -Field Type: Date +Field Type :date ---------------- -Mongoid allows assignment of values of several types to ``Date`` fields: +Mongoid allows assignment of values of several types to ``:date`` fields: - ``Date`` - the provided date is stored as is. - ``Time``, ``DateTime``, ``ActiveSupport::TimeWithZone`` - the date component @@ -280,20 +286,20 @@ As a date & time to date conversion is lossy (it discards the time component), especially if an application operates with times in different time zones it is recommended to explicitly convert ``String``, ``Time`` and ``DateTime`` objects to ``Date`` objects before assigning the values to fields of type -``Date``. +``:date``. .. _field-type-date-time: -Field Type: DateTime +Field Type :date_time --------------------- MongoDB stores all times as UTC timestamps. When assigning a value to a -``DateTime`` field, or when querying a ``DateTime`` field, Mongoid +``:date_time`` field, or when querying a ``:date_time`` field, Mongoid converts the passed in value to a UTC ``Time`` before sending it to the MongoDB server. -``Time``, ``ActiveSupport::TimeWithZone`` and ``DateTime`` objects embed +``Time``, ``ActiveSupport::TimeWithZone``, and ``DateTime`` objects embed time zone information, and the value persisted is the specified moment in time, in UTC. When the value is retrieved, the time zone in which it is returned is defined by the :ref:`configured time zone settings `. @@ -302,7 +308,7 @@ returned is defined by the :ref:`configured time zone settings `. class Ticket include Mongoid::Document - field :opened_at, type: DateTime + field :opened_at, type: :date_time end Mongoid.use_activesupport_time_zone = true @@ -332,7 +338,7 @@ doing so, the integers/floats are assumed to be Unix timestamps (in UTC): ticket.opened_at # => Fri, 14 Dec 2018 16:12:54 +0000 -If a string is used as a ``DateTime`` field value, the behavior depends on +If a ``String`` is used as a ``:date_time`` field value, the behavior depends on whether the string includes a time zone. If no time zone is specified, the :ref:`default Mongoid time zone ` is used: @@ -354,7 +360,7 @@ If a time zone is specified, it is respected: .. _field-type-regexp: -Field Type: Regexp +Field Type :regexp ------------------ MongoDB supports storing regular expressions in documents, and querying using @@ -365,7 +371,7 @@ fork of `Oniguruma regular expression engine The two regular expression implementations generally provide equivalent functionality but have several important syntax differences. -When a field is declared to be of type Regexp, Mongoid converts Ruby regular +When a field is declared to be of type ``:regexp``, Mongoid converts Ruby regular expressions to BSON regular expressions and stores the result in MongoDB. Retrieving the field from the database produces a ``BSON::Regexp::Raw`` instance: @@ -375,7 +381,7 @@ instance: class Token include Mongoid::Document - field :pattern, type: Regexp + field :pattern, type: :regexp end token = Token.create!(pattern: /hello.world/m) @@ -847,9 +853,20 @@ can use in our model class as follows: class Profile include Mongoid::Document - field :location, type: Point + field :location, type: :point + end + +First, declare the new field type mapping in an initializer: + +.. code-block:: ruby + + # in /config/initializers/mongoid_custom_fields.rb + + Mongoid::Fields.configure do + 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: @@ -945,8 +962,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: diff --git a/docs/release-notes/mongoid-8.0.txt b/docs/release-notes/mongoid-8.0.txt index ef2ddfce8d..cd1089093a 100644 --- a/docs/release-notes/mongoid-8.0.txt +++ b/docs/release-notes/mongoid-8.0.txt @@ -276,3 +276,40 @@ as well as ActiveRecord-compatible ``previously_new_record?`` and user.destroy user.previously_persisted? # => true + + +Setting Field Type as a Class is Deprecated +------------------------------------------- + +Mongoid has historically supported defining the ``field :type`` option +as either a Symbol or a Class. As of Mongoid 8.0, using a Class is deprecated +and Symbol is preferred. Support for ``field :type`` as a Class will be +removed in a future major version of Mongoid. + +.. code-block:: ruby + + class Person + include Mongoid::Document + + # Deprecated; will log an warning message. + field :first_name, type: String + + # Good + field :last_name, type: :string + 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 + type :point, Point + end + +Refer to the :ref:`docs ` for details. diff --git a/lib/config/locales/en.yml b/lib/config/locales/en.yml index 4942a5431d..0e00a818dc 100644 --- a/lib/config/locales/en.yml +++ b/lib/config/locales/en.yml @@ -172,8 +172,10 @@ 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 @@ -181,6 +183,22 @@ en: \_\_end\n\n Refer to: https://docs.mongodb.com/mongoid/current/reference/fields/#custom-field-options" + invalid_field_type: + message: "Invalid field type :%{type} for field :%{field} on model '%{klass}'." + summary: "Model '%{klass}' defines a field :%{field} with an unknown :type value + :%{type}. This value is neither present in Mongoid's default type mapping, + nor defined in a custom field type mapping." + resolution: "Please provide a valid :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 + \_\_\_\_type :%{type}, YourTypeClass + \_\_end\n + \_\_class %{klass}\n + \_\_\_\_include Mongoid::Document\n + \_\_\_\_field :%{field}, type: :%{type}\n + \_\_end\n\n + Refer to: + 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 diff --git a/lib/mongoid/errors.rb b/lib/mongoid/errors.rb index e530c54f88..dc36124e76 100644 --- a/lib/mongoid/errors.rb +++ b/lib/mongoid/errors.rb @@ -15,6 +15,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_find" require "mongoid/errors/invalid_includes" require "mongoid/errors/invalid_index" diff --git a/lib/mongoid/errors/invalid_field_type.rb b/lib/mongoid/errors/invalid_field_type.rb new file mode 100644 index 0000000000..403866d254 --- /dev/null +++ b/lib/mongoid/errors/invalid_field_type.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Mongoid + 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 + + # Create the new error. + # + # @example Instantiate the error. + # InvalidFieldType.new('Person', 'first_name', 'stringgy') + # + # @param [ String ] klass The model class. + # @param [ String ] field The field on which the invalid type is used. + # @param [ String ] type The value of the field :type option. + def initialize(klass, field, type) + super( + compose_message('invalid_field_type', { klass: klass, field: field, type: type }) + ) + end + end + end +end diff --git a/lib/mongoid/fields.rb b/lib/mongoid/fields.rb index f9efa2d500..f8ff0349be 100644 --- a/lib/mongoid/fields.rb +++ b/lib/mongoid/fields.rb @@ -4,6 +4,7 @@ require "mongoid/fields/foreign_key" require "mongoid/fields/localized" require "mongoid/fields/validators" +require "mongoid/fields/field_types" module Mongoid @@ -11,30 +12,10 @@ module Mongoid module Fields extend ActiveSupport::Concern + # @deprecated Remove class aliases in Mongoid 8.0. 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 - # Constant for all names of the _id field in a document. # # This does not include aliases of _id field. @@ -241,6 +222,27 @@ def using_object_ids? 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 + # type :point, Point + # end + def type(symbol, klass) + Fields::FieldTypes.define(symbol, klass) + end + # Stores the provided block to be run when the option name specified is # defined on a field. # @@ -249,8 +251,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 @@ -729,19 +733,16 @@ 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) + opts[:type] = field_type_klass_for(name, options[:type]) 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 - else - options[:type] || Object - end + def field_type_klass_for(field, type) + klass = Fields::FieldTypes.get(type) + return klass if klass + raise Mongoid::Errors::InvalidFieldType.new(self.name, field, type) end end end diff --git a/lib/mongoid/fields/field_types.rb b/lib/mongoid/fields/field_types.rb new file mode 100644 index 0000000000..0ea0db02f4 --- /dev/null +++ b/lib/mongoid/fields/field_types.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +module Mongoid + module Fields + + # Singleton module which contains a cache for field type definitions. + # Custom field types can be configured. + module FieldTypes + extend self + + # For fields defined with symbols use the correct class. + DEFAULT_MAPPING = { + array: Array, + bigdecimal: BigDecimal, + big_decimal: BigDecimal, + binary: BSON::Binary, + boolean: Mongoid::Boolean, + date: Date, + datetime: DateTime, + date_time: DateTime, + decimal128: BSON::Decimal128, + double: Float, + float: Float, + hash: Hash, + integer: Integer, + object: Object, + object_id: BSON::ObjectId, + range: Range, + regexp: Regexp, + set: Set, + string: String, + stringified_symbol: Mongoid::StringifiedSymbol, + symbol: Symbol, + time: Time, + time_with_zone: ActiveSupport::TimeWithZone + }.with_indifferent_access.freeze + + def get(value) + value = value.to_sym if value.is_a?(String) + mapping[value] || handle_unmapped_type(value) + end + + def define(symbol, klass) + mapping[symbol.to_sym] = klass + end + + delegate :delete, to: :mapping + + private + + def mapping + @mapping ||= DEFAULT_MAPPING.dup + end + + def handle_unmapped_type(type) + return Object if type.nil? + + if type.is_a?(Module) + warn_class_type(type.name) + return Mongoid::Boolean if type.to_s == 'Boolean' + return type + end + + nil + end + + def warn_class_type(type) + return if warned_class_types.include?(type) + symbol = type.demodulize.underscore + Mongoid.logger.warn( + "Using a Class (#{type}) in the field :type option is deprecated " + + "and will be removed in a future major Mongoid version. " + + "Please use a Symbol (:#{symbol}) instead." + ) + warned_class_types << type + end + + def warned_class_types + @warned_class_types ||= [] + end + end + end +end diff --git a/spec/mongoid/errors/invalid_field_type_spec.rb b/spec/mongoid/errors/invalid_field_type_spec.rb new file mode 100644 index 0000000000..97aec11bdb --- /dev/null +++ b/spec/mongoid/errors/invalid_field_type_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Mongoid::Errors::InvalidFieldType do + + describe "#message" do + + let(:error) do + described_class.new(Person, :first_name, :stringgy) + end + + it "contains the problem in the message" do + expect(error.message).to include( + "Invalid field type :stringgy for field :first_name on model 'Person'." + ) + end + + it "contains the summary in the message" do + expect(error.message).to include( + "Model 'Person' defines a field :first_name with an unknown :type value :stringgy." + ) + end + + it "contains the resolution in the message" do + expect(error.message).to include( + 'Please provide a valid :type value for the field. If you meant to define' + ) + end + end +end diff --git a/spec/mongoid/fields/field_types_spec.rb b/spec/mongoid/fields/field_types_spec.rb new file mode 100644 index 0000000000..cf8e32c026 --- /dev/null +++ b/spec/mongoid/fields/field_types_spec.rb @@ -0,0 +1,148 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Mongoid::Fields::FieldTypes do + + after do + described_class.instance_variable_set(:@mapping, described_class::DEFAULT_MAPPING.dup) + end + + describe '.get' do + subject { described_class.get(type) } + + context 'when value is a default mapped symbol' do + let(:type) { :float } + + it 'uses the default mapped type' do + is_expected.to eq Float + end + end + + context 'when value is a default mapped string' do + let(:type) { 'double' } + + it 'uses the default mapped type' do + is_expected.to eq Float + end + end + + context 'when value is a custom mapped symbol' do + before { described_class.define('number', Integer) } + let(:type) { :number } + + it 'uses the custom mapped type' do + is_expected.to eq Integer + end + end + + context 'when value is a custom mapped string' do + before { described_class.define(:number, Float) } + let(:type) { 'number' } + + it 'uses the custom mapped type' do + is_expected.to eq Float + end + end + + context 'when value is an unmapped symbol' do + let(:type) { :my_value } + + it 'returns nil' do + is_expected.to eq nil + end + end + + context 'when value is a unmapped string' do + let(:type) { 'my_value' } + + it 'returns nil' do + is_expected.to eq nil + end + end + + context 'when value is a module' do + let(:type) { String } + + it 'uses the module type' do + is_expected.to eq String + end + + context 'deprecation' do + around do |example| + old_types = described_class.instance_variable_get(:@warned_class_types) + described_class.instance_variable_set(:@warned_class_types, []) + example.run + described_class.instance_variable_set(:@warned_class_types, old_types) + end + + it 'warns deprecation for the class type once' do + expect(Mongoid.logger).to receive(:warn).once.with(match(/\AUsing a Class \(String\)/)) + described_class.get(String) + described_class.get(String) + expect(Mongoid.logger).to receive(:warn).once.with(match(/\AUsing a Class \(Integer\)/)) + described_class.get(Integer) + described_class.get(String) + described_class.get(Integer) + end + end + end + + context 'when value is the module Boolean' do + let(:type) do + stub_const('Boolean', Module.new) + Boolean + end + + it 'returns Mongoid::Boolean type' do + is_expected.to eq Mongoid::Boolean + end + end + + context 'when value is nil' do + let(:type) { nil } + + it 'returns Object type' do + is_expected.to eq Object + end + end + end + + describe '.define' do + + it 'can define a new type' do + described_class.define(:my_string, String) + expect(described_class.get(:my_string)).to eq String + end + + it 'can override a default type' do + described_class.define(:integer, String) + expect(described_class.get(:integer)).to eq String + end + + it 'does not alter the DEFAULT_MAPPING constant' do + described_class.define(:integer, String) + expect(described_class::DEFAULT_MAPPING[:integer]).to eq Integer + end + end + + describe '.delete' do + + it 'can delete a custom type' do + described_class.define(:my_string, String) + expect(described_class.get(:my_string)).to eq String + described_class.delete('my_string') + expect(described_class.get(:my_string)).to eq nil + end + + it 'can delete a default type' do + described_class.delete(:integer) + expect(described_class.get(:integer)).to eq nil + end + + it 'does not alter the DEFAULT_MAPPING constant' do + described_class.delete(:integer) + expect(described_class::DEFAULT_MAPPING[:integer]).to eq Integer + end + end +end diff --git a/spec/mongoid/fields_spec.rb b/spec/mongoid/fields_spec.rb index 7798f8fc11..72b4857554 100644 --- a/spec/mongoid/fields_spec.rb +++ b/spec/mongoid/fields_spec.rb @@ -330,68 +330,39 @@ end end - it "converts :array to Array" do - expect(klass.field(:test, type: :array).type).to be(Array) - end - - it "converts :big_decimal to BigDecimal" do - expect(klass.field(:test, type: :big_decimal).type).to be(BigDecimal) - end - - it "converts :binary to BSON::Binary" do - expect(klass.field(:test, type: :binary).type).to be(BSON::Binary) - end - - it "converts :boolean to Mongoid::Boolean" do - expect(klass.field(:test, type: :boolean).type).to be(Mongoid::Boolean) - end - - it "converts :date to Date" do - expect(klass.field(:test, type: :date).type).to be(Date) - end - - it "converts :date_time to DateTime" do - expect(klass.field(:test, type: :date_time).type).to be(DateTime) - end - - it "converts :float to Float" do - expect(klass.field(:test, type: :float).type).to be(Float) - end - - it "converts :hash to Hash" do - expect(klass.field(:test, type: :hash).type).to be(Hash) - end - - it "converts :integer to Integer" do - expect(klass.field(:test, type: :integer).type).to be(Integer) - end - - it "converts :object_id to BSON::ObjectId" do - expect(klass.field(:test, type: :object_id).type).to be(BSON::ObjectId) - end - - it "converts :range to Range" do - expect(klass.field(:test, type: :range).type).to be(Range) - end - - it "converts :regexp to Rexegp" do - expect(klass.field(:test, type: :regexp).type).to be(Regexp) - end - - it "converts :set to Set" do - expect(klass.field(:test, type: :set).type).to be(Set) - end - - it "converts :string to String" do - expect(klass.field(:test, type: :string).type).to be(String) - end - - it "converts :symbol to Symbol" do - expect(klass.field(:test, type: :symbol).type).to be(Symbol) - end - - it "converts :time to Time" do - expect(klass.field(:test, type: :time).type).to be(Time) + { + array: Array, + bigdecimal: BigDecimal, + big_decimal: BigDecimal, + binary: BSON::Binary, + boolean: Mongoid::Boolean, + date: Date, + datetime: DateTime, + date_time: DateTime, + decimal128: BSON::Decimal128, + double: Float, + float: Float, + hash: Hash, + integer: Integer, + object: Object, + object_id: BSON::ObjectId, + range: Range, + regexp: Regexp, + set: Set, + string: String, + stringified_symbol: Mongoid::StringifiedSymbol, + symbol: Symbol, + time: Time, + time_with_zone: ActiveSupport::TimeWithZone + }.each do |field_type, field_klass| + + it "converts Symbol :#{field_type} to #{field_klass}" do + expect(klass.field(:test, type: field_type).type).to be(field_klass) + end + + it "converts String \"#{field_type}\" to #{field_klass}" do + expect(klass.field(:test, type: field_type.to_s).type).to be(field_klass) + end end end @@ -1796,4 +1767,57 @@ class DiscriminatorChild2 < DiscriminatorParent end end end + + describe '.configure DSL' do + + context '.type method' do + after do + klass = Mongoid::Fields::FieldTypes + klass.instance_variable_set(:@mapping, klass::DEFAULT_MAPPING.dup) + end + + it 'can define a custom type' do + described_class.configure do + type :my_type, Integer + end + + expect(described_class::FieldTypes.get(:my_type)).to eq Integer + end + + it 'can override and existing type' do + described_class.configure do + type :integer, String + end + + expect(described_class::FieldTypes.get(:integer)).to eq String + end + end + + context '.option method' do + after do + described_class.instance_variable_set(:@options, {}) + end + + it 'can define a custom field option' do + described_class.configure do + option :my_required do |model, field, value| + model.validates_presence_of field.name if value + end + end + + klass = Class.new do + include Mongoid::Document + field :my_field, my_required: true + + def self.model_name + OpenStruct.new(human: 'Klass') + end + end + + instance = klass.new + expect(instance.valid?).to eq false + expect(instance.errors.full_messages).to eq ["My field can't be blank"] + end + end + end end