From 8c752c7ac739d5a86d4136ab1e9d0142c4041e58 Mon Sep 17 00:00:00 2001 From: Sean Griffin Date: Fri, 6 Feb 2015 13:02:03 -0700 Subject: [PATCH] Docs pass for the attributes API --- activerecord/lib/active_record/attributes.rb | 126 ++++++++++++++++--- activerecord/lib/active_record/type/value.rb | 65 ++++++---- 2 files changed, 153 insertions(+), 38 deletions(-) diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index f34e6cf912..53f54ebae9 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -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) diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb index 859b51ca90..7f41d525e1 100644 --- a/activerecord/lib/active_record/type/value.rb +++ b/activerecord/lib/active_record/type/value.rb @@ -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