Skip to content

Commit

Permalink
Documentation for Active Model Attributes
Browse files Browse the repository at this point in the history
This commit adds documentation to the constants and methods that are
part of Active Model's Attributes API. So far this API has been hidden
with the :nodoc: flag since its inception in Active Record and
subsequent move to Active Model (#30920 and #30985); as the API matures
and gets ready for public usage, visible documentation for its endpoints
becomes necessary.

The classes and modules being documented and publicized by this commit
are the main `Attributes` module, the `Type` namespace, and all the
standard attribute type classes included in the current API, which users
will be able to extend and replicate to suit their customization needs.

Some private modules are also receiving documetation, although they will
continue using the :nodoc: flag. Those are `Attribute` and
`AttributeSet`. Although they remain private I found useful to add some
comments to describe their responsibilities.
  • Loading branch information
volmer committed Feb 9, 2022
1 parent 2b6182c commit 07a5584
Show file tree
Hide file tree
Showing 13 changed files with 286 additions and 34 deletions.
62 changes: 49 additions & 13 deletions activemodel/lib/active_model/attributes.rb
Expand Up @@ -4,7 +4,30 @@
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

Expand All @@ -16,6 +39,21 @@ module Attributes # :nodoc:
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

Expand All @@ -27,7 +65,7 @@ def attribute(name, cast_type = nil, default: NO_DEFAULT_PROVIDED, **options)
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
Expand All @@ -36,8 +74,7 @@ def attribute(name, cast_type = nil, default: NO_DEFAULT_PROVIDED, **options)
# attribute :age, :integer
# end
#
# Person.attribute_names
# # => ["name", "age"]
# Person.attribute_names # => ["name", "age"]
def attribute_names
attribute_types.keys
end
Expand Down Expand Up @@ -75,7 +112,7 @@ def define_default_attribute(name, value, type)
end
end

def initialize(*)
def initialize(*) # :nodoc:
@attributes = self.class._default_attributes.deep_dup
super
end
Expand All @@ -85,7 +122,8 @@ def initialize_dup(other) # :nodoc:
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
Expand All @@ -94,14 +132,13 @@ def initialize_dup(other) # :nodoc:
# 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
Expand All @@ -111,13 +148,12 @@ def attributes
# 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
Expand Down
16 changes: 15 additions & 1 deletion activemodel/lib/active_model/type/big_integer.rb
Expand Up @@ -4,7 +4,21 @@

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
Expand Down
6 changes: 5 additions & 1 deletion activemodel/lib/active_model/type/binary.rb
Expand Up @@ -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
Expand Down
16 changes: 6 additions & 10 deletions activemodel/lib/active_model/type/boolean.rb
Expand Up @@ -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+
# - <tt>"false"</tt>, <tt>"f"</tt> , <tt>"0"</tt>, +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,
Expand Down
20 changes: 19 additions & 1 deletion activemodel/lib/active_model/type/date.rb
Expand Up @@ -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

Expand Down
36 changes: 35 additions & 1 deletion activemodel/lib/active_model/type/date_time.rb
Expand Up @@ -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(
Expand Down
27 changes: 26 additions & 1 deletion activemodel/lib/active_model/type/decimal.rb
Expand Up @@ -4,7 +4,32 @@

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

Expand Down
21 changes: 20 additions & 1 deletion activemodel/lib/active_model/type/float.rb
Expand Up @@ -4,7 +4,26 @@

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:
#
# - <tt>"Infinity"</tt> is cast to <tt>Float::INFINITY</tt>.
# - <tt>"-Infinity"</tt> is cast to <tt>-Float::INFINITY</tt>.
# - <tt>"NaN"</tt> is cast to <tt>Float::NAN</tt>.
class Float < Value
include Helpers::Numeric

def type
Expand Down
29 changes: 28 additions & 1 deletion activemodel/lib/active_model/type/immutable_string.rb
Expand Up @@ -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 <tt>"t"</tt> and
# +false+ will be cast to <tt>"f"</tt>. 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")
Expand Down
33 changes: 32 additions & 1 deletion activemodel/lib/active_model/type/integer.rb
Expand Up @@ -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.
Expand Down

0 comments on commit 07a5584

Please sign in to comment.