lib/mongoid/fields.rb
# encoding: utf-8
require "mongoid/fields/standard"
require "mongoid/fields/foreign_key"
require "mongoid/fields/localized"
require "mongoid/fields/validators"
module Mongoid
# This module defines behaviour for fields.
module Fields
extend ActiveSupport::Concern
# For fields defined with symbols use the correct class.
#
# @since 4.0.0
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,
symbol: Symbol,
time: Time
}.with_indifferent_access
# Constant for all names of the id field in a document.
#
# @since 5.0.0
IDS = [ :_id, :id, '_id', 'id' ].freeze
included do
class_attribute :aliased_fields
class_attribute :localized_fields
class_attribute :fields
class_attribute :pre_processed_defaults
class_attribute :post_processed_defaults
self.aliased_fields = { "id" => "_id" }
self.fields = {}
self.localized_fields = {}
self.pre_processed_defaults = []
self.post_processed_defaults = []
field(
:_id,
default: ->{ BSON::ObjectId.new },
pre_processed: true,
type: BSON::ObjectId
)
alias :id :_id
alias :id= :_id=
end
# Apply all default values to the document which are not procs.
#
# @example Apply all the non-proc defaults.
# model.apply_pre_processed_defaults
#
# @return [ Array<String ] The names of the non-proc defaults.
#
# @since 2.4.0
def apply_pre_processed_defaults
pre_processed_defaults.each do |name|
apply_default(name)
end
end
# Apply all default values to the document which are procs.
#
# @example Apply all the proc defaults.
# model.apply_post_processed_defaults
#
# @return [ Array<String ] The names of the proc defaults.
#
# @since 2.4.0
def apply_post_processed_defaults
post_processed_defaults.each do |name|
apply_default(name)
end
end
# Applies a single default value for the given name.
#
# @example Apply a single default.
# model.apply_default("name")
#
# @param [ String ] name The name of the field.
#
# @since 2.4.0
def apply_default(name)
unless attributes.key?(name)
if field = fields[name]
default = field.eval_default(self)
unless default.nil? || field.lazy?
attribute_will_change!(name)
attributes[name] = default
end
end
end
end
# Apply all the defaults at once.
#
# @example Apply all the defaults.
# model.apply_defaults
#
# @since 2.4.0
def apply_defaults
apply_pre_processed_defaults
apply_post_processed_defaults
end
# Returns an array of names for the attributes available on this object.
#
# Provides the field names in an ORM-agnostic way. Rails v3.1+ uses this
# method to automatically wrap params in JSON requests.
#
# @example Get the field names
# docment.attribute_names
#
# @return [ Array<String> ] The field names
#
# @since 3.0.0
def attribute_names
self.class.attribute_names
end
# Get the name of the provided field as it is stored in the database.
# Used in determining if the field is aliased or not.
#
# @example Get the database field name.
# model.database_field_name(:authorization)
#
# @param [ String, Symbol ] name The name to get.
#
# @return [ String ] The name of the field as it's stored in the db.
#
# @since 3.0.7
def database_field_name(name)
self.class.database_field_name(name)
end
# Is the provided field a lazy evaluation?
#
# @example If the field is lazy settable.
# doc.lazy_settable?(field, nil)
#
# @param [ Field ] field The field.
# @param [ Object ] value The current value.
#
# @return [ true, false ] If we set the field lazily.
#
# @since 3.1.0
def lazy_settable?(field, value)
!frozen? && value.nil? && field.lazy?
end
# Is the document using object ids?
#
# @note Refactored from using delegate for class load performance.
#
# @example Is the document using object ids?
# model.using_object_ids?
#
# @return [ true, false ] Using object ids.
def using_object_ids?
self.class.using_object_ids?
end
class << self
# Stores the provided block to be run when the option name specified is
# defined on a field.
#
# No assumptions are made about what sort of work 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::Fields.option :required do |model, field, value|
# model.validates_presence_of field if value
# end
#
# @param [ Symbol ] option_name the option name to match against
# @param [ Proc ] block the handler to execute when the option is
# provided.
#
# @since 2.1.0
def option(option_name, &block)
options[option_name] = block
end
# Return a map of custom option names to their handlers.
#
# @example
# Mongoid::Fields.options
# # => { :required => #<Proc:0x00000100976b38> }
#
# @return [ Hash ] the option map
#
# @since 2.1.0
def options
@options ||= {}
end
end
module ClassMethods
# Returns an array of names for the attributes available on this object.
#
# Provides the field names in an ORM-agnostic way. Rails v3.1+ uses this
# method to automatically wrap params in JSON requests.
#
# @example Get the field names
# Model.attribute_names
#
# @return [ Array<String> ] The field names
#
# @since 3.0.0
def attribute_names
fields.keys
end
# Get the name of the provided field as it is stored in the database.
# Used in determining if the field is aliased or not.
#
# @example Get the database field name.
# Model.database_field_name(:authorization)
#
# @param [ String, Symbol ] name The name to get.
#
# @return [ String ] The name of the field as it's stored in the db.
#
# @since 3.0.7
def database_field_name(name)
return nil unless name
normalized = name.to_s
aliased_fields[normalized] || normalized
end
# Defines all the fields that are accessible on the Document
# For each field that is defined, a getter and setter will be
# added as an instance method to the Document.
#
# @example Define a field.
# field :score, :type => Integer, :default => 0
#
# @param [ Symbol ] name The name of the field.
# @param [ Hash ] options The options to pass to the field.
#
# @option options [ Class ] :type The type of the field.
# @option options [ String ] :label The label for the field.
# @option options [ Object, Proc ] :default The field's default
#
# @return [ Field ] The generated field
def field(name, options = {})
named = name.to_s
Validators::Macro.validate(self, name, options)
added = add_field(named, options)
descendants.each do |subclass|
subclass.add_field(named, options)
end
added
end
# Replace a field with a new type.
#
# @example Replace the field.
# Model.replace_field("_id", String)
#
# @param [ String ] name The name of the field.
# @param [ Class ] type The new type of field.
#
# @return [ Serializable ] The new field.
#
# @since 2.1.0
def replace_field(name, type)
remove_defaults(name)
add_field(name, fields[name].options.merge(type: type))
end
# Convenience method for determining if we are using +BSON::ObjectIds+ as
# our id.
#
# @example Does this class use object ids?
# person.using_object_ids?
#
# @return [ true, false ] If the class uses BSON::ObjectIds for the id.
#
# @since 1.0.0
def using_object_ids?
fields["_id"].object_id_field?
end
protected
# Add the defaults to the model. This breaks them up between ones that
# are procs and ones that are not.
#
# @example Add to the defaults.
# Model.add_defaults(field)
#
# @param [ Field ] field The field to add for.
#
# @since 2.4.0
def add_defaults(field)
default, name = field.default_val, field.name.to_s
remove_defaults(name)
unless default.nil?
if field.pre_processed?
pre_processed_defaults.push(name)
else
post_processed_defaults.push(name)
end
end
end
# Define a field attribute for the +Document+.
#
# @example Set the field.
# Person.add_field(:name, :default => "Test")
#
# @param [ Symbol ] name The name of the field.
# @param [ Hash ] options The hash of options.
def add_field(name, options = {})
aliased = options[:as]
aliased_fields[aliased.to_s] = name if aliased
field = field_for(name, options)
fields[name] = field
add_defaults(field)
create_accessors(name, name, options)
create_accessors(name, aliased, options) if aliased
process_options(field)
create_dirty_methods(name, name)
create_dirty_methods(name, aliased) if aliased
field
end
# Run through all custom options stored in Mongoid::Fields.options and
# execute the handler if the option is provided.
#
# @example
# Mongoid::Fields.option :custom do
# puts "called"
# end
#
# field = Mongoid::Fields.new(:test, :custom => true)
# Person.process_options(field)
# # => "called"
#
# @param [ Field ] field the field to process
def process_options(field)
field_options = field.options
Fields.options.each_pair do |option_name, handler|
if field_options.key?(option_name)
handler.call(self, field, field_options[option_name])
end
end
end
# Create the field accessors.
#
# @example Generate the accessors.
# Person.create_accessors(:name, "name")
# person.name #=> returns the field
# person.name = "" #=> sets the field
# person.name? #=> Is the field present?
# person.name_before_type_cast #=> returns the field before type cast
#
# @param [ Symbol ] name The name of the field.
# @param [ Symbol ] meth The name of the accessor.
# @param [ Hash ] options The options.
#
# @since 2.0.0
def create_accessors(name, meth, options = {})
field = fields[name]
create_field_getter(name, meth, field)
create_field_getter_before_type_cast(name, meth)
create_field_setter(name, meth, field)
create_field_check(name, meth)
if options[:localize]
create_translations_getter(name, meth)
create_translations_setter(name, meth, field)
localized_fields[name] = field
end
end
# Create the getter method for the provided field.
#
# @example Create the getter.
# Model.create_field_getter("name", "name", field)
#
# @param [ String ] name The name of the attribute.
# @param [ String ] meth The name of the method.
# @param [ Field ] field The field.
#
# @since 2.4.0
def create_field_getter(name, meth, field)
generated_methods.module_eval do
re_define_method(meth) do
raw = read_attribute(name)
if lazy_settable?(field, raw)
write_attribute(name, field.eval_default(self))
else
value = field.demongoize(raw)
attribute_will_change!(name) if value.resizable?
value
end
end
end
end
# Create the getter_before_type_cast method for the provided field. If
# the attribute has been assigned, return the attribute before it was
# type cast. Otherwise, delegate to the getter.
#
# @example Create the getter_before_type_cast.
# Model.create_field_getter_before_type_cast("name", "name")
#
# @param [ String ] name The name of the attribute.
# @param [ String ] meth The name of the method.
#
# @since 3.1.0
def create_field_getter_before_type_cast(name, meth)
generated_methods.module_eval do
re_define_method("#{meth}_before_type_cast") do
if has_attribute_before_type_cast?(name)
read_attribute_before_type_cast(name)
else
send meth
end
end
end
end
# Create the setter method for the provided field.
#
# @example Create the setter.
# Model.create_field_setter("name", "name")
#
# @param [ String ] name The name of the attribute.
# @param [ String ] meth The name of the method.
# @param [ Field ] field The field.
#
# @since 2.4.0
def create_field_setter(name, meth, field)
generated_methods.module_eval do
re_define_method("#{meth}=") do |value|
val = write_attribute(name, value)
if field.foreign_key?
remove_ivar(field.metadata.name)
end
val
end
end
end
# Create the check method for the provided field.
#
# @example Create the check.
# Model.create_field_check("name", "name")
#
# @param [ String ] name The name of the attribute.
# @param [ String ] meth The name of the method.
#
# @since 2.4.0
def create_field_check(name, meth)
generated_methods.module_eval do
re_define_method("#{meth}?") do
attr = read_attribute(name)
attr == true || attr.present?
end
end
end
# Create the translation getter method for the provided field.
#
# @example Create the translation getter.
# Model.create_translations_getter("name", "name")
#
# @param [ String ] name The name of the attribute.
# @param [ String ] meth The name of the method.
#
# @since 2.4.0
def create_translations_getter(name, meth)
generated_methods.module_eval do
re_define_method("#{meth}_translations") do
(attributes[name] ||= {}).with_indifferent_access
end
alias_method :"#{meth}_t", :"#{meth}_translations"
end
end
# Create the translation setter method for the provided field.
#
# @example Create the translation setter.
# Model.create_translations_setter("name", "name")
#
# @param [ String ] name The name of the attribute.
# @param [ String ] meth The name of the method.
# @param [ Field ] field The field.
#
# @since 2.4.0
def create_translations_setter(name, meth, field)
generated_methods.module_eval do
re_define_method("#{meth}_translations=") do |value|
attribute_will_change!(name)
if value
value.update_values do |_value|
field.type.mongoize(_value)
end
end
attributes[name] = value
end
alias_method :"#{meth}_t=", :"#{meth}_translations="
end
end
# Include the field methods as a module, so they can be overridden.
#
# @example Include the fields.
# Person.generated_methods
#
# @return [ Module ] The module of generated methods.
#
# @since 2.0.0
def generated_methods
@generated_methods ||= begin
mod = Module.new
include(mod)
mod
end
end
# Remove the default keys for the provided name.
#
# @example Remove the default keys.
# Model.remove_defaults(name)
#
# @param [ String ] name The field name.
#
# @since 2.4.0
def remove_defaults(name)
pre_processed_defaults.delete_one(name)
post_processed_defaults.delete_one(name)
end
def field_for(name, options)
opts = options.merge(klass: self)
type_mapping = TYPE_MAPPINGS[options[:type]]
opts[:type] = type_mapping || unmapped_type(options)
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
end
end
end
end