Permalink
Browse files

Docs pass for the attributes API

  • Loading branch information...
sgrif committed Feb 6, 2015
1 parent b71e08f commit 8c752c7ac739d5a86d4136ab1e9d0142c4041e58
Showing with 154 additions and 39 deletions.
  1. +112 −16 activerecord/lib/active_record/attributes.rb
  2. +42 −23 activerecord/lib/active_record/type/value.rb
@@ -1,5 +1,5 @@
module ActiveRecord
- module Attributes # :nodoc:
+ module Attributes
extend ActiveSupport::Concern
Type = ActiveRecord::Type
@@ -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.

Show comment
Hide comment
@rafaelfranca

rafaelfranca Feb 6, 2015

Member

missing .

@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.

Show comment
Hide comment
@rafaelfranca

rafaelfranca Feb 6, 2015

Member

missing .

@rafaelfranca

rafaelfranca Feb 6, 2015

Member

missing .

#
# ==== Examples
#
@@ -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)
@@ -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
@@ -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.

Show comment
Hide comment
@rafaelfranca

rafaelfranca Feb 6, 2015

Member

missing ..

@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.

Show comment
Hide comment
@rafaelfranca

rafaelfranca Feb 6, 2015

Member

missing ..

@rafaelfranca

rafaelfranca Feb 6, 2015

Member

missing ..

def define_attribute(
name,
cast_type,
@@ -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)
@@ -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.

Show comment
Hide comment
@rafaelfranca

rafaelfranca Feb 6, 2015

Member

Missing ..

@rafaelfranca

rafaelfranca Feb 6, 2015

Member

Missing ..

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

This comment has been minimized.

Show comment
Hide comment
@rafaelfranca

rafaelfranca Feb 6, 2015

Member

Missing ..

@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.

Show comment
Hide comment
@rafaelfranca

rafaelfranca Feb 6, 2015

Member

Missing ..

@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
@@ -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
@@ -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.

Show comment
Hide comment
@rafaelfranca

rafaelfranca Feb 6, 2015

Member

missing ..

@rafaelfranca

rafaelfranca Feb 6, 2015

Member

missing ..

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

1 comment on commit 8c752c7

@sgrif

This comment has been minimized.

Show comment
Hide comment
@sgrif

sgrif Jun 18, 2015

Member

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

Member

sgrif commented on 8c752c7 Jun 18, 2015

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

Please sign in to comment.