Skip to content

Commit 8c752c7

Browse files
committed
Docs pass for the attributes API
1 parent b71e08f commit 8c752c7

2 files changed

Lines changed: 154 additions & 39 deletions

File tree

activerecord/lib/active_record/attributes.rb

Lines changed: 112 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module ActiveRecord
2-
module Attributes # :nodoc:
2+
module Attributes
33
extend ActiveSupport::Concern
44

55
Type = ActiveRecord::Type
@@ -9,21 +9,31 @@ module Attributes # :nodoc:
99
self.attributes_to_define_after_schema_loads = {}
1010
end
1111

12-
module ClassMethods # :nodoc:
13-
# Defines or overrides an attribute on this model. This allows customization of
14-
# Active Record's type casting behavior, as well as adding support for user defined
15-
# types.
16-
#
17-
# +name+ The name of the methods to define attribute methods for, and the column which
18-
# this will persist to.
12+
module ClassMethods
13+
# Defines an attribute with a type on this model. It will override the
14+
# type of existing attributes if needed. This allows control over how
15+
# values are converted to and from SQL when assigned to a model. It also
16+
# changes the behavior of values passed to
17+
# +ActiveRecord::Relation::QueryMethods#where+. This will let you use
18+
# your domain objects across much of Active Record, without having to
19+
# rely on implementation details or monkey patching.
20+
#
21+
# +name+ The name of the methods to define attribute methods for, and the
22+
# column which this will persist to.
1923
#
2024
# +cast_type+ A type object that contains information about how to type cast the value.
2125
# See the examples section for more information.
2226
#
2327
# ==== Options
24-
# The options hash accepts the following options:
28+
# The following options are accepted:
29+
#
30+
# +default+ The default value to use when no value is provided. If this option
31+
# is not passed, the previous default value (if any) will be used.
32+
# Otherwise, the default will be +nil+.
2533
#
26-
# +default+ is the default value that the column should use on a new record.
34+
# +array+ (PG only) specifies that the type should be an array (see the examples below)
35+
#
36+
# +range+ (PG only) specifies that the type should be a range (see the examples below)
2737
#
2838
# ==== Examples
2939
#
@@ -50,11 +60,35 @@ module ClassMethods # :nodoc:
5060
# # after
5161
# store_listing.price_in_cents # => 10
5262
#
53-
# Users may also define their own custom types, as long as they respond to the methods
54-
# defined on the value type. The +type_cast+ method on your type object will be called
55-
# with values both from the database, and from your controllers. See
56-
# +ActiveRecord::Attributes::Type::Value+ for the expected API. It is recommended that your
57-
# type objects inherit from an existing type, or the base value type.
63+
# Attributes do not need to be backed by a database column.
64+
#
65+
# class MyModel < ActiveRecord::Base
66+
# attribute :my_string, :string
67+
# attribute :my_int_array, :integer, array: true
68+
# attribute :my_float_range, :float, range: true
69+
# end
70+
#
71+
# model = MyModel.new(
72+
# my_string: "string",
73+
# my_int_array: ["1", "2", "3"],
74+
# my_float_range: "[1,3.5]",
75+
# )
76+
# model.attributes
77+
# # =>
78+
# {
79+
# my_string: "string",
80+
# my_int_array: [1, 2, 3],
81+
# my_float_range: 1.0..3.5
82+
# }
83+
#
84+
# ==== Creating Custom Types
85+
#
86+
# Users may also define their own custom types, as long as they respond
87+
# to the methods defined on the value type. The +type_cast+ method on
88+
# your type object will be called with values both from the database, and
89+
# from your controllers. See +ActiveRecord::Attributes::Type::Value+ for
90+
# the expected API. It is recommended that your type objects inherit from
91+
# an existing type, or the base value type.
5892
#
5993
# class MoneyType < ActiveRecord::Type::Integer
6094
# def type_cast(value)
@@ -73,6 +107,51 @@ module ClassMethods # :nodoc:
73107
#
74108
# store_listing = StoreListing.new(price_in_cents: '$10.00')
75109
# store_listing.price_in_cents # => 1000
110+
#
111+
# For more details on creating custom types, see the documentation for
112+
# +ActiveRecord::Type::Value+
113+
#
114+
# ==== Querying
115+
#
116+
# When +ActiveRecord::Relation::QueryMethods#where+ is called, it will
117+
# use the type defined by the model class to convert the value to SQL,
118+
# calling +type_cast_for_database+ on your type object. For example:
119+
#
120+
# class Money < Struct.new(:amount, :currency)
121+
# end
122+
#
123+
# class MoneyType < Type::Value
124+
# def initialize(currency_converter)
125+
# @currency_converter = currency_converter
126+
# end
127+
#
128+
# # value will be the result of +type_cast_from_database+ or
129+
# # +type_cast_from_user+. Assumed to be in instance of +Money+ in
130+
# # this case.
131+
# def type_cast_for_database(value)
132+
# value_in_bitcoins = currency_converter.convert_to_bitcoins(value)
133+
# value_in_bitcoins.amount
134+
# end
135+
# end
136+
#
137+
# class Product < ActiveRecord::Base
138+
# currency_converter = ConversionRatesFromTheInternet.new
139+
# attribute :price_in_bitcoins, MoneyType.new(currency_converter)
140+
# end
141+
#
142+
# Product.where(price_in_bitcoins: Money.new(5, "USD"))
143+
# # => SELECT * FROM products WHERE price_in_bitcoins = 0.02230
144+
#
145+
# Product.where(price_in_bitcoins: Money.new(5, "GBP"))
146+
# # => SELECT * FROM products WHERE price_in_bitcoins = 0.03412
147+
#
148+
# ==== Dirty Tracking
149+
#
150+
# The type of an attribute is given the opportunity to change how dirty
151+
# tracking is performed. The methods +changed?+ and +changed_in_place?+
152+
# will be called from +ActiveRecord::AttributeMethods::Dirty+. See the
153+
# documentation for those methods in +ActiveRecord::Type::Value+ for more
154+
# details.
76155
def attribute(name, cast_type, **options)
77156
name = name.to_s
78157
reload_schema_from_cache
@@ -83,6 +162,23 @@ def attribute(name, cast_type, **options)
83162
)
84163
end
85164

