1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Docs pass for the attributes API

This commit is contained in:
Sean Griffin 2015-02-06 13:02:03 -07:00
parent b71e08f8ba
commit 8c752c7ac7
2 changed files with 153 additions and 38 deletions

View file

@ -1,5 +1,5 @@
module ActiveRecord
module Attributes # :nodoc:
module Attributes
extend ActiveSupport::Concern
Type = ActiveRecord::Type
@ -9,21 +9,31 @@ module ActiveRecord
self.attributes_to_define_after_schema_loads = {}
end
module ClassMethods # :nodoc:
# Defines or overrides an attribute on this model. This allows customization of
# Active Record's type casting behavior, as well as adding support for user defined
# types.
module ClassMethods
# Defines an attribute with a type on this model. It will override the
# type of existing attributes if needed. This allows control over how
# values are converted to and from SQL when assigned to a model. It also
# changes the behavior of values passed to
# +ActiveRecord::Relation::QueryMethods#where+. This will let you use
# your domain objects across much of Active Record, without having to
# rely on implementation details or monkey patching.
#
# +name+ The name of the methods to define attribute methods for, and the column which
# this will persist to.
# +name+ The name of the methods to define attribute methods for, and the
# column which this will persist to.
#
# +cast_type+ A type object that contains information about how to type cast the value.
# See the examples section for more information.
#
# ==== Options
# The options hash accepts the following options:
# The following options are accepted:
#
# +default+ is the default value that the column should use on a new record.
# +default+ The default value to use when no value is provided. If this option
# is not passed, the previous default value (if any) will be used.
# Otherwise, the default will be +nil+.
#
# +array+ (PG only) specifies that the type should be an array (see the examples below)
#
# +range+ (PG only) specifies that the type should be a range (see the examples below)
#
# ==== Examples
#
@ -50,11 +60,35 @@ module ActiveRecord
# # after
# store_listing.price_in_cents # => 10
#
# Users may also define their own custom types, as long as they respond to the methods
# defined on the value type. The +type_cast+ method on your type object will be called
# with values both from the database, and from your controllers. See
# +ActiveRecord::Attributes::Type::Value+ for the expected API. It is recommended that your
# type objects inherit from an existing type, or the base value type.
# Attributes do not need to be backed by a database column.
#
# class MyModel < ActiveRecord::Base
# attribute :my_string, :string
# attribute :my_int_array, :integer, array: true
# attribute :my_float_range, :float, range: true
# end
#
# model = MyModel.new(
# my_string: "string",
# my_int_array: ["1", "2", "3"],
# my_float_range: "[1,3.5]",
# )
# model.attributes
# # =>
# {
# my_string: "string",
# my_int_array: [1, 2, 3],
# my_float_range: 1.0..3.5
# }
#
# ==== Creating Custom Types
#
# Users may also define their own custom types, as long as they respond
# to the methods defined on the value type. The +type_cast+ method on
# your type object will be called with values both from the database, and
# from your controllers. See +ActiveRecord::Attributes::Type::Value+ for
# the expected API. It is recommended that your type objects inherit from
# an existing type, or the base value type.
#
# class MoneyType < ActiveRecord::Type::Integer
# def type_cast(value)
@ -73,6 +107,51 @@ module ActiveRecord
#
# store_listing = StoreListing.new(price_in_cents: '$10.00')
# store_listing.price_in_cents # => 1000
#
# For more details on creating custom types, see the documentation for
# +ActiveRecord::Type::Value+
#
# ==== Querying
#
# When +ActiveRecord::Relation::QueryMethods#where+ is called, it will
# use the type defined by the model class to convert the value to SQL,
# calling +type_cast_for_database+ on your type object. For example:
#
# class Money < Struct.new(:amount, :currency)
# end
#
# class MoneyType < Type::Value
# def initialize(currency_converter)
# @currency_converter = currency_converter
# end
#
# # value will be the result of +type_cast_from_database+ or
# # +type_cast_from_user+. Assumed to be in instance of +Money+ in
# # this case.
# def type_cast_for_database(value)
# value_in_bitcoins = currency_converter.convert_to_bitcoins(value)
# value_in_bitcoins.amount
# end
# end
#
# class Product < ActiveRecord::Base
# currency_converter = ConversionRatesFromTheInternet.new
# attribute :price_in_bitcoins, MoneyType.new(currency_converter)
# end
#
# Product.where(price_in_bitcoins: Money.new(5, "USD"))
# # => SELECT * FROM products WHERE price_in_bitcoins = 0.02230
#
# Product.where(price_in_bitcoins: Money.new(5, "GBP"))
# # => SELECT * FROM products WHERE price_in_bitcoins = 0.03412
#
# ==== Dirty Tracking
#
# The type of an attribute is given the opportunity to change how dirty
# tracking is performed. The methods +changed?+ and +changed_in_place?+
# will be called from +ActiveRecord::AttributeMethods::Dirty+. See the
# documentation for those methods in +ActiveRecord::Type::Value+ for more
# details.
def attribute(name, cast_type, **options)
name = name.to_s
reload_schema_from_cache
@ -83,6 +162,23 @@ module ActiveRecord
)
end
# This is the low level API which sits beneath +attribute+. It only
# accepts type objects, and will do its work immediately instead of
# waiting for the schema to load. Automatic schema detection and
# +attribute+ both call this under the hood. While this method is
# provided so it can be used by plugin authors, application code should
# probably use +attribute+.
#
# +name+ The name of the attribute being defined. Expected to be a +String+.
#
# +cast_type+ The type object to use for this attribute
#
# +default+ The default value to use when no value is provided. If this option
# is not passed, the previous default value (if any) will be used.
# Otherwise, the default will be +nil+.
#
# +user_provided_default+ Whether the default value should be cast using
# +type_cast_from_user+ or +type_cast_from_database+
def define_attribute(
name,
cast_type,
@ -93,7 +189,7 @@ module ActiveRecord
define_default_attribute(name, default, cast_type, from_user: user_provided_default)
end
def load_schema!
def load_schema! # :nodoc:
super
attributes_to_define_after_schema_loads.each do |name, (type, options)|
if type.is_a?(Symbol)

View file

@ -1,34 +1,36 @@
module ActiveRecord
module Type
class Value # :nodoc:
class Value
attr_reader :precision, :scale, :limit
# Valid options are +precision+, +scale+, and +limit+.
def initialize(options = {})
options.assert_valid_keys(:precision, :scale, :limit)
@precision = options[:precision]
@scale = options[:scale]
@limit = options[:limit]
def initialize(precision: nil, limit: nil, scale: nil)
@precision = precision
@scale = scale
@limit = limit
end
# The simplified type that this object represents. Returns a symbol such
# as +:string+ or +:integer+
def type; end
def type; end # :nodoc:
# Type casts a string from the database into the appropriate ruby type.
# Classes which do not need separate type casting behavior for database
# and user provided values should override +cast_value+ instead.
# Convert a value from database input to the appropriate ruby type. The
# return value of this method will be returned from
# +ActiveRecord::AttributeMethods::Read#read_attribute+. See also
# +type_cast+ and +cast_value+
#
# +value+ The raw input, as provided from the database
def type_cast_from_database(value)
type_cast(value)
end
# Type casts a value from user input (e.g. from a setter). This value may
# be a string from the form builder, or an already type cast value
# provided manually to a setter.
# be a string from the form builder, or a ruby object passed to a setter.
# There is currently no way to differentiate between which source it came
# from.
#
# Classes which do not need separate type casting behavior for database
# and user provided values should override +type_cast+ or +cast_value+
# instead.
# The return value of this method will be returned from
# +ActiveRecord::AttributeMethods::Read#read_attribute+. See also:
# +type_cast+ and +cast_value+
#
# +value+ The raw input, as provided to the attribute setter.
def type_cast_from_user(value)
type_cast(value)
end
@ -72,10 +74,23 @@ module ActiveRecord
end
# Determines whether the mutable value has been modified since it was
# read. Returns +false+ by default. This method should not be overridden
# directly. Types which return a mutable value should include
# +Type::Mutable+, which will define this method.
def changed_in_place?(*)
# read. Returns +false+ by default. If your type returns an object
# which could be mutated, you should override this method. You will need
# to either:
#
# - pass +new_value+ to +type_cast_for_database+ and compare it to
# +raw_old_value+
#
# or
#
# - pass +raw_old_value+ to +type_cast_from_database+ and compare it to
# +new_value+
#
# +raw_old_value+ The original value, before being passed to
# +type_cast_from_database+.
#
# +new_value+ The current value, after type casting.
def changed_in_place?(raw_old_value, new_value)
false
end
@ -88,7 +103,11 @@ module ActiveRecord
private
def type_cast(value)
# Convenience method. If you don't need separate behavior for
# +type_cast_from_database+ and +type_cast_from_user+, you can override
# this method instead. The default behavior of both methods is to call
# this one. See also +cast_value+
def type_cast(value) # :doc:
cast_value(value) unless value.nil?
end