activemodel 5.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +114 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +264 -0
  5. data/lib/active_model.rb +77 -0
  6. data/lib/active_model/attribute.rb +248 -0
  7. data/lib/active_model/attribute/user_provided_default.rb +52 -0
  8. data/lib/active_model/attribute_assignment.rb +57 -0
  9. data/lib/active_model/attribute_methods.rb +478 -0
  10. data/lib/active_model/attribute_mutation_tracker.rb +124 -0
  11. data/lib/active_model/attribute_set.rb +114 -0
  12. data/lib/active_model/attribute_set/builder.rb +126 -0
  13. data/lib/active_model/attribute_set/yaml_encoder.rb +41 -0
  14. data/lib/active_model/attributes.rb +111 -0
  15. data/lib/active_model/callbacks.rb +153 -0
  16. data/lib/active_model/conversion.rb +111 -0
  17. data/lib/active_model/dirty.rb +343 -0
  18. data/lib/active_model/errors.rb +517 -0
  19. data/lib/active_model/forbidden_attributes_protection.rb +31 -0
  20. data/lib/active_model/gem_version.rb +17 -0
  21. data/lib/active_model/lint.rb +118 -0
  22. data/lib/active_model/locale/en.yml +36 -0
  23. data/lib/active_model/model.rb +99 -0
  24. data/lib/active_model/naming.rb +318 -0
  25. data/lib/active_model/railtie.rb +14 -0
  26. data/lib/active_model/secure_password.rb +129 -0
  27. data/lib/active_model/serialization.rb +192 -0
  28. data/lib/active_model/serializers/json.rb +146 -0
  29. data/lib/active_model/translation.rb +70 -0
  30. data/lib/active_model/type.rb +53 -0
  31. data/lib/active_model/type/big_integer.rb +15 -0
  32. data/lib/active_model/type/binary.rb +52 -0
  33. data/lib/active_model/type/boolean.rb +38 -0
  34. data/lib/active_model/type/date.rb +57 -0
  35. data/lib/active_model/type/date_time.rb +51 -0
  36. data/lib/active_model/type/decimal.rb +70 -0
  37. data/lib/active_model/type/float.rb +36 -0
  38. data/lib/active_model/type/helpers.rb +7 -0
  39. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +41 -0
  40. data/lib/active_model/type/helpers/mutable.rb +20 -0
  41. data/lib/active_model/type/helpers/numeric.rb +37 -0
  42. data/lib/active_model/type/helpers/time_value.rb +68 -0
  43. data/lib/active_model/type/helpers/timezone.rb +19 -0
  44. data/lib/active_model/type/immutable_string.rb +32 -0
  45. data/lib/active_model/type/integer.rb +70 -0
  46. data/lib/active_model/type/registry.rb +70 -0
  47. data/lib/active_model/type/string.rb +26 -0
  48. data/lib/active_model/type/time.rb +51 -0
  49. data/lib/active_model/type/value.rb +126 -0
  50. data/lib/active_model/validations.rb +439 -0
  51. data/lib/active_model/validations/absence.rb +33 -0
  52. data/lib/active_model/validations/acceptance.rb +106 -0
  53. data/lib/active_model/validations/callbacks.rb +122 -0
  54. data/lib/active_model/validations/clusivity.rb +54 -0
  55. data/lib/active_model/validations/confirmation.rb +80 -0
  56. data/lib/active_model/validations/exclusion.rb +49 -0
  57. data/lib/active_model/validations/format.rb +114 -0
  58. data/lib/active_model/validations/helper_methods.rb +15 -0
  59. data/lib/active_model/validations/inclusion.rb +47 -0
  60. data/lib/active_model/validations/length.rb +129 -0
  61. data/lib/active_model/validations/numericality.rb +189 -0
  62. data/lib/active_model/validations/presence.rb +39 -0
  63. data/lib/active_model/validations/validates.rb +174 -0
  64. data/lib/active_model/validations/with.rb +147 -0
  65. data/lib/active_model/validator.rb +183 -0
  66. data/lib/active_model/version.rb +10 -0
  67. metadata +125 -0
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/object/duplicable"
4
+
5
+ module ActiveModel
6
+ class Attribute # :nodoc:
7
+ class << self
8
+ def from_database(name, value, type)
9
+ FromDatabase.new(name, value, type)
10
+ end
11
+
12
+ def from_user(name, value, type, original_attribute = nil)
13
+ FromUser.new(name, value, type, original_attribute)
14
+ end
15
+
16
+ def with_cast_value(name, value, type)
17
+ WithCastValue.new(name, value, type)
18
+ end
19
+
20
+ def null(name)
21
+ Null.new(name)
22
+ end
23
+
24
+ def uninitialized(name, type)
25
+ Uninitialized.new(name, type)
26
+ end
27
+ end
28
+
29
+ attr_reader :name, :value_before_type_cast, :type
30
+
31
+ # This method should not be called directly.
32
+ # Use #from_database or #from_user
33
+ def initialize(name, value_before_type_cast, type, original_attribute = nil)
34
+ @name = name
35
+ @value_before_type_cast = value_before_type_cast
36
+ @type = type
37
+ @original_attribute = original_attribute
38
+ end
39
+
40
+ def value
41
+ # `defined?` is cheaper than `||=` when we get back falsy values
42
+ @value = type_cast(value_before_type_cast) unless defined?(@value)
43
+ @value
44
+ end
45
+
46
+ def original_value
47
+ if assigned?
48
+ original_attribute.original_value
49
+ else
50
+ type_cast(value_before_type_cast)
51
+ end
52
+ end
53
+
54
+ def value_for_database
55
+ type.serialize(value)
56
+ end
57
+
58
+ def changed?
59
+ changed_from_assignment? || changed_in_place?
60
+ end
61
+
62
+ def changed_in_place?
63
+ has_been_read? && type.changed_in_place?(original_value_for_database, value)
64
+ end
65
+
66
+ def forgetting_assignment
67
+ with_value_from_database(value_for_database)
68
+ end
69
+
70
+ def with_value_from_user(value)
71
+ type.assert_valid_value(value)
72
+ self.class.from_user(name, value, type, original_attribute || self)
73
+ end
74
+
75
+ def with_value_from_database(value)
76
+ self.class.from_database(name, value, type)
77
+ end
78
+
79
+ def with_cast_value(value)
80
+ self.class.with_cast_value(name, value, type)
81
+ end
82
+
83
+ def with_type(type)
84
+ if changed_in_place?
85
+ with_value_from_user(value).with_type(type)
86
+ else
87
+ self.class.new(name, value_before_type_cast, type, original_attribute)
88
+ end
89
+ end
90
+
91
+ def type_cast(*)
92
+ raise NotImplementedError
93
+ end
94
+
95
+ def initialized?
96
+ true
97
+ end
98
+
99
+ def came_from_user?
100
+ false
101
+ end
102
+
103
+ def has_been_read?
104
+ defined?(@value)
105
+ end
106
+
107
+ def ==(other)
108
+ self.class == other.class &&
109
+ name == other.name &&
110
+ value_before_type_cast == other.value_before_type_cast &&
111
+ type == other.type
112
+ end
113
+ alias eql? ==
114
+
115
+ def hash
116
+ [self.class, name, value_before_type_cast, type].hash
117
+ end
118
+
119
+ def init_with(coder)
120
+ @name = coder["name"]
121
+ @value_before_type_cast = coder["value_before_type_cast"]
122
+ @type = coder["type"]
123
+ @original_attribute = coder["original_attribute"]
124
+ @value = coder["value"] if coder.map.key?("value")
125
+ end
126
+
127
+ def encode_with(coder)
128
+ coder["name"] = name
129
+ coder["value_before_type_cast"] = value_before_type_cast unless value_before_type_cast.nil?
130
+ coder["type"] = type if type
131
+ coder["original_attribute"] = original_attribute if original_attribute
132
+ coder["value"] = value if defined?(@value)
133
+ end
134
+
135
+ protected
136
+
137
+ attr_reader :original_attribute
138
+ alias_method :assigned?, :original_attribute
139
+
140
+ def original_value_for_database
141
+ if assigned?
142
+ original_attribute.original_value_for_database
143
+ else
144
+ _original_value_for_database
145
+ end
146
+ end
147
+
148
+ private
149
+ def initialize_dup(other)
150
+ if defined?(@value) && @value.duplicable?
151
+ @value = @value.dup
152
+ end
153
+ end
154
+
155
+ def changed_from_assignment?
156
+ assigned? && type.changed?(original_value, value, value_before_type_cast)
157
+ end
158
+
159
+ def _original_value_for_database
160
+ type.serialize(original_value)
161
+ end
162
+
163
+ class FromDatabase < Attribute # :nodoc:
164
+ def type_cast(value)
165
+ type.deserialize(value)
166
+ end
167
+
168
+ def _original_value_for_database
169
+ value_before_type_cast
170
+ end
171
+ end
172
+
173
+ class FromUser < Attribute # :nodoc:
174
+ def type_cast(value)
175
+ type.cast(value)
176
+ end
177
+
178
+ def came_from_user?
179
+ !type.value_constructed_by_mass_assignment?(value_before_type_cast)
180
+ end
181
+ end
182
+
183
+ class WithCastValue < Attribute # :nodoc:
184
+ def type_cast(value)
185
+ value
186
+ end
187
+
188
+ def changed_in_place?
189
+ false
190
+ end
191
+ end
192
+
193
+ class Null < Attribute # :nodoc:
194
+ def initialize(name)
195
+ super(name, nil, Type.default_value)
196
+ end
197
+
198
+ def type_cast(*)
199
+ nil
200
+ end
201
+
202
+ def with_type(type)
203
+ self.class.with_cast_value(name, nil, type)
204
+ end
205
+
206
+ def with_value_from_database(value)
207
+ raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
208
+ end
209
+ alias_method :with_value_from_user, :with_value_from_database
210
+ alias_method :with_cast_value, :with_value_from_database
211
+ end
212
+
213
+ class Uninitialized < Attribute # :nodoc:
214
+ UNINITIALIZED_ORIGINAL_VALUE = Object.new
215
+
216
+ def initialize(name, type)
217
+ super(name, nil, type)
218
+ end
219
+
220
+ def value
221
+ if block_given?
222
+ yield name
223
+ end
224
+ end
225
+
226
+ def original_value
227
+ UNINITIALIZED_ORIGINAL_VALUE
228
+ end
229
+
230
+ def value_for_database
231
+ end
232
+
233
+ def initialized?
234
+ false
235
+ end
236
+
237
+ def forgetting_assignment
238
+ dup
239
+ end
240
+
241
+ def with_type(type)
242
+ self.class.new(name, type)
243
+ end
244
+ end
245
+
246
+ private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue
247
+ end
248
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model/attribute"
4
+
5
+ module ActiveModel
6
+ class Attribute # :nodoc:
7
+ class UserProvidedDefault < FromUser # :nodoc:
8
+ def initialize(name, value, type, database_default)
9
+ @user_provided_value = value
10
+ super(name, value, type, database_default)
11
+ end
12
+
13
+ def value_before_type_cast
14
+ if user_provided_value.is_a?(Proc)
15
+ @memoized_value_before_type_cast ||= user_provided_value.call
16
+ else
17
+ @user_provided_value
18
+ end
19
+ end
20
+
21
+ def with_type(type)
22
+ self.class.new(name, user_provided_value, type, original_attribute)
23
+ end
24
+
25
+ def marshal_dump
26
+ result = [
27
+ name,
28
+ value_before_type_cast,
29
+ type,
30
+ original_attribute,
31
+ ]
32
+ result << value if defined?(@value)
33
+ result
34
+ end
35
+
36
+ def marshal_load(values)
37
+ name, user_provided_value, type, original_attribute, value = values
38
+ @name = name
39
+ @user_provided_value = user_provided_value
40
+ @type = type
41
+ @original_attribute = original_attribute
42
+ if values.length == 5
43
+ @value = value
44
+ end
45
+ end
46
+
47
+ protected
48
+
49
+ attr_reader :user_provided_value
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/keys"
4
+
5
+ module ActiveModel
6
+ module AttributeAssignment
7
+ include ActiveModel::ForbiddenAttributesProtection
8
+
9
+ # Allows you to set all the attributes by passing in a hash of attributes with
10
+ # keys matching the attribute names.
11
+ #
12
+ # If the passed hash responds to <tt>permitted?</tt> method and the return value
13
+ # of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt>
14
+ # exception is raised.
15
+ #
16
+ # class Cat
17
+ # include ActiveModel::AttributeAssignment
18
+ # attr_accessor :name, :status
19
+ # end
20
+ #
21
+ # cat = Cat.new
22
+ # cat.assign_attributes(name: "Gorby", status: "yawning")
23
+ # cat.name # => 'Gorby'
24
+ # cat.status # => 'yawning'
25
+ # cat.assign_attributes(status: "sleeping")
26
+ # cat.name # => 'Gorby'
27
+ # cat.status # => 'sleeping'
28
+ def assign_attributes(new_attributes)
29
+ if !new_attributes.respond_to?(:stringify_keys)
30
+ raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
31
+ end
32
+ return if new_attributes.empty?
33
+
34
+ attributes = new_attributes.stringify_keys
35
+ _assign_attributes(sanitize_for_mass_assignment(attributes))
36
+ end
37
+
38
+ alias attributes= assign_attributes
39
+
40
+ private
41
+
42
+ def _assign_attributes(attributes)
43
+ attributes.each do |k, v|
44
+ _assign_attribute(k, v)
45
+ end
46
+ end
47
+
48
+ def _assign_attribute(k, v)
49
+ setter = :"#{k}="
50
+ if respond_to?(setter)
51
+ public_send(setter, v)
52
+ else
53
+ raise UnknownAttributeError.new(self, k)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,478 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/map"
4
+
5
+ module ActiveModel
6
+ # Raised when an attribute is not defined.
7
+ #
8
+ # class User < ActiveRecord::Base
9
+ # has_many :pets
10
+ # end
11
+ #
12
+ # user = User.first
13
+ # user.pets.select(:id).first.user_id
14
+ # # => ActiveModel::MissingAttributeError: missing attribute: user_id
15
+ class MissingAttributeError < NoMethodError
16
+ end
17
+
18
+ # == Active \Model \Attribute \Methods
19
+ #
20
+ # Provides a way to add prefixes and suffixes to your methods as
21
+ # well as handling the creation of <tt>ActiveRecord::Base</tt>-like
22
+ # class methods such as +table_name+.
23
+ #
24
+ # The requirements to implement <tt>ActiveModel::AttributeMethods</tt> are to:
25
+ #
26
+ # * <tt>include ActiveModel::AttributeMethods</tt> in your class.
27
+ # * Call each of its methods you want to add, such as +attribute_method_suffix+
28
+ # or +attribute_method_prefix+.
29
+ # * Call +define_attribute_methods+ after the other methods are called.
30
+ # * Define the various generic +_attribute+ methods that you have declared.
31
+ # * Define an +attributes+ method which returns a hash with each
32
+ # attribute name in your model as hash key and the attribute value as hash value.
33
+ # Hash keys must be strings.
34
+ #
35
+ # A minimal implementation could be:
36
+ #
37
+ # class Person
38
+ # include ActiveModel::AttributeMethods
39
+ #
40
+ # attribute_method_affix prefix: 'reset_', suffix: '_to_default!'
41
+ # attribute_method_suffix '_contrived?'
42
+ # attribute_method_prefix 'clear_'
43
+ # define_attribute_methods :name
44
+ #
45
+ # attr_accessor :name
46
+ #
47
+ # def attributes
48
+ # { 'name' => @name }
49
+ # end
50
+ #
51
+ # private
52
+ #
53
+ # def attribute_contrived?(attr)
54
+ # true
55
+ # end
56
+ #
57
+ # def clear_attribute(attr)
58
+ # send("#{attr}=", nil)
59
+ # end
60
+ #
61
+ # def reset_attribute_to_default!(attr)
62
+ # send("#{attr}=", 'Default Name')
63
+ # end
64
+ # end
65
+ module AttributeMethods
66
+ extend ActiveSupport::Concern
67
+
68
+ NAME_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?=]?\z/
69
+ CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/
70
+
71
+ included do
72
+ class_attribute :attribute_aliases, instance_writer: false, default: {}
73
+ class_attribute :attribute_method_matchers, instance_writer: false, default: [ ClassMethods::AttributeMethodMatcher.new ]
74
+ end
75
+
76
+ module ClassMethods
77
+ # Declares a method available for all attributes with the given prefix.
78
+ # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
79
+ #
80
+ # #{prefix}#{attr}(*args, &block)
81
+ #
82
+ # to
83
+ #
84
+ # #{prefix}attribute(#{attr}, *args, &block)
85
+ #
86
+ # An instance method <tt>#{prefix}attribute</tt> must exist and accept
87
+ # at least the +attr+ argument.
88
+ #
89
+ # class Person
90
+ # include ActiveModel::AttributeMethods
91
+ #
92
+ # attr_accessor :name
93
+ # attribute_method_prefix 'clear_'
94
+ # define_attribute_methods :name
95
+ #
96
+ # private
97
+ #
98
+ # def clear_attribute(attr)
99
+ # send("#{attr}=", nil)
100
+ # end
101
+ # end
102
+ #
103
+ # person = Person.new
104
+ # person.name = 'Bob'
105
+ # person.name # => "Bob"
106
+ # person.clear_name
107
+ # person.name # => nil
108
+ def attribute_method_prefix(*prefixes)
109
+ self.attribute_method_matchers += prefixes.map! { |prefix| AttributeMethodMatcher.new prefix: prefix }
110
+ undefine_attribute_methods
111
+ end
112
+
113
+ # Declares a method available for all attributes with the given suffix.
114
+ # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
115
+ #
116
+ # #{attr}#{suffix}(*args, &block)
117
+ #
118
+ # to
119
+ #
120
+ # attribute#{suffix}(#{attr}, *args, &block)
121
+ #
122
+ # An <tt>attribute#{suffix}</tt> instance method must exist and accept at
123
+ # least the +attr+ argument.
124
+ #
125
+ # class Person
126
+ # include ActiveModel::AttributeMethods
127
+ #
128
+ # attr_accessor :name
129
+ # attribute_method_suffix '_short?'
130
+ # define_attribute_methods :name
131
+ #
132
+ # private
133
+ #
134
+ # def attribute_short?(attr)
135
+ # send(attr).length < 5
136
+ # end
137
+ # end
138
+ #
139
+ # person = Person.new
140
+ # person.name = 'Bob'
141
+ # person.name # => "Bob"
142
+ # person.name_short? # => true
143
+ def attribute_method_suffix(*suffixes)
144
+ self.attribute_method_matchers += suffixes.map! { |suffix| AttributeMethodMatcher.new suffix: suffix }
145
+ undefine_attribute_methods
146
+ end
147
+
148
+ # Declares a method available for all attributes with the given prefix
149
+ # and suffix. Uses +method_missing+ and <tt>respond_to?</tt> to rewrite
150
+ # the method.
151
+ #
152
+ # #{prefix}#{attr}#{suffix}(*args, &block)
153
+ #
154
+ # to
155
+ #
156
+ # #{prefix}attribute#{suffix}(#{attr}, *args, &block)
157
+ #
158
+ # An <tt>#{prefix}attribute#{suffix}</tt> instance method must exist and
159
+ # accept at least the +attr+ argument.
160
+ #
161
+ # class Person
162
+ # include ActiveModel::AttributeMethods
163
+ #
164
+ # attr_accessor :name
165
+ # attribute_method_affix prefix: 'reset_', suffix: '_to_default!'
166
+ # define_attribute_methods :name
167
+ #
168
+ # private
169
+ #
170
+ # def reset_attribute_to_default!(attr)
171
+ # send("#{attr}=", 'Default Name')
172
+ # end
173
+ # end
174
+ #
175
+ # person = Person.new
176
+ # person.name # => 'Gem'
177
+ # person.reset_name_to_default!
178
+ # person.name # => 'Default Name'
179
+ def attribute_method_affix(*affixes)
180
+ self.attribute_method_matchers += affixes.map! { |affix| AttributeMethodMatcher.new prefix: affix[:prefix], suffix: affix[:suffix] }
181
+ undefine_attribute_methods
182
+ end
183
+
184
+ # Allows you to make aliases for attributes.
185
+ #
186
+ # class Person
187
+ # include ActiveModel::AttributeMethods
188
+ #
189
+ # attr_accessor :name
190
+ # attribute_method_suffix '_short?'
191
+ # define_attribute_methods :name
192
+ #
193
+ # alias_attribute :nickname, :name
194
+ #
195
+ # private
196
+ #
197
+ # def attribute_short?(attr)
198
+ # send(attr).length < 5
199
+ # end
200
+ # end
201
+ #
202
+ # person = Person.new
203
+ # person.name = 'Bob'
204
+ # person.name # => "Bob"
205
+ # person.nickname # => "Bob"
206
+ # person.name_short? # => true
207
+ # person.nickname_short? # => true
208
+ def alias_attribute(new_name, old_name)
209
+ self.attribute_aliases = attribute_aliases.merge(new_name.to_s => old_name.to_s)
210
+ attribute_method_matchers.each do |matcher|
211
+ matcher_new = matcher.method_name(new_name).to_s
212
+ matcher_old = matcher.method_name(old_name).to_s
213
+ define_proxy_call false, self, matcher_new, matcher_old
214
+ end
215
+ end
216
+
217
+ # Is +new_name+ an alias?
218
+ def attribute_alias?(new_name)
219
+ attribute_aliases.key? new_name.to_s
220
+ end
221
+
222
+ # Returns the original name for the alias +name+
223
+ def attribute_alias(name)
224
+ attribute_aliases[name.to_s]
225
+ end
226
+
227
+ # Declares the attributes that should be prefixed and suffixed by
228
+ # <tt>ActiveModel::AttributeMethods</tt>.
229
+ #
230
+ # To use, pass attribute names (as strings or symbols). Be sure to declare
231
+ # +define_attribute_methods+ after you define any prefix, suffix or affix
232
+ # methods, or they will not hook in.
233
+ #
234
+ # class Person
235
+ # include ActiveModel::AttributeMethods
236
+ #
237
+ # attr_accessor :name, :age, :address
238
+ # attribute_method_prefix 'clear_'
239
+ #
240
+ # # Call to define_attribute_methods must appear after the
241
+ # # attribute_method_prefix, attribute_method_suffix or
242
+ # # attribute_method_affix declarations.
243
+ # define_attribute_methods :name, :age, :address
244
+ #
245
+ # private
246
+ #
247
+ # def clear_attribute(attr)
248
+ # send("#{attr}=", nil)
249
+ # end
250
+ # end
251
+ def define_attribute_methods(*attr_names)
252
+ attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
253
+ end
254
+
255
+ # Declares an attribute that should be prefixed and suffixed by
256
+ # <tt>ActiveModel::AttributeMethods</tt>.
257
+ #
258
+ # To use, pass an attribute name (as string or symbol). Be sure to declare
259
+ # +define_attribute_method+ after you define any prefix, suffix or affix
260
+ # method, or they will not hook in.
261
+ #
262
+ # class Person
263
+ # include ActiveModel::AttributeMethods
264
+ #
265
+ # attr_accessor :name
266
+ # attribute_method_suffix '_short?'
267
+ #
268
+ # # Call to define_attribute_method must appear after the
269
+ # # attribute_method_prefix, attribute_method_suffix or
270
+ # # attribute_method_affix declarations.
271
+ # define_attribute_method :name
272
+ #
273
+ # private
274
+ #
275
+ # def attribute_short?(attr)
276
+ # send(attr).length < 5
277
+ # end
278
+ # end
279
+ #
280
+ # person = Person.new
281
+ # person.name = 'Bob'
282
+ # person.name # => "Bob"
283
+ # person.name_short? # => true
284
+ def define_attribute_method(attr_name)
285
+ attribute_method_matchers.each do |matcher|
286
+ method_name = matcher.method_name(attr_name)
287
+
288
+ unless instance_method_already_implemented?(method_name)
289
+ generate_method = "define_method_#{matcher.method_missing_target}"
290
+
291
+ if respond_to?(generate_method, true)
292
+ send(generate_method, attr_name.to_s)
293
+ else
294
+ define_proxy_call true, generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s
295
+ end
296
+ end
297
+ end
298
+ attribute_method_matchers_cache.clear
299
+ end
300
+
301
+ # Removes all the previously dynamically defined methods from the class.
302
+ #
303
+ # class Person
304
+ # include ActiveModel::AttributeMethods
305
+ #
306
+ # attr_accessor :name
307
+ # attribute_method_suffix '_short?'
308
+ # define_attribute_method :name
309
+ #
310
+ # private
311
+ #
312
+ # def attribute_short?(attr)
313
+ # send(attr).length < 5
314
+ # end
315
+ # end
316
+ #
317
+ # person = Person.new
318
+ # person.name = 'Bob'
319
+ # person.name_short? # => true
320
+ #
321
+ # Person.undefine_attribute_methods
322
+ #
323
+ # person.name_short? # => NoMethodError
324
+ def undefine_attribute_methods
325
+ generated_attribute_methods.module_eval do
326
+ instance_methods.each { |m| undef_method(m) }
327
+ end
328
+ attribute_method_matchers_cache.clear
329
+ end
330
+
331
+ private
332
+ def generated_attribute_methods
333
+ @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
334
+ end
335
+
336
+ def instance_method_already_implemented?(method_name)
337
+ generated_attribute_methods.method_defined?(method_name)
338
+ end
339
+
340
+ # The methods +method_missing+ and +respond_to?+ of this module are
341
+ # invoked often in a typical rails, both of which invoke the method
342
+ # +matched_attribute_method+. The latter method iterates through an
343
+ # array doing regular expression matches, which results in a lot of
344
+ # object creations. Most of the time it returns a +nil+ match. As the
345
+ # match result is always the same given a +method_name+, this cache is
346
+ # used to alleviate the GC, which ultimately also speeds up the app
347
+ # significantly (in our case our test suite finishes 10% faster with
348
+ # this cache).
349
+ def attribute_method_matchers_cache
350
+ @attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4)
351
+ end
352
+
353
+ def attribute_method_matchers_matching(method_name)
354
+ attribute_method_matchers_cache.compute_if_absent(method_name) do
355
+ # Must try to match prefixes/suffixes first, or else the matcher with no prefix/suffix
356
+ # will match every time.
357
+ matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
358
+ matchers.map { |method| method.match(method_name) }.compact
359
+ end
360
+ end
361
+
362
+ # Define a method `name` in `mod` that dispatches to `send`
363
+ # using the given `extra` args. This falls back on `define_method`
364
+ # and `send` if the given names cannot be compiled.
365
+ def define_proxy_call(include_private, mod, name, send, *extra)
366
+ defn = if NAME_COMPILABLE_REGEXP.match?(name)
367
+ "def #{name}(*args)"
368
+ else
369
+ "define_method(:'#{name}') do |*args|"
370
+ end
371
+
372
+ extra = (extra.map!(&:inspect) << "*args").join(", ".freeze)
373
+
374
+ target = if CALL_COMPILABLE_REGEXP.match?(send)
375
+ "#{"self." unless include_private}#{send}(#{extra})"
376
+ else
377
+ "send(:'#{send}', #{extra})"
378
+ end
379
+
380
+ mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
381
+ #{defn}
382
+ #{target}
383
+ end
384
+ RUBY
385
+ end
386
+
387
+ class AttributeMethodMatcher #:nodoc:
388
+ attr_reader :prefix, :suffix, :method_missing_target
389
+
390
+ AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
391
+
392
+ def initialize(options = {})
393
+ @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
394
+ @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
395
+ @method_missing_target = "#{@prefix}attribute#{@suffix}"
396
+ @method_name = "#{prefix}%s#{suffix}"
397
+ end
398
+
399
+ def match(method_name)
400
+ if @regex =~ method_name
401
+ AttributeMethodMatch.new(method_missing_target, $1, method_name)
402
+ end
403
+ end
404
+
405
+ def method_name(attr_name)
406
+ @method_name % attr_name
407
+ end
408
+
409
+ def plain?
410
+ prefix.empty? && suffix.empty?
411
+ end
412
+ end
413
+ end
414
+
415
+ # Allows access to the object attributes, which are held in the hash
416
+ # returned by <tt>attributes</tt>, as though they were first-class
417
+ # methods. So a +Person+ class with a +name+ attribute can for example use
418
+ # <tt>Person#name</tt> and <tt>Person#name=</tt> and never directly use
419
+ # the attributes hash -- except for multiple assignments with
420
+ # <tt>ActiveRecord::Base#attributes=</tt>.
421
+ #
422
+ # It's also possible to instantiate related objects, so a <tt>Client</tt>
423
+ # class belonging to the +clients+ table with a +master_id+ foreign key
424
+ # can instantiate master through <tt>Client#master</tt>.
425
+ def method_missing(method, *args, &block)
426
+ if respond_to_without_attributes?(method, true)
427
+ super
428
+ else
429
+ match = matched_attribute_method(method.to_s)
430
+ match ? attribute_missing(match, *args, &block) : super
431
+ end
432
+ end
433
+
434
+ # +attribute_missing+ is like +method_missing+, but for attributes. When
435
+ # +method_missing+ is called we check to see if there is a matching
436
+ # attribute method. If so, we tell +attribute_missing+ to dispatch the
437
+ # attribute. This method can be overloaded to customize the behavior.
438
+ def attribute_missing(match, *args, &block)
439
+ __send__(match.target, match.attr_name, *args, &block)
440
+ end
441
+
442
+ # A +Person+ instance with a +name+ attribute can ask
443
+ # <tt>person.respond_to?(:name)</tt>, <tt>person.respond_to?(:name=)</tt>,
444
+ # and <tt>person.respond_to?(:name?)</tt> which will all return +true+.
445
+ alias :respond_to_without_attributes? :respond_to?
446
+ def respond_to?(method, include_private_methods = false)
447
+ if super
448
+ true
449
+ elsif !include_private_methods && super(method, true)
450
+ # If we're here then we haven't found among non-private methods
451
+ # but found among all methods. Which means that the given method is private.
452
+ false
453
+ else
454
+ !matched_attribute_method(method.to_s).nil?
455
+ end
456
+ end
457
+
458
+ private
459
+ def attribute_method?(attr_name)
460
+ respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
461
+ end
462
+
463
+ # Returns a struct representing the matching attribute method.
464
+ # The struct's attributes are prefix, base and suffix.
465
+ def matched_attribute_method(method_name)
466
+ matches = self.class.send(:attribute_method_matchers_matching, method_name)
467
+ matches.detect { |match| attribute_method?(match.attr_name) }
468
+ end
469
+
470
+ def missing_attribute(attr_name, stack)
471
+ raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack
472
+ end
473
+
474
+ def _read_attribute(attr)
475
+ __send__(attr)
476
+ end
477
+ end
478
+ end