diff --git a/activemodel/lib/active_model/attributes.rb b/activemodel/lib/active_model/attributes.rb
index 5e1a22da82..92f54dc4a9 100644
--- a/activemodel/lib/active_model/attributes.rb
+++ b/activemodel/lib/active_model/attributes.rb
@@ -4,7 +4,30 @@ require "active_model/attribute_set"
require "active_model/attribute/user_provided_default"
module ActiveModel
- module Attributes # :nodoc:
+ # The Attributes module allows models to define attributes beyond simple Ruby
+ # readers and writers. Similar to Active Record attributes, which are
+ # typically inferred from the database schema, Active Model Attributes are
+ # aware of data types, can have default values, and can handle casting and
+ # serialization.
+ #
+ # To use Attributes, include the module in your model class and define your
+ # attributes using the +attribute+ macro. It accepts a name, a type, a default
+ # value, and any other options supported by the attribute type.
+ #
+ # ==== Examples
+ #
+ # class Person
+ # include ActiveModel::Attributes
+ #
+ # attribute :name, :string
+ # attribute :active, :boolean, default: true
+ # end
+ #
+ # person = Person.new(name: "Volmer")
+ #
+ # person.name # => "Volmer"
+ # person.active # => true
+ module Attributes
extend ActiveSupport::Concern
include ActiveModel::AttributeMethods
@@ -16,6 +39,21 @@ module ActiveModel
end
module ClassMethods
+ # Defines a model attribute. In addition to the attribute name, a cast
+ # type and default value may be specified, as well as any options
+ # supported by the given cast type.
+ #
+ # class Person
+ # include ActiveModel::Attributes
+ #
+ # attribute :name, :string
+ # attribute :active, :boolean, default: true
+ # end
+ #
+ # person = Person.new(name: "Volmer")
+ #
+ # person.name # => "Volmer"
+ # person.active # => true
def attribute(name, cast_type = nil, default: NO_DEFAULT_PROVIDED, **options)
name = name.to_s
@@ -27,7 +65,7 @@ module ActiveModel
define_attribute_method(name)
end
- # Returns an array of attribute names as strings
+ # Returns an array of attribute names as strings.
#
# class Person
# include ActiveModel::Attributes
@@ -36,8 +74,7 @@ module ActiveModel
# attribute :age, :integer
# end
#
- # Person.attribute_names
- # # => ["name", "age"]
+ # Person.attribute_names # => ["name", "age"]
def attribute_names
attribute_types.keys
end
@@ -75,7 +112,7 @@ module ActiveModel
end
end
- def initialize(*)
+ def initialize(*) # :nodoc:
@attributes = self.class._default_attributes.deep_dup
super
end
@@ -85,7 +122,8 @@ module ActiveModel
super
end
- # Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
+ # Returns a hash of all the attributes with their names as keys and the
+ # values of the attributes as values.
#
# class Person
# include ActiveModel::Attributes
@@ -94,14 +132,13 @@ module ActiveModel
# attribute :age, :integer
# end
#
- # person = Person.new(name: 'Francesco', age: 22)
- # person.attributes
- # # => {"name"=>"Francesco", "age"=>22}
+ # person = Person.new(name: "Francesco", age: 22)
+ # person.attributes # => { "name" => "Francesco", "age" => 22}
def attributes
@attributes.to_hash
end
- # Returns an array of attribute names as strings
+ # Returns an array of attribute names as strings.
#
# class Person
# include ActiveModel::Attributes
@@ -111,13 +148,12 @@ module ActiveModel
# end
#
# person = Person.new
- # person.attribute_names
- # # => ["name", "age"]
+ # person.attribute_names # => ["name", "age"]
def attribute_names
@attributes.keys
end
- def freeze
+ def freeze # :nodoc:
@attributes = @attributes.clone.freeze unless frozen?
super
end
diff --git a/activemodel/lib/active_model/type/big_integer.rb b/activemodel/lib/active_model/type/big_integer.rb
index b2c3ee50aa..9abe7375db 100644
--- a/activemodel/lib/active_model/type/big_integer.rb
+++ b/activemodel/lib/active_model/type/big_integer.rb
@@ -4,7 +4,21 @@ require "active_model/type/integer"
module ActiveModel
module Type
- class BigInteger < Integer # :nodoc:
+ # Attribute type for integers that can be serialized to an unlimited number
+ # of bytes. This type is registered under the +:big_integer+ key.
+ #
+ # class Person
+ # include ActiveModel::Attributes
+ #
+ # attribute :id, :big_integer
+ # end
+ #
+ # person = Person.new(id: "18_000_000_000")
+ # person.id # => 18000000000
+ #
+ # All casting and serialization are performed in the same way as the
+ # standard ActiveModel::Type::Integer type.
+ class BigInteger < Integer
private
def max_value
::Float::INFINITY
diff --git a/activemodel/lib/active_model/type/binary.rb b/activemodel/lib/active_model/type/binary.rb
index 76203c5a88..d62a0cd7d2 100644
--- a/activemodel/lib/active_model/type/binary.rb
+++ b/activemodel/lib/active_model/type/binary.rb
@@ -2,7 +2,11 @@
module ActiveModel
module Type
- class Binary < Value # :nodoc:
+ # Attribute type for representation of binary data. This type is registered
+ # under the +:binary+ key.
+ #
+ # Non-string values are coerced to strings using their +to_s+ method.
+ class Binary < Value
def type
:binary
end
diff --git a/activemodel/lib/active_model/type/boolean.rb b/activemodel/lib/active_model/type/boolean.rb
index 1214e9319b..0a8cc26e30 100644
--- a/activemodel/lib/active_model/type/boolean.rb
+++ b/activemodel/lib/active_model/type/boolean.rb
@@ -2,17 +2,13 @@
module ActiveModel
module Type
- # == Active \Model \Type \Boolean
+ # A class that behaves like a boolean type, including rules for coercion of
+ # user input.
#
- # A class that behaves like a boolean type, including rules for coercion of user input.
- #
- # === Coercion
- # Values set from user input will first be coerced into the appropriate ruby type.
- # Coercion behavior is roughly mapped to Ruby's boolean semantics.
- #
- # - "false", "f" , "0", +0+ or any other value in +FALSE_VALUES+ will be coerced to +false+
- # - Empty strings are coerced to +nil+
- # - All other values will be coerced to +true+
+ # - "false", "f" , "0", +0+ or any other value in
+ # +FALSE_VALUES+ will be coerced to +false+.
+ # - Empty strings are coerced to +nil+.
+ # - All other values will be coerced to +true+.
class Boolean < Value
FALSE_VALUES = [
false, 0,
diff --git a/activemodel/lib/active_model/type/date.rb b/activemodel/lib/active_model/type/date.rb
index 4196fd97cd..d1e6d88211 100644
--- a/activemodel/lib/active_model/type/date.rb
+++ b/activemodel/lib/active_model/type/date.rb
@@ -2,7 +2,25 @@
module ActiveModel
module Type
- class Date < Value # :nodoc:
+ # Attribute type for date representation. It is registered under the
+ # +:date+ key.
+ #
+ # class Person
+ # include ActiveModel::Attributes
+ #
+ # attribute :birthday, :date
+ # end
+ #
+ # person = Person.new(birthday: "1989-07-13")
+ #
+ # person.birthday.class # => Date
+ # person.birthday.year # => 1989
+ # person.birthday.month # => 7
+ # person.birthday.day # => 13
+ #
+ # String values are parsed using the ISO 8601 date format. Any other values
+ # are cast using their +to_date+ method, if it exists.
+ class Date < Value
include Helpers::Timezone
include Helpers::AcceptsMultiparameterTime.new
diff --git a/activemodel/lib/active_model/type/date_time.rb b/activemodel/lib/active_model/type/date_time.rb
index 532f0f0e86..6ef1d38ce1 100644
--- a/activemodel/lib/active_model/type/date_time.rb
+++ b/activemodel/lib/active_model/type/date_time.rb
@@ -2,7 +2,41 @@
module ActiveModel
module Type
- class DateTime < Value # :nodoc:
+ # Attribute type to represent dates and times. It is registered under the
+ # +:datetime+ key.
+ #
+ # class Event
+ # include ActiveModel::Attributes
+ #
+ # attribute :start, :datetime
+ # end
+ #
+ # event = Event.new(start: "Wed, 04 Sep 2013 03:00:00 EAT")
+ #
+ # event.start.class # => Time
+ # event.start.year # => 2013
+ # event.start.month # => 9
+ # event.start.day # => 4
+ # event.start.hour # => 3
+ # event.start.min # => 0
+ # event.start.sec # => 0
+ # event.start.zone # => "EAT"
+ #
+ # String values are parsed using the ISO 8601 datetime format. Partial
+ # time-only formats are also accepted.
+ #
+ # event.start = "06:07:08+09:00"
+ # event.start.utc # => 1999-12-31 21:07:08 UTC
+ #
+ # The degree of sub-second precision can be customized when declaring an
+ # attribute:
+ #
+ # class Event
+ # include ActiveModel::Attributes
+ #
+ # attribute :start, :datetime, precision: 4
+ # end
+ class DateTime < Value
include Helpers::Timezone
include Helpers::TimeValue
include Helpers::AcceptsMultiparameterTime.new(
diff --git a/activemodel/lib/active_model/type/decimal.rb b/activemodel/lib/active_model/type/decimal.rb
index 6aa51ff2ac..4539537baf 100644
--- a/activemodel/lib/active_model/type/decimal.rb
+++ b/activemodel/lib/active_model/type/decimal.rb
@@ -4,7 +4,32 @@ require "bigdecimal/util"
module ActiveModel
module Type
- class Decimal < Value # :nodoc:
+ # Attribute type for decimal, high-precision floating point numeric
+ # representation. It is registered under the +:decimal+ key.
+ #
+ # class BagOfCoffee
+ # include ActiveModel::Attributes
+ #
+ # attribute :weight, :decimal
+ # end
+ #
+ # bag = BagOfCoffee.new(weight: "0.0001")
+ # bag.weight # => 0.1e-3
+ #
+ # Numeric instances are converted to BigDecimal instances. Any other objects
+ # are cast using their +to_d+ method, if it exists. If it does not exist,
+ # the object is converted to a string using +to_s+, which is then coerced to
+ # a BigDecimal using +to_d+.
+ #
+ # Decimal precision defaults to 18, and can be customized when declaring an
+ # attribute:
+ #
+ # class BagOfCoffee
+ # include ActiveModel::Attributes
+ #
+ # attribute :weight, :decimal, precision: 24
+ # end
+ class Decimal < Value
include Helpers::Numeric
BIGDECIMAL_PRECISION = 18
diff --git a/activemodel/lib/active_model/type/float.rb b/activemodel/lib/active_model/type/float.rb
index 435e39b2c9..a96d7e9b62 100644
--- a/activemodel/lib/active_model/type/float.rb
+++ b/activemodel/lib/active_model/type/float.rb
@@ -4,7 +4,26 @@ require "active_support/core_ext/object/try"
module ActiveModel
module Type
- class Float < Value # :nodoc:
+ # Attribute type for floating point numeric values. It is registered under
+ # the +:float+ key.
+ #
+ # class BagOfCoffee
+ # include ActiveModel::Attributes
+ #
+ # attribute :weight, :float
+ # end
+ #
+ # bag = BagOfCoffee.new(weight: "0.25")
+ # bag.weight # => 0.25
+ #
+ # Values are coerced to their float representation using their +to_f+
+ # methods. However, the following strings which represent floating point
+ # constants are cast accordingly:
+ #
+ # - "Infinity" is cast to Float::INFINITY.
+ # - "-Infinity" is cast to -Float::INFINITY.
+ # - "NaN" is cast to Float::NAN.
+ class Float < Value
include Helpers::Numeric
def type
diff --git a/activemodel/lib/active_model/type/immutable_string.rb b/activemodel/lib/active_model/type/immutable_string.rb
index 5cb24a3928..38ebea5666 100644
--- a/activemodel/lib/active_model/type/immutable_string.rb
+++ b/activemodel/lib/active_model/type/immutable_string.rb
@@ -2,7 +2,34 @@
module ActiveModel
module Type
- class ImmutableString < Value # :nodoc:
+ # Attribute type to represent immutable strings. It casts incoming values to
+ # frozen strings.
+ #
+ # class Person
+ # include ActiveModel::Attributes
+ #
+ # attribute :name, :immutable_string
+ # end
+ #
+ # person = Person.new
+ # person.name = 1
+ # person.name # => "1"
+ # person.name.frozen? # => true
+ #
+ # Values are coerced to strings using their +to_s+ method. Boolean values
+ # are treated differently, however: +true+ will be cast to "t" and
+ # +false+ will be cast to "f". These strings can be customized when
+ # declaring an attribute:
+ #
+ # class Person
+ # include ActiveModel::Attributes
+ #
+ # attribute :active, :immutable_string, true: "aye", false: "nay"
+ # end
+ #
+ # person = Person.new(active: true)
+ # person.active # => "aye"
+ class ImmutableString < Value
def initialize(**args)
@true = -(args.delete(:true)&.to_s || "t")
@false = -(args.delete(:false)&.to_s || "f")
diff --git a/activemodel/lib/active_model/type/integer.rb b/activemodel/lib/active_model/type/integer.rb
index 4f256c9859..9bbe540099 100644
--- a/activemodel/lib/active_model/type/integer.rb
+++ b/activemodel/lib/active_model/type/integer.rb
@@ -2,7 +2,38 @@
module ActiveModel
module Type
- class Integer < Value # :nodoc:
+ # Attribute type for integer representation. This type is registered under
+ # the +:integer+ key.
+ #
+ # class Person
+ # include ActiveModel::Attributes
+ #
+ # attribute :age, :integer
+ # end
+ #
+ # person = Person.new(age: "18")
+ # person.age # => 18
+ #
+ # Values are cast using their +to_i+ method, if it exists. If it does not
+ # exist, or if it raises an error, the value will be cast to +nil+:
+ #
+ # person.age = :not_an_integer
+ # person.age # => nil (because Symbol does not define #to_i)
+ #
+ # Serialization also works under the same principle. Non-numeric strings are
+ # serialized as +nil+, for example.
+ #
+ # Serialization also validates that the integer can be stored using a
+ # limited number of bytes. If it cannot, an ActiveModel::RangeError will be
+ # raised. The default limit is 4 bytes, and can be customized when declaring
+ # an attribute:
+ #
+ # class Person
+ # include ActiveModel::Attributes
+ #
+ # attribute :age, :integer, limit: 6
+ # end
+ class Integer < Value
include Helpers::Numeric
# Column storage size in bytes.
diff --git a/activemodel/lib/active_model/type/string.rb b/activemodel/lib/active_model/type/string.rb
index 631f29613a..635a0cc06d 100644
--- a/activemodel/lib/active_model/type/string.rb
+++ b/activemodel/lib/active_model/type/string.rb
@@ -4,7 +4,13 @@ require "active_model/type/immutable_string"
module ActiveModel
module Type
- class String < ImmutableString # :nodoc:
+ # Attribute type for strings. It is registered under the +:string+ key.
+ #
+ # This class is a specialization of ActiveModel::Type::ImmutableString. It
+ # performs coercion in the same way, and can be configured in the same way.
+ # However, it accounts for mutable strings, so dirty tracking can properly
+ # check if a string has changed.
+ class String < ImmutableString
def changed_in_place?(raw_old_value, new_value)
if new_value.is_a?(::String)
raw_old_value != new_value
diff --git a/activemodel/lib/active_model/type/time.rb b/activemodel/lib/active_model/type/time.rb
index a27cb8416b..62f6e513f7 100644
--- a/activemodel/lib/active_model/type/time.rb
+++ b/activemodel/lib/active_model/type/time.rb
@@ -2,7 +2,41 @@
module ActiveModel
module Type
- class Time < Value # :nodoc:
+ # Attribute type for time representation. It is registered under the
+ # +:time+ key.
+ #
+ # class Event
+ # include ActiveModel::Attributes
+ #
+ # attribute :start, :time
+ # end
+ #
+ # event = Event.new(start: "2022-02-18T13:15:00-05:00")
+ #
+ # event.start.class # => Time
+ # event.start.year # => 2022
+ # event.start.month # => 2
+ # event.start.day # => 18
+ # event.start.hour # => 13
+ # event.start.min # => 15
+ # event.start.sec # => 0
+ # event.start.zone # => "EST"
+ #
+ # String values are parsed using the ISO 8601 datetime format. Partial
+ # time-only formats are also accepted.
+ #
+ # event.start = "06:07:08+09:00"
+ # event.start.utc # => 1999-12-31 21:07:08 UTC
+ #
+ # The degree of sub-second precision can be customized when declaring an
+ # attribute:
+ #
+ # class Event
+ # include ActiveModel::Attributes
+ #
+ # attribute :start, :time, precision: 4
+ # end
+ class Time < Value
include Helpers::Timezone
include Helpers::TimeValue
include Helpers::AcceptsMultiparameterTime.new(
diff --git a/activemodel/lib/active_model/type/value.rb b/activemodel/lib/active_model/type/value.rb
index 1bcebe1b6b..880f74a99b 100644
--- a/activemodel/lib/active_model/type/value.rb
+++ b/activemodel/lib/active_model/type/value.rb
@@ -2,9 +2,15 @@
module ActiveModel
module Type
+ # The base class for all attribute types. This class also serves as the
+ # default type for attributes that do not specify a type.
class Value
attr_reader :precision, :scale, :limit
+ # Initializes a type with three basic configuration settings: precision,
+ # limit, and scale. The Value base class does not define behavior for
+ # these settings. It uses them for equality comparison and hash key
+ # generation only.
def initialize(precision: nil, limit: nil, scale: nil)
@precision = precision
@scale = scale
@@ -19,7 +25,9 @@ module ActiveModel
true
end
- def type # :nodoc:
+ # Returns the unique type name as a Symbol. Subclasses should override
+ # this method.
+ def type
end
# Converts a value from database input to the appropriate ruby type. The