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