omg-activemodel 8.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +67 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +266 -0
  5. data/lib/active_model/access.rb +16 -0
  6. data/lib/active_model/api.rb +99 -0
  7. data/lib/active_model/attribute/user_provided_default.rb +55 -0
  8. data/lib/active_model/attribute.rb +277 -0
  9. data/lib/active_model/attribute_assignment.rb +78 -0
  10. data/lib/active_model/attribute_methods.rb +592 -0
  11. data/lib/active_model/attribute_mutation_tracker.rb +189 -0
  12. data/lib/active_model/attribute_registration.rb +117 -0
  13. data/lib/active_model/attribute_set/builder.rb +182 -0
  14. data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
  15. data/lib/active_model/attribute_set.rb +118 -0
  16. data/lib/active_model/attributes.rb +165 -0
  17. data/lib/active_model/callbacks.rb +155 -0
  18. data/lib/active_model/conversion.rb +121 -0
  19. data/lib/active_model/deprecator.rb +7 -0
  20. data/lib/active_model/dirty.rb +416 -0
  21. data/lib/active_model/error.rb +208 -0
  22. data/lib/active_model/errors.rb +547 -0
  23. data/lib/active_model/forbidden_attributes_protection.rb +33 -0
  24. data/lib/active_model/gem_version.rb +17 -0
  25. data/lib/active_model/lint.rb +118 -0
  26. data/lib/active_model/locale/en.yml +38 -0
  27. data/lib/active_model/model.rb +78 -0
  28. data/lib/active_model/naming.rb +359 -0
  29. data/lib/active_model/nested_error.rb +22 -0
  30. data/lib/active_model/railtie.rb +24 -0
  31. data/lib/active_model/secure_password.rb +231 -0
  32. data/lib/active_model/serialization.rb +198 -0
  33. data/lib/active_model/serializers/json.rb +154 -0
  34. data/lib/active_model/translation.rb +78 -0
  35. data/lib/active_model/type/big_integer.rb +36 -0
  36. data/lib/active_model/type/binary.rb +62 -0
  37. data/lib/active_model/type/boolean.rb +48 -0
  38. data/lib/active_model/type/date.rb +78 -0
  39. data/lib/active_model/type/date_time.rb +88 -0
  40. data/lib/active_model/type/decimal.rb +107 -0
  41. data/lib/active_model/type/float.rb +64 -0
  42. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +53 -0
  43. data/lib/active_model/type/helpers/mutable.rb +24 -0
  44. data/lib/active_model/type/helpers/numeric.rb +61 -0
  45. data/lib/active_model/type/helpers/time_value.rb +127 -0
  46. data/lib/active_model/type/helpers/timezone.rb +23 -0
  47. data/lib/active_model/type/helpers.rb +7 -0
  48. data/lib/active_model/type/immutable_string.rb +71 -0
  49. data/lib/active_model/type/integer.rb +113 -0
  50. data/lib/active_model/type/registry.rb +37 -0
  51. data/lib/active_model/type/serialize_cast_value.rb +47 -0
  52. data/lib/active_model/type/string.rb +43 -0
  53. data/lib/active_model/type/time.rb +87 -0
  54. data/lib/active_model/type/value.rb +157 -0
  55. data/lib/active_model/type.rb +55 -0
  56. data/lib/active_model/validations/absence.rb +33 -0
  57. data/lib/active_model/validations/acceptance.rb +113 -0
  58. data/lib/active_model/validations/callbacks.rb +119 -0
  59. data/lib/active_model/validations/clusivity.rb +54 -0
  60. data/lib/active_model/validations/comparability.rb +18 -0
  61. data/lib/active_model/validations/comparison.rb +90 -0
  62. data/lib/active_model/validations/confirmation.rb +80 -0
  63. data/lib/active_model/validations/exclusion.rb +49 -0
  64. data/lib/active_model/validations/format.rb +112 -0
  65. data/lib/active_model/validations/helper_methods.rb +15 -0
  66. data/lib/active_model/validations/inclusion.rb +47 -0
  67. data/lib/active_model/validations/length.rb +130 -0
  68. data/lib/active_model/validations/numericality.rb +222 -0
  69. data/lib/active_model/validations/presence.rb +39 -0
  70. data/lib/active_model/validations/resolve_value.rb +26 -0
  71. data/lib/active_model/validations/validates.rb +175 -0
  72. data/lib/active_model/validations/with.rb +154 -0
  73. data/lib/active_model/validations.rb +489 -0
  74. data/lib/active_model/validator.rb +190 -0
  75. data/lib/active_model/version.rb +10 -0
  76. data/lib/active_model.rb +84 -0
  77. metadata +139 -0
