activemodel 5.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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