aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/attributes.rb
blob: b263a89d790a6052fe0078db7bca1c634d77908b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
module ActiveRecord
  module Attributes # :nodoc:
    extend ActiveSupport::Concern

    Type = ActiveRecord::Type

    included do
      class_attribute :user_provided_columns, instance_accessor: false # :internal:
      class_attribute :user_provided_defaults, instance_accessor: false # :internal:
      self.user_provided_columns = {}
      self.user_provided_defaults = {}

      delegate :persistable_attribute_names, to: :class
    end

    module ClassMethods # :nodoc:
      # Defines or overrides a 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.
      #
      # +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.
      #
      # ==== Examples
      #
      # The type detected by Active Record can be overridden.
      #
      #   # 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
      #     attribute :price_in_cents, Type::Integer.new
      #   end
      #
      #   # 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.
      #
      #   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
      #     attribute :price_in_cents, MoneyType.new
      #   end
      #
      #   store_listing = StoreListing.new(price_in_cents: '$10.00')
      #   store_listing.price_in_cents # => 1000
      def attribute(name, cast_type, options = {})
        name = name.to_s
        clear_caches_calculated_from_columns
        # Assign a new hash to ensure that subclasses do not share a hash
        self.user_provided_columns = user_provided_columns.merge(name => cast_type)

        if options.key?(:default)
          self.user_provided_defaults = user_provided_defaults.merge(name => options[:default])
        end
      end

      # Returns an array of column objects for the table associated with this class.
      def columns
        @columns ||= add_user_provided_columns(connection.schema_cache.columns(table_name))
      end

      # Returns a hash of column objects for the table associated with this class.
      def columns_hash
        @columns_hash ||= Hash[columns.map { |c| [c.name, c] }]
      end

      def persistable_attribute_names # :nodoc:
        @persistable_attribute_names ||= connection.schema_cache.columns_hash(table_name).keys
      end

      def reset_column_information # :nodoc:
        super
        clear_caches_calculated_from_columns
      end

      private

      def add_user_provided_columns(schema_columns)
        existing_columns = schema_columns.map do |column|
          new_type = user_provided_columns[column.name]
          if new_type
            column.with_type(new_type)
          else
            column
          end
        end

        existing_column_names = existing_columns.map(&:name)
        new_columns = user_provided_columns.except(*existing_column_names).map do |(name, type)|
          connection.new_column(name, nil, type)
        end

        existing_columns + new_columns
      end

      def clear_caches_calculated_from_columns
        @arel_table = nil
        @attributes_builder = nil
        @column_names = nil
        @column_types = nil
        @columns = nil
        @columns_hash = nil
        @content_columns = nil
        @default_attributes = nil
        @persistable_attribute_names = nil
      end

      def raw_default_values
        super.merge(user_provided_defaults)
      end
    end
  end
end