From f28fd741cdd7fbb158a4ae0f2498d6831412b9ab Mon Sep 17 00:00:00 2001 From: shields Date: Sat, 25 Jun 2022 00:24:40 +0900 Subject: [PATCH] MONGOID-5336 User-defined symbol field types - squashed commits --- docs/reference/fields.txt | 17 +- docs/release-notes/mongoid-8.0.txt | 18 +- lib/config/locales/en.yml | 35 +++- lib/mongoid/errors.rb | 3 +- .../errors/invalid_field_type_definition.rb | 27 +++ ...id_field_type.rb => unknown_field_type.rb} | 6 +- lib/mongoid/fields.rb | 88 +++++----- lib/mongoid/fields/field_types.rb | 89 ++++++++++ .../invalid_field_type_definition_spec.rb | 57 ++++++ ...ype_spec.rb => unknown_field_type_spec.rb} | 18 +- spec/mongoid/fields/field_types_spec.rb | 148 ++++++++++++++++ spec/mongoid/fields_spec.rb | 165 ++++++++++-------- 12 files changed, 542 insertions(+), 129 deletions(-) create mode 100644 lib/mongoid/errors/invalid_field_type_definition.rb rename lib/mongoid/errors/{invalid_field_type.rb => unknown_field_type.rb} (81%) create mode 100644 lib/mongoid/fields/field_types.rb create mode 100644 spec/mongoid/errors/invalid_field_type_definition_spec.rb rename spec/mongoid/errors/{invalid_field_type_spec.rb => unknown_field_type_spec.rb} (63%) create mode 100644 spec/mongoid/fields/field_types_spec.rb diff --git a/docs/reference/fields.txt b/docs/reference/fields.txt index 74ba19efee..184a648bd1 100644 --- a/docs/reference/fields.txt +++ b/docs/reference/fields.txt @@ -913,6 +913,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::Fields.configure do + define_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: @@ -1008,8 +1019,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 f1a89c1e85..6ea75e532b 100644 --- a/docs/release-notes/mongoid-8.0.txt +++ b/docs/release-notes/mongoid-8.0.txt @@ -289,7 +289,7 @@ Mongoid 8 behavior: include Mongoid::Document field :name, type: :bogus - # => raises Mongoid::Errors::InvalidFieldType + # => raises Mongoid::Errors::UnknownFieldType end Mongoid 7 behavior: @@ -305,6 +305,22 @@ Mongoid 7 behavior: 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 + define_type :point, Point + end + +Refer to the :ref:`docs ` for details. + + Removed ``:drop_dups`` Option from Indexes ------------------------------------------ diff --git a/lib/config/locales/en.yml b/lib/config/locales/en.yml index eaed9a3ada..1867c92fd8 100644 --- a/lib/config/locales/en.yml +++ b/lib/config/locales/en.yml @@ -189,8 +189,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 @@ -198,13 +200,14 @@ 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_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. + 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://docs.mongodb.com/mongoid/current/reference/fields/#using-symbols-or-strings-instead-of-classes" + 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 @@ -584,6 +587,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::Fields.configure do\n + \_\_\_\_define_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 diff --git a/lib/mongoid/errors.rb b/lib/mongoid/errors.rb index 89b3a33caa..2390254f07 100644 --- a/lib/mongoid/errors.rb +++ b/lib/mongoid/errors.rb @@ -14,7 +14,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_includes" require "mongoid/errors/invalid_index" @@ -56,6 +56,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" diff --git a/lib/mongoid/errors/invalid_field_type_definition.rb b/lib/mongoid/errors/invalid_field_type_definition.rb new file mode 100644 index 0000000000..5e8951f9b3 --- /dev/null +++ b/lib/mongoid/errors/invalid_field_type_definition.rb @@ -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 diff --git a/lib/mongoid/errors/invalid_field_type.rb b/lib/mongoid/errors/unknown_field_type.rb similarity index 81% rename from lib/mongoid/errors/invalid_field_type.rb rename to lib/mongoid/errors/unknown_field_type.rb index 6b6c8959b4..921425a230 100644 --- a/lib/mongoid/errors/invalid_field_type.rb +++ b/lib/mongoid/errors/unknown_field_type.rb @@ -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 diff --git a/lib/mongoid/fields.rb b/lib/mongoid/fields.rb index 5a9e436b4a..d2a7d95acd 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 @@ -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. # @@ -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 @@ -274,6 +257,33 @@ def validate_writable_field_name!(name) 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 + # define_type :point, Point + # end + # + # @param [ Symbol | String ] field_type the identifier of the + # defined type. This identifier will be accessible as either a + # string or a symbol 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 define_type(field_type, klass) + Fields::FieldTypes.define_type(field_type, klass) + end + # Stores the provided block to be run when the option name specified is # defined on a field. # @@ -282,8 +292,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 @@ -767,32 +779,28 @@ 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) - if !opts[:type].is_a?(Class) - raise Errors::InvalidFieldType.new(self, name, options[:type]) - else - if INVALID_BSON_CLASSES.include?(opts[:type]) - warn_message = "Using #{opts[:type]} as the field type is not supported. " - if opts[: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 - 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) + if type = options[:type] + type = Fields::FieldTypes.get(type) + unless type + raise Mongoid::Errors::UnknownFieldType.new(self.name, name, type) end + opts[:type] = type + warn_unsupported_bson_type(type) end 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 + def warn_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 - options[: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 diff --git a/lib/mongoid/fields/field_types.rb b/lib/mongoid/fields/field_types.rb new file mode 100644 index 0000000000..c05b7fa82f --- /dev/null +++ b/lib/mongoid/fields/field_types.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +module Mongoid + module Fields + + # Singleton module which contains a mapping of field types to field class. + # Custom field types can be configured. + # + # @api private + module FieldTypes + + # The default mapping of field type symbol/string identifiers to classes. + # + # @api private + DEFAULT_MAPPING = { + array: Array, + big_decimal: BigDecimal, + binary: BSON::Binary, + boolean: Mongoid::Boolean, + date: Date, + date_time: DateTime, + #decimal128: BSON::Decimal128, + 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 + + class << self + + # Resolves the user-provided field type to the field type class. + # + # @example + # Mongoid::FieldTypes.get(:point) + # + # @param [ Module | Symbol | String ] field_type The field + # type class or its string or symbol identifier. + # + # @return [ Module | nil ] The underlying field type class, or nil if + # string or symbol was passed and it is not mapped to any class. + def get(field_type) + case field_type + when Module + field_type + when Symbol, String + mapping[field_type] + else + raise Mongoid::Errors::UnknownFieldType.new(self.name, field, field_type) + end + end + + # Defines a field type mapping, for later use in field :type option. + # + # @example + # Mongoid::FieldTypes.define_type(:point, Point) + # + # @param [ Symbol | String ] field_type the identifier of the + # defined type. This identifier will be accessible as either a + # string or a symbol regardless of the type passed to this method. + # @param [ Class ] klass the class of the defined type, which must + # include mongoize, demongoize, and evolve methods. + def define_type(field_type, klass) + unless (field_type.is_a?(String) || field_type.is_a?(Symbol)) && klass.is_a?(Module) + raise Mongoid::Errors::InvalidFieldTypeDefinition.new(field_type, klass) + end + mapping[field_type] = klass + end + + delegate :delete, to: :mapping + + # The memoized mapping of field type definitions to classes. + # + # @return [ ActiveSupport::HashWithIndifferentAccess ] The memoized field mapping. + def mapping + @mapping ||= DEFAULT_MAPPING.dup + end + end + end + end +end diff --git a/spec/mongoid/errors/invalid_field_type_definition_spec.rb b/spec/mongoid/errors/invalid_field_type_definition_spec.rb new file mode 100644 index 0000000000..887fe32412 --- /dev/null +++ b/spec/mongoid/errors/invalid_field_type_definition_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Mongoid::Errors::InvalidFieldTypeDefinition do + + describe "#message" do + + context 'when field_type is the wrong type' do + let(:error) do + described_class.new(123, Integer) + end + + it "contains the problem in the message" do + expect(error.message).to include( + "The field type definition of 123 to Integer is invalid." + ) + end + + it "contains the summary in the message" do + expect(error.message).to include( + "In the field type definition, either field_type 123 is not a Symbol" + ) + end + + it "contains the resolution in the message" do + expect(error.message).to include( + 'Please ensure you are specifying field_type as either a Symbol' + ) + end + end + + context 'when klass is the wrong type' do + let(:error) do + described_class.new('number', 123) + end + + it "contains the problem in the message" do + expect(error.message).to include( + 'The field type definition of "number" to 123 is invalid.' + ) + end + + it "contains the summary in the message" do + expect(error.message).to include( + 'In the field type definition, either field_type "number" is not a Symbol' + ) + end + + it "contains the resolution in the message" do + expect(error.message).to include( + 'Please ensure you are specifying field_type as either a Symbol' + ) + end + end + end +end diff --git a/spec/mongoid/errors/invalid_field_type_spec.rb b/spec/mongoid/errors/unknown_field_type_spec.rb similarity index 63% rename from spec/mongoid/errors/invalid_field_type_spec.rb rename to spec/mongoid/errors/unknown_field_type_spec.rb index f903f5d50b..7fdac53e14 100644 --- a/spec/mongoid/errors/invalid_field_type_spec.rb +++ b/spec/mongoid/errors/unknown_field_type_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -describe Mongoid::Errors::InvalidFieldType do +describe Mongoid::Errors::UnknownFieldType do describe "#message" do @@ -17,13 +17,19 @@ it "contains the problem in the message" do expect(error.message).to include( - "Invalid field type :stringgy for field 'first_name' on model 'Person'." + "Unknown 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." + "Model 'Person' declares 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 known type value for the field.' ) end end @@ -35,20 +41,20 @@ it "contains the problem in the message" do expect(error.message).to include( - %q,Invalid field type "stringgy" for field 'first_name' on model 'Person'., + %q,Unknown field type "stringgy" for field 'first_name' on model 'Person'., ) end it "contains the summary in the message" do expect(error.message).to include( - %q,Model 'Person' defines a field 'first_name' with an unknown type value "stringgy"., + %q,Model 'Person' declares a field 'first_name' with an unknown type value "stringgy"., ) end end it "contains the resolution in the message" do expect(error.message).to include( - 'Please provide a valid type value for the field.' + 'Please provide a known type value for the field.' ) 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..4a00243b0f --- /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 + + around do |example| + described_class.instance_variable_set(:@mapping, described_class::DEFAULT_ALIASES.dup) + example.run + described_class.instance_variable_set(:@mapping, described_class::DEFAULT_ALIASES.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 + 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_ALIASES constant' do + described_class.define(:integer, String) + expect(described_class::DEFAULT_ALIASES[: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_ALIASES constant' do + described_class.delete(:integer) + expect(described_class::DEFAULT_ALIASES[:integer]).to eq Integer + end + end + + describe '.mapping' do + + it 'returns the default mapping by default' do + expect(described_class.mapping).to eq described_class::DEFAULT_ALIASES + end + + it 'can add a type' do + described_class.define(:my_string, String) + expect(described_class.mapping[:my_string]).to eq(String) + end + + it 'can delete a default type' do + described_class.delete(:integer) + expect(described_class.mapping).to_not have_key(:integer) + end + end +end diff --git a/spec/mongoid/fields_spec.rb b/spec/mongoid/fields_spec.rb index 5539fc0ab8..0e5764257c 100644 --- a/spec/mongoid/fields_spec.rb +++ b/spec/mongoid/fields_spec.rb @@ -330,83 +330,51 @@ 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, + big_decimal: BigDecimal, + binary: BSON::Binary, + boolean: Mongoid::Boolean, + date: Date, + date_time: DateTime, + #decimal128: BSON::Decimal128, + 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 context 'when using an unknown symbol' do - it 'raises InvalidFieldType' do + it 'raises UnknownFieldType' do lambda do - klass.field(:test, type: :bogus) - end.should raise_error(Mongoid::Errors::InvalidFieldType, /defines a field 'test' with an unknown type value :bogus/) + klass.field(:test, type: :bogus) + end.should raise_error(Mongoid::Errors::UnknownFieldType, /declares a field 'test' with an unknown type value :bogus/) end end context 'when using an unknown string' do - it 'raises InvalidFieldType' do + it 'raises UnknownFieldType' do lambda do - klass.field(:test, type: 'bogus') - end.should raise_error(Mongoid::Errors::InvalidFieldType, /defines a field 'test' with an unknown type value "bogus"/) + klass.field(:test, type: 'bogus') + end.should raise_error(Mongoid::Errors::UnknownFieldType, /declares a field 'test' with an unknown type value "bogus"/) end end end @@ -1812,4 +1780,65 @@ class DiscriminatorChild2 < DiscriminatorParent end end end + + describe '.configure DSL' do + + context '.type method' do + around do |example| + klass = Mongoid::Fields::FieldTypes + klass.instance_variable_set(:@mapping, klass::DEFAULT_ALIASES.dup) + example.run + klass.instance_variable_set(:@mapping, klass::DEFAULT_ALIASES.dup) + end + + it 'can define a custom type' do + described_class.configure do + define_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 + define_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 + + describe '::TYPE_MAPPINGS' do + it 'returns the default mapping' do + expect(described_class::TYPE_MAPPINGS).to eq ::Mongoid::Fields::FieldTypes::DEFAULT_ALIASES + end + end end