Skip to content

Commit

Permalink
Docs pass for the attributes API
Browse files Browse the repository at this point in the history
  • Loading branch information
sgrif committed Feb 6, 2015
1 parent b71e08f commit 8c752c7
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 39 deletions.
128 changes: 112 additions & 16 deletions activerecord/lib/active_record/attributes.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module ActiveRecord
module Attributes # :nodoc:
module Attributes
extend ActiveSupport::Concern

Type = ActiveRecord::Type
Expand All @@ -9,21 +9,31 @@ module Attributes # :nodoc:
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.
#
# +name+ The name of the methods to define attribute methods for, and the column which
# this will persist to.
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.
#
# +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+ 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+.
#
# +default+ is the default value that the column should use on a new record.
# +array+ (PG only) specifies that the type should be an array (see the examples below)

This comment has been minimized.

Copy link
@rafaelfranca

rafaelfranca Feb 6, 2015

Member

missing .

#
# +range+ (PG only) specifies that the type should be a range (see the examples below)

This comment has been minimized.

Copy link
@rafaelfranca

rafaelfranca Feb 6, 2015

Member

missing .

#
# ==== Examples
#
Expand All @@ -50,11 +60,35 @@ module ClassMethods # :nodoc:
# # 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)
Expand All @@ -73,6 +107,51 @@ module ClassMethods # :nodoc:
#
# 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
Expand All @@ -83,6 +162,23 @@ def attribute(name, cast_type, **options)
)
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

This comment has been minimized.

Copy link
@rafaelfranca

rafaelfranca Feb 6, 2015

Member

missing ..

#
# +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+

This comment has been minimized.

Copy link
@rafaelfranca

rafaelfranca Feb 6, 2015

Member

missing ..

def define_attribute(
name,
cast_type,
Expand All @@ -93,7 +189,7 @@ def define_attribute(
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)
Expand Down
65 changes: 42 additions & 23 deletions activerecord/lib/active_record/type/value.rb
Original file line number Diff line number Diff line change
@@ -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+

This comment has been minimized.

Copy link
@rafaelfranca

rafaelfranca Feb 6, 2015

Member

Missing ..

#
# +value+ The raw input, as provided from the database

This comment has been minimized.

Copy link
@rafaelfranca

rafaelfranca Feb 6, 2015

Member

Missing ..

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.
#
# The return value of this method will be returned from
# +ActiveRecord::AttributeMethods::Read#read_attribute+. See also:
# +type_cast+ and +cast_value+

This comment has been minimized.

Copy link
@rafaelfranca

rafaelfranca Feb 6, 2015

Member

Missing ..

#
# Classes which do not need separate type casting behavior for database
# and user provided values should override +type_cast+ or +cast_value+
# instead.
# +value+ The raw input, as provided to the attribute setter.
def type_cast_from_user(value)
type_cast(value)
end
Expand Down Expand Up @@ -72,10 +74,23 @@ def changed?(old_value, new_value, _new_value_before_type_cast)
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

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

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+

This comment has been minimized.

Copy link
@rafaelfranca

rafaelfranca Feb 6, 2015

Member

missing ..

def type_cast(value) # :doc:
cast_value(value) unless value.nil?
end

Expand Down

1 comment on commit 8c752c7

@sgrif
Copy link
Contributor Author

@sgrif sgrif commented on 8c752c7 Jun 18, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is the best commit to reference, but ping #20612

Please sign in to comment.