attributes.rb 3.8 KB
Newer Older
1
module ActiveRecord
S
Sean Griffin 已提交
2
  module Attributes # :nodoc:
3 4
    extend ActiveSupport::Concern

5
    Type = ActiveRecord::Type
6

7
    included do
8 9
      class_attribute :user_provided_types, instance_accessor: false # :internal:
      self.user_provided_types = {}
10 11
    end

12
    module ClassMethods # :nodoc:
13
      # Defines or overrides an attribute on this model. This allows customization of
14 15 16
      # Active Record's type casting behavior, as well as adding support for user defined
      # types.
      #
17 18 19 20 21 22 23 24 25 26 27
      # +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:
      #
      # +default+ is the default value that the column should use on a new record.
      #
28 29
      # ==== Examples
      #
A
Anton Cherepanov 已提交
30
      # The type detected by Active Record can be overridden.
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
      #
      #   # db/schema.rb
      #   create_table :store_listings, force: true do |t|
      #     t.decimal :price_in_cents
      #   end
      #
      #   # app/models/store_listing.rb
      #   class StoreListing < ActiveRecord::Base
      #   end
      #
      #   store_listing = StoreListing.new(price_in_cents: '10.1')
      #
      #   # before
      #   store_listing.price_in_cents # => BigDecimal.new(10.1)
      #
      #   class StoreListing < ActiveRecord::Base
S
Sean Griffin 已提交
47
      #     attribute :price_in_cents, Type::Integer.new
48 49 50 51 52 53
      #   end
      #
      #   # after
      #   store_listing.price_in_cents # => 10
      #
      # 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 58 59 60 61 62 63 64 65 66 67 68 69 70
      # type objects inherit from an existing type, or the base value type.
      #
      #   class MoneyType < ActiveRecord::Type::Integer
      #     def type_cast(value)
      #       if value.include?('$')
      #         price_in_dollars = value.gsub(/\$/, '').to_f
      #         price_in_dollars * 100
      #       else
      #         value.to_i
      #       end
      #     end
      #   end
      #
      #   class StoreListing < ActiveRecord::Base
S
Sean Griffin 已提交
71
      #     attribute :price_in_cents, MoneyType.new
72 73 74 75
      #   end
      #
      #   store_listing = StoreListing.new(price_in_cents: '$10.00')
      #   store_listing.price_in_cents # => 1000
76
      def attribute(name, cast_type, **options)
77
        name = name.to_s
78
        reload_schema_from_cache
79

80
        self.user_provided_types = user_provided_types.merge(name => [cast_type, options])
81 82
      end

83 84 85 86 87 88 89 90
      def define_attribute(
        name,
        cast_type,
        default: NO_DEFAULT_PROVIDED,
        user_provided_default: true
      )
        attribute_types[name] = cast_type
        define_default_attribute(name, default, cast_type, from_user: user_provided_default)
91 92
      end

93
      def load_schema!
94
        super
95 96 97
        user_provided_types.each do |name, (type, options)|
          define_attribute(name, type, **options)
        end
98 99 100 101
      end

      private

102 103
      NO_DEFAULT_PROVIDED = Object.new # :nodoc:
      private_constant :NO_DEFAULT_PROVIDED
104

105 106 107 108 109 110 111
      def define_default_attribute(name, value, type, from_user:)
        if value == NO_DEFAULT_PROVIDED
          default_attribute = _default_attributes[name].with_type(type)
        elsif from_user
          default_attribute = Attribute.from_user(name, value, type)
        else
          default_attribute = Attribute.from_database(name, value, type)
112
        end
113
        _default_attributes[name] = default_attribute
114
      end
115 116 117
    end
  end
end