165+
# This is the low level API which sits beneath +attribute+. It only
166+
# accepts type objects, and will do its work immediately instead of
167+
# waiting for the schema to load. Automatic schema detection and
168+
# +attribute+ both call this under the hood. While this method is
169+
# provided so it can be used by plugin authors, application code should
170+
# probably use +attribute+.
171+
#
172+
# +name+ The name of the attribute being defined. Expected to be a +String+.
173+
#
174+
# +cast_type+ The type object to use for this attribute
175+
#
176+
# +default+ The default value to use when no value is provided. If this option
177+
# is not passed, the previous default value (if any) will be used.
178+
# Otherwise, the default will be +nil+.
179+
#
180+
# +user_provided_default+ Whether the default value should be cast using
181+
# +type_cast_from_user+ or +type_cast_from_database+
86182
def define_attribute(
87183
name,
88184
cast_type,
@@ -93,7 +189,7 @@ def define_attribute(
93189
define_default_attribute(name, default, cast_type, from_user: user_provided_default)
94190
end
95191

96-
def load_schema!
192+
def load_schema! # :nodoc:
97193
super
98194
attributes_to_define_after_schema_loads.each do |name, (type, options)|
99195
if type.is_a?(Symbol)

activerecord/lib/active_record/type/value.rb

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,36 @@
11
module ActiveRecord
22
module Type
3-
class Value # :nodoc:
3+
class Value
44
attr_reader :precision, :scale, :limit
55

6-
# Valid options are +precision+, +scale+, and +limit+.
7-
def initialize(options = {})
8-
options.assert_valid_keys(:precision, :scale, :limit)
9-
@precision = options[:precision]
10-
@scale = options[:scale]
11-
@limit = options[:limit]
6+
def initialize(precision: nil, limit: nil, scale: nil)
7+
@precision = precision
8+
@scale = scale
9+
@limit = limit
1210
end
1311

14-
# The simplified type that this object represents. Returns a symbol such
15-
# as +:string+ or +:integer+
16-
def type; end
12+
def type; end # :nodoc:
1713

18-
# Type casts a string from the database into the appropriate ruby type.
19-
# Classes which do not need separate type casting behavior for database
20-
# and user provided values should override +cast_value+ instead.
14+
# Convert a value from database input to the appropriate ruby type. The
15+
# return value of this method will be returned from
16+
# +ActiveRecord::AttributeMethods::Read#read_attribute+. See also
17+
# +type_cast+ and +cast_value+
18+
#
19+
# +value+ The raw input, as provided from the database
2120
def type_cast_from_database(value)
2221
type_cast(value)
2322
end
2423

2524
# Type casts a value from user input (e.g. from a setter). This value may
26-
# be a string from the form builder, or an already type cast value
27-
# provided manually to a setter.
25+
# be a string from the form builder, or a ruby object passed to a setter.
26+
# There is currently no way to differentiate between which source it came
27+
# from.
28+
#
29+
# The return value of this method will be returned from
30+
# +ActiveRecord::AttributeMethods::Read#read_attribute+. See also:
31+
# +type_cast+ and +cast_value+
2832
#
29-
# Classes which do not need separate type casting behavior for database
30-
# and user provided values should override +type_cast+ or +cast_value+
31-
# instead.
33+
# +value+ The raw input, as provided to the attribute setter.
3234
def type_cast_from_user(value)
3335
type_cast(value)
3436
end
@@ -72,10 +74,23 @@ def changed?(old_value, new_value, _new_value_before_type_cast)
7274
end
7375

7476
# Determines whether the mutable value has been modified since it was
75-
# read. Returns +false+ by default. This method should not be overridden
76-
# directly. Types which return a mutable value should include
77-
# +Type::Mutable+, which will define this method.
78-
def changed_in_place?(*)
77+
# read. Returns +false+ by default. If your type returns an object
78+
# which could be mutated, you should override this method. You will need
79+
# to either:
80+
#
81+
# - pass +new_value+ to +type_cast_for_database+ and compare it to
82+
# +raw_old_value+
83+
#
84+
# or
85+
#
86+
# - pass +raw_old_value+ to +type_cast_from_database+ and compare it to
87+
# +new_value+
88+
#
89+
# +raw_old_value+ The original value, before being passed to
90+
# +type_cast_from_database+.
91+
#
92+
# +new_value+ The current value, after type casting.
93+
def changed_in_place?(raw_old_value, new_value)
7994
false
8095
end
8196

@@ -88,7 +103,11 @@ def ==(other)
88103

89104
private
90105

91-
def type_cast(value)
106+
# Convenience method. If you don't need separate behavior for
107+
# +type_cast_from_database+ and +type_cast_from_user+, you can override
108+
# this method instead. The default behavior of both methods is to call
109+
# this one. See also +cast_value+
110+
def type_cast(value) # :doc:
92111
cast_value(value) unless value.nil?
93112
end
94113

0 commit comments

Comments
 (0)