@@ -0,0 +1,592 @@
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' for Pet
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 ActiveRecord::Base - like
22
+ # class methods such as +table_name+.
23
+ #
24
+ # The requirements to implement +ActiveModel::AttributeMethods+ 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
+ # def attribute_contrived?(attr)
53
+ # true
54
+ # end
55
+ #
56
+ # def clear_attribute(attr)
57
+ # send("#{attr}=", nil)
58
+ # end
59
+ #
60
+ # def reset_attribute_to_default!(attr)
61
+ # send("#{attr}=", 'Default Name')
62
+ # end
63
+ # end
64
+ module AttributeMethods
65
+ extend ActiveSupport::Concern
66
+
67
+ NAME_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?=]?\z/
68
+ CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/
69
+
70
+ included do
71
+ class_attribute :attribute_aliases, instance_writer: false, default: {}
72
+ class_attribute :attribute_method_patterns, instance_writer: false, default: [ ClassMethods::AttributeMethodPattern.new ]
73
+ end
74
+
75
+ module ClassMethods
76
+ # Declares a method available for all attributes with the given prefix.
77
+ # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
78
+ #
79
+ # #{prefix}#{attr}(*args, &block)
80
+ #
81
+ # to
82
+ #
83
+ # #{prefix}attribute(#{attr}, *args, &block)
84
+ #
85
+ # An instance method <tt>#{prefix}attribute</tt> must exist and accept
86
+ # at least the +attr+ argument.
87
+ #
88
+ # class Person
89
+ # include ActiveModel::AttributeMethods
90
+ #
91
+ # attr_accessor :name
92
+ # attribute_method_prefix 'clear_'
93
+ # define_attribute_methods :name
94
+ #
95
+ # private
96
+ # def clear_attribute(attr)
97
+ # send("#{attr}=", nil)
98
+ # end
99
+ # end
100
+ #
101
+ # person = Person.new
102
+ # person.name = 'Bob'
103
+ # person.name # => "Bob"
104
+ # person.clear_name
105
+ # person.name # => nil
106
+ def attribute_method_prefix(*prefixes, parameters: nil)
107
+ self.attribute_method_patterns += prefixes.map! { |prefix| AttributeMethodPattern.new(prefix: prefix, parameters: parameters) }
108
+ undefine_attribute_methods
109
+ end
110
+
111
+ # Declares a method available for all attributes with the given suffix.
112
+ # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
113
+ #
114
+ # #{attr}#{suffix}(*args, &block)
115
+ #
116
+ # to
117
+ #
118
+ # attribute#{suffix}(#{attr}, *args, &block)
119
+ #
120
+ # An <tt>attribute#{suffix}</tt> instance method must exist and accept at
121
+ # least the +attr+ argument.
122
+ #
123
+ # class Person
124
+ # include ActiveModel::AttributeMethods
125
+ #
126
+ # attr_accessor :name
127
+ # attribute_method_suffix '_short?'
128
+ # define_attribute_methods :name
129
+ #
130
+ # private
131
+ # def attribute_short?(attr)
132
+ # send(attr).length < 5
133
+ # end
134
+ # end
135
+ #
136
+ # person = Person.new
137
+ # person.name = 'Bob'
138
+ # person.name # => "Bob"
139
+ # person.name_short? # => true
140
+ def attribute_method_suffix(*suffixes, parameters: nil)
141
+ self.attribute_method_patterns += suffixes.map! { |suffix| AttributeMethodPattern.new(suffix: suffix, parameters: parameters) }
142
+ undefine_attribute_methods
143
+ end
144
+
145
+ # Declares a method available for all attributes with the given prefix
146
+ # and suffix. Uses +method_missing+ and <tt>respond_to?</tt> to rewrite
147
+ # the method.
148
+ #
149
+ # #{prefix}#{attr}#{suffix}(*args, &block)
150
+ #
151
+ # to
152
+ #
153
+ # #{prefix}attribute#{suffix}(#{attr}, *args, &block)
154
+ #
155
+ # An <tt>#{prefix}attribute#{suffix}</tt> instance method must exist and
156
+ # accept at least the +attr+ argument.
157
+ #
158
+ # class Person
159
+ # include ActiveModel::AttributeMethods
160
+ #
161
+ # attr_accessor :name
162
+ # attribute_method_affix prefix: 'reset_', suffix: '_to_default!'
163
+ # define_attribute_methods :name
164
+ #
165
+ # private
166
+ # def reset_attribute_to_default!(attr)
167
+ # send("#{attr}=", 'Default Name')
168
+ # end
169
+ # end
170
+ #
171
+ # person = Person.new
172
+ # person.name # => 'Gem'
173
+ # person.reset_name_to_default!
174
+ # person.name # => 'Default Name'
175
+ def attribute_method_affix(*affixes)
176
+ self.attribute_method_patterns += affixes.map! { |affix| AttributeMethodPattern.new(**affix) }
177
+ undefine_attribute_methods
178
+ end
179
+
180
+ # Allows you to make aliases for attributes.
181
+ #
182
+ # class Person
183
+ # include ActiveModel::AttributeMethods
184
+ #
185
+ # attr_accessor :name
186
+ # attribute_method_suffix '_short?'
187
+ # define_attribute_methods :name
188
+ #
189
+ # alias_attribute :nickname, :name
190
+ #
191
+ # private
192
+ # def attribute_short?(attr)
193
+ # send(attr).length < 5
194
+ # end
195
+ # end
196
+ #
197
+ # person = Person.new
198
+ # person.name = 'Bob'
199
+ # person.name # => "Bob"
200
+ # person.nickname # => "Bob"
201
+ # person.name_short? # => true
202
+ # person.nickname_short? # => true
203
+ def alias_attribute(new_name, old_name)
204
+ old_name = old_name.to_s
205
+ new_name = new_name.to_s
206
+ self.attribute_aliases = attribute_aliases.merge(new_name => old_name)
207
+ aliases_by_attribute_name[old_name] << new_name
208
+ eagerly_generate_alias_attribute_methods(new_name, old_name)
209
+ end
210
+
211
+ def eagerly_generate_alias_attribute_methods(new_name, old_name) # :nodoc:
212
+ ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |code_generator|
213
+ generate_alias_attribute_methods(code_generator, new_name, old_name)
214
+ end
215
+ end
216
+
217
+ def generate_alias_attribute_methods(code_generator, new_name, old_name)
218
+ ActiveSupport::CodeGenerator.batch(code_generator, __FILE__, __LINE__) do |owner|
219
+ attribute_method_patterns.each do |pattern|
220
+ alias_attribute_method_definition(code_generator, pattern, new_name, old_name)
221
+ end
222
+ attribute_method_patterns_cache.clear
223
+ end
224
+ end
225
+
226
+ def alias_attribute_method_definition(code_generator, pattern, new_name, old_name) # :nodoc:
227
+ method_name = pattern.method_name(new_name).to_s
228
+ target_name = pattern.method_name(old_name).to_s
229
+ parameters = pattern.parameters
230
+
231
+ mangled_name = build_mangled_name(target_name)
232
+
233
+ call_args = []
234
+ call_args << parameters if parameters
235
+
236
+ define_call(code_generator, method_name, target_name, mangled_name, parameters, call_args, namespace: :alias_attribute, as: method_name)
237
+ end
238
+
239
+ # Is +new_name+ an alias?
240
+ def attribute_alias?(new_name)
241
+ attribute_aliases.key? new_name.to_s
242
+ end
243
+
244
+ # Returns the original name for the alias +name+
245
+ def attribute_alias(name)
246
+ attribute_aliases[name.to_s]
247
+ end
248
+
249
+ # Declares the attributes that should be prefixed and suffixed by
250
+ # +ActiveModel::AttributeMethods+.
251
+ #
252
+ # To use, pass attribute names (as strings or symbols). Be sure to declare
253
+ # +define_attribute_methods+ after you define any prefix, suffix, or affix
254
+ # methods, or they will not hook in.
255
+ #
256
+ # class Person
257
+ # include ActiveModel::AttributeMethods
258
+ #
259
+ # attr_accessor :name, :age, :address
260
+ # attribute_method_prefix 'clear_'
261
+ #
262
+ # # Call to define_attribute_methods must appear after the
263
+ # # attribute_method_prefix, attribute_method_suffix or
264
+ # # attribute_method_affix declarations.
265
+ # define_attribute_methods :name, :age, :address
266
+ #
267
+ # private
268
+ # def clear_attribute(attr)
269
+ # send("#{attr}=", nil)
270
+ # end
271
+ # end
272
+ def define_attribute_methods(*attr_names)
273
+ ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
274
+ attr_names.flatten.each do |attr_name|
275
+ define_attribute_method(attr_name, _owner: owner)
276
+ aliases_by_attribute_name[attr_name.to_s].each do |aliased_name|
277
+ generate_alias_attribute_methods owner, aliased_name, attr_name
278
+ end
279
+ end
280
+ end
281
+ end
282
+
283
+ # Declares an attribute that should be prefixed and suffixed by
284
+ # +ActiveModel::AttributeMethods+.
285
+ #
286
+ # To use, pass an attribute name (as string or symbol). Be sure to declare
287
+ # +define_attribute_method+ after you define any prefix, suffix or affix
288
+ # method, or they will not hook in.
289
+ #
290
+ # class Person
291
+ # include ActiveModel::AttributeMethods
292
+ #
293
+ # attr_accessor :name
294
+ # attribute_method_suffix '_short?'
295
+ #
296
+ # # Call to define_attribute_method must appear after the
297
+ # # attribute_method_prefix, attribute_method_suffix or
298
+ # # attribute_method_affix declarations.
299
+ # define_attribute_method :name
300
+ #
301
+ # private
302
+ # def attribute_short?(attr)
303
+ # send(attr).length < 5
304
+ # end
305
+ # end
306
+ #
307
+ # person = Person.new
308
+ # person.name = 'Bob'
309
+ # person.name # => "Bob"
310
+ # person.name_short? # => true
311
+ def define_attribute_method(attr_name, _owner: generated_attribute_methods, as: attr_name)
312
+ ActiveSupport::CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
313
+ attribute_method_patterns.each do |pattern|
314
+ define_attribute_method_pattern(pattern, attr_name, owner: owner, as: as)
315
+ end
316
+ attribute_method_patterns_cache.clear
317
+ end
318
+ end
319
+
320
+ def define_attribute_method_pattern(pattern, attr_name, owner:, as:, override: false) # :nodoc:
321
+ canonical_method_name = pattern.method_name(attr_name)
322
+ public_method_name = pattern.method_name(as)
323
+
324
+ # If defining a regular attribute method, we don't override methods that are explictly
325
+ # defined in parrent classes.
326
+ if instance_method_already_implemented?(public_method_name)
327
+ # However, for `alias_attribute`, we always define the method.
328
+ # We check for override second because `instance_method_already_implemented?`
329
+ # also check for dangerous methods.
330
+ return unless override
331
+ end
332
+
333
+ generate_method = "define_method_#{pattern.proxy_target}"
334
+
335
+ if respond_to?(generate_method, true)
336
+ send(generate_method, attr_name.to_s, owner: owner, as: as)
337
+ else
338
+ define_proxy_call(
339
+ owner,
340
+ canonical_method_name,
341
+ pattern.proxy_target,
342
+ pattern.parameters,
343
+ attr_name.to_s,
344
+ namespace: :active_model_proxy,
345
+ as: public_method_name
346
+ )
347
+ end
348
+ end
349
+
350
+ # Removes all the previously dynamically defined methods from the class, including alias attribute methods.
351
+ #
352
+ # class Person
353
+ # include ActiveModel::AttributeMethods
354
+ #
355
+ # attr_accessor :name
356
+ # attribute_method_suffix '_short?'
357
+ # define_attribute_method :name
358
+ # alias_attribute :first_name, :name
359
+ #
360
+ # private
361
+ # def attribute_short?(attr)
362
+ # send(attr).length < 5
363
+ # end
364
+ # end
365
+ #
366
+ # person = Person.new
367
+ # person.name = 'Bob'
368
+ # person.first_name # => "Bob"
369
+ # person.name_short? # => true
370
+ #
371
+ # Person.undefine_attribute_methods
372
+ #
373
+ # person.name_short? # => NoMethodError
374
+ # person.first_name # => NoMethodError
375
+ def undefine_attribute_methods
376
+ generated_attribute_methods.module_eval do
377
+ undef_method(*instance_methods)
378
+ end
379
+ attribute_method_patterns_cache.clear
380
+ end
381
+
382
+ def aliases_by_attribute_name # :nodoc:
383
+ @aliases_by_attribute_name ||= Hash.new { |h, k| h[k] = [] }
384
+ end
385
+
386
+ private
387
+ def inherited(base) # :nodoc:
388
+ super
389
+ base.class_eval do
390
+ @attribute_method_patterns_cache = nil
391
+ @aliases_by_attribute_name = nil
392
+ @generated_attribute_methods = nil
393
+ end
394
+ end
395
+
396
+ def resolve_attribute_name(name)
397
+ attribute_aliases.fetch(super, &:itself)
398
+ end
399
+
400
+ def generated_attribute_methods
401
+ @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
402
+ end
403
+
404
+ def instance_method_already_implemented?(method_name)
405
+ generated_attribute_methods.method_defined?(method_name)
406
+ end
407
+
408
+ # The methods +method_missing+ and +respond_to?+ of this module are
409
+ # invoked often in a typical rails, both of which invoke the method
410
+ # +matched_attribute_method+. The latter method iterates through an
411
+ # array doing regular expression matches, which results in a lot of
412
+ # object creations. Most of the time it returns a +nil+ match. As the
413
+ # match result is always the same given a +method_name+, this cache is
414
+ # used to alleviate the GC, which ultimately also speeds up the app
415
+ # significantly (in our case our test suite finishes 10% faster with
416
+ # this cache).
417
+ def attribute_method_patterns_cache
418
+ @attribute_method_patterns_cache ||= Concurrent::Map.new(initial_capacity: 4)
419
+ end
420
+
421
+ def attribute_method_patterns_matching(method_name)
422
+ attribute_method_patterns_cache.compute_if_absent(method_name) do
423
+ attribute_method_patterns.filter_map { |pattern| pattern.match(method_name) }
424
+ end
425
+ end
426
+
427
+ # Define a method `name` in `mod` that dispatches to `send`
428
+ # using the given `extra` args. This falls back on `send`
429
+ # if the called name cannot be compiled.
430
+ def define_proxy_call(code_generator, name, proxy_target, parameters, *call_args, namespace:, as: name)
431
+ mangled_name = build_mangled_name(name)
432
+
433
+ call_args.map!(&:inspect)
434
+ call_args << parameters if parameters
435
+
436
+ # We have to use a different namespace for every target method, because
437
+ # if someone defines an attribute that look like an attribute method we could clash, e.g.
438
+ # attribute :title_was
439
+ # attribute :title
440
+ namespace = :"#{namespace}_#{proxy_target}"
441
+
442
+ define_call(code_generator, name, proxy_target, mangled_name, parameters, call_args, namespace: namespace, as: as)
443
+ end
444
+
445
+ def build_mangled_name(name)
446
+ mangled_name = name
447
+
448
+ unless NAME_COMPILABLE_REGEXP.match?(name)
449
+ mangled_name = :"__temp__#{name.unpack1("h*")}"
450
+ end
451
+
452
+ mangled_name
453
+ end
454
+
455
+ def define_call(code_generator, name, target_name, mangled_name, parameters, call_args, namespace:, as:)
456
+ code_generator.define_cached_method(mangled_name, as: as, namespace: namespace) do |batch|
457
+ body = if CALL_COMPILABLE_REGEXP.match?(target_name)
458
+ "self.#{target_name}(#{call_args.join(", ")})"
459
+ else
460
+ call_args.unshift(":'#{target_name}'")
461
+ "send(#{call_args.join(", ")})"
462
+ end
463
+
464
+ batch <<
465
+ "def #{mangled_name}(#{parameters || ''})" <<
466
+ body <<
467
+ "end"
468
+ end
469
+ end
470
+
471
+ class AttributeMethodPattern # :nodoc:
472
+ attr_reader :prefix, :suffix, :proxy_target, :parameters
473
+
474
+ AttributeMethod = Struct.new(:proxy_target, :attr_name)
475
+
476
+ def initialize(prefix: "", suffix: "", parameters: nil)
477
+ @prefix = prefix
478
+ @suffix = suffix
479
+ @parameters = parameters.nil? ? "..." : parameters
480
+ @regex = /\A(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})\z/
481
+ @proxy_target = "#{@prefix}attribute#{@suffix}"
482
+ @method_name = "#{prefix}%s#{suffix}"
483
+ end
484
+
485
+ def match(method_name)
486
+ if @regex =~ method_name
487
+ AttributeMethod.new(proxy_target, $1)
488
+ end
489
+ end
490
+
491
+ def method_name(attr_name)
492
+ @method_name % attr_name
493
+ end
494
+ end
495
+ end
496
+
497
+ # Allows access to the object attributes, which are held in the hash
498
+ # returned by <tt>attributes</tt>, as though they were first-class
499
+ # methods. So a +Person+ class with a +name+ attribute can for example use
500
+ # <tt>Person#name</tt> and <tt>Person#name=</tt> and never directly use
501
+ # the attributes hash -- except for multiple assignments with
502
+ # <tt>ActiveRecord::Base#attributes=</tt>.
503
+ #
504
+ # It's also possible to instantiate related objects, so a <tt>Client</tt>
505
+ # class belonging to the +clients+ table with a +master_id+ foreign key
506
+ # can instantiate master through <tt>Client#master</tt>.
507
+ def method_missing(method, ...)
508
+ if respond_to_without_attributes?(method, true)
509
+ super
510
+ else
511
+ match = matched_attribute_method(method.name)
512
+ match ? attribute_missing(match, ...) : super
513
+ end
514
+ end
515
+
516
+ # +attribute_missing+ is like +method_missing+, but for attributes. When
517
+ # +method_missing+ is called we check to see if there is a matching
518
+ # attribute method. If so, we tell +attribute_missing+ to dispatch the
519
+ # attribute. This method can be overloaded to customize the behavior.
520
+ def attribute_missing(match, ...)
521
+ __send__(match.proxy_target, match.attr_name, ...)
522
+ end
523
+
524
+ # A +Person+ instance with a +name+ attribute can ask
525
+ # <tt>person.respond_to?(:name)</tt>, <tt>person.respond_to?(:name=)</tt>,
526
+ # and <tt>person.respond_to?(:name?)</tt> which will all return +true+.
527
+ alias :respond_to_without_attributes? :respond_to?
528
+ def respond_to?(method, include_private_methods = false)
529
+ if super
530
+ true
531
+ elsif !include_private_methods && super(method, true)
532
+ # If we're here then we haven't found among non-private methods
533
+ # but found among all methods. Which means that the given method is private.
534
+ false
535
+ else
536
+ !matched_attribute_method(method.to_s).nil?
537
+ end
538
+ end
539
+
540
+ private
541
+ def attribute_method?(attr_name)
542
+ respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
543
+ end
544
+
545
+ # Returns a struct representing the matching attribute method.
546
+ # The struct's attributes are prefix, base and suffix.
547
+ def matched_attribute_method(method_name)
548
+ matches = self.class.send(:attribute_method_patterns_matching, method_name)
549
+ matches.detect { |match| attribute_method?(match.attr_name) }
550
+ end
551
+
552
+ def missing_attribute(attr_name, stack)
553
+ raise ActiveModel::MissingAttributeError, "missing attribute '#{attr_name}' for #{self.class}", stack
554
+ end
555
+
556
+ def _read_attribute(attr)
557
+ __send__(attr)
558
+ end
559
+
560
+ module AttrNames # :nodoc:
561
+ DEF_SAFE_NAME = /\A[a-zA-Z_]\w*\z/
562
+
563
+ # We want to generate the methods via module_eval rather than
564
+ # define_method, because define_method is slower on dispatch.
565
+ #
566
+ # But sometimes the database might return columns with
567
+ # characters that are not allowed in normal method names (like
568
+ # 'my_column(omg)'. So to work around this we first define with
569
+ # the __temp__ identifier, and then use alias method to rename
570
+ # it to what we want.
571
+ #
572
+ # We are also defining a constant to hold the frozen string of
573
+ # the attribute name. Using a constant means that we do not have
574
+ # to allocate an object on each call to the attribute method.
575
+ # Making it frozen means that it doesn't get duped when used to
576
+ # key the @attributes in read_attribute.
577
+ def self.define_attribute_accessor_method(owner, attr_name, writer: false)
578
+ method_name = "#{attr_name}#{'=' if writer}"
579
+ if attr_name.ascii_only? && DEF_SAFE_NAME.match?(attr_name)
580
+ yield method_name, "'#{attr_name}'"
581
+ else
582
+ safe_name = attr_name.unpack1("h*")
583
+ const_name = "ATTR_#{safe_name}"
584
+ const_set(const_name, attr_name) unless const_defined?(const_name)
585
+ temp_method_name = "__temp__#{safe_name}#{'=' if writer}"
586
+ attr_name_expr = "::ActiveModel::AttributeMethods::AttrNames::#{const_name}"
587
+ yield temp_method_name, attr_name_expr
588
+ end
589
+ end
590
+ end
591
+ end
592
+ end