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
|
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 = {}
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 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
@attributes_builder = nil
@column_names = nil
@column_types = nil
@columns = nil
@columns_hash = nil
@content_columns = nil
@default_attributes = nil
end
def raw_default_values
super.merge(user_provided_defaults)
end
end
end
end
|