omg-activemodel 8.0.0.alpha1

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 (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