all_futures 1.0.3 → 2.0.0

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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +4 -1
  3. data/Gemfile.lock +272 -61
  4. data/LICENSE.txt +1 -1
  5. data/README.md +59 -79
  6. data/Rakefile +13 -1
  7. data/SUMMARY.md +22 -2
  8. data/bin/console +8 -0
  9. data/bin/loc +0 -0
  10. data/bin/setup +0 -0
  11. data/bin/standardize +0 -0
  12. data/lib/all_futures/association.rb +42 -0
  13. data/lib/all_futures/association_index.rb +69 -0
  14. data/lib/all_futures/attributes.rb +28 -0
  15. data/lib/all_futures/base.rb +66 -0
  16. data/lib/all_futures/callbacks.rb +71 -0
  17. data/lib/all_futures/configuration.rb +9 -0
  18. data/lib/all_futures/dirty.rb +116 -0
  19. data/lib/all_futures/embeds.rb +40 -0
  20. data/lib/all_futures/errors.rb +37 -0
  21. data/lib/all_futures/finder.rb +214 -0
  22. data/lib/all_futures/locale/en.yml +49 -0
  23. data/lib/all_futures/persistence.rb +595 -0
  24. data/lib/all_futures/presenter.rb +35 -0
  25. data/lib/all_futures/railtie.rb +26 -0
  26. data/lib/all_futures/translation.rb +22 -0
  27. data/lib/all_futures/validations.rb +10 -0
  28. data/lib/all_futures/version.rb +3 -1
  29. data/lib/all_futures/versions.rb +80 -0
  30. data/lib/all_futures.rb +24 -1
  31. data/lib/vendor/ACTIVE_ENTITY_MIT-LICENSE +42 -0
  32. data/lib/vendor/README.md +8 -0
  33. data/lib/vendor/active_entity/aggregations.rb +277 -0
  34. data/lib/vendor/active_entity/associations/embeds/association.rb +134 -0
  35. data/lib/vendor/active_entity/associations/embeds/builder/association.rb +105 -0
  36. data/lib/vendor/active_entity/associations/embeds/builder/collection_association.rb +71 -0
  37. data/lib/vendor/active_entity/associations/embeds/builder/embedded_in.rb +38 -0
  38. data/lib/vendor/active_entity/associations/embeds/builder/embeds_many.rb +13 -0
  39. data/lib/vendor/active_entity/associations/embeds/builder/embeds_one.rb +16 -0
  40. data/lib/vendor/active_entity/associations/embeds/builder/singular_association.rb +28 -0
  41. data/lib/vendor/active_entity/associations/embeds/collection_association.rb +209 -0
  42. data/lib/vendor/active_entity/associations/embeds/collection_proxy.rb +310 -0
  43. data/lib/vendor/active_entity/associations/embeds/embedded_in_association.rb +31 -0
  44. data/lib/vendor/active_entity/associations/embeds/embeds_many_association.rb +15 -0
  45. data/lib/vendor/active_entity/associations/embeds/embeds_one_association.rb +26 -0
  46. data/lib/vendor/active_entity/associations/embeds/singular_association.rb +35 -0
  47. data/lib/vendor/active_entity/associations.rb +168 -0
  48. data/lib/vendor/active_entity/attribute_assignment.rb +87 -0
  49. data/lib/vendor/active_entity/attribute_decorators.rb +90 -0
  50. data/lib/vendor/active_entity/attribute_methods/before_type_cast.rb +78 -0
  51. data/lib/vendor/active_entity/attribute_methods/dirty.rb +180 -0
  52. data/lib/vendor/active_entity/attribute_methods/primary_key.rb +96 -0
  53. data/lib/vendor/active_entity/attribute_methods/query.rb +38 -0
  54. data/lib/vendor/active_entity/attribute_methods/read.rb +44 -0
  55. data/lib/vendor/active_entity/attribute_methods/serialization.rb +90 -0
  56. data/lib/vendor/active_entity/attribute_methods/time_zone_conversion.rb +93 -0
  57. data/lib/vendor/active_entity/attribute_methods/write.rb +54 -0
  58. data/lib/vendor/active_entity/attribute_methods.rb +833 -0
  59. data/lib/vendor/active_entity/attributes.rb +240 -0
  60. data/lib/vendor/active_entity/base.rb +293 -0
  61. data/lib/vendor/active_entity/callbacks.rb +21 -0
  62. data/lib/vendor/active_entity/coders/json.rb +15 -0
  63. data/lib/vendor/active_entity/coders/yaml_column.rb +50 -0
  64. data/lib/vendor/active_entity/core.rb +348 -0
  65. data/lib/vendor/active_entity/enum.rb +260 -0
  66. data/lib/vendor/active_entity/errors.rb +69 -0
  67. data/lib/vendor/active_entity/gem_version.rb +17 -0
  68. data/lib/vendor/active_entity/inheritance.rb +176 -0
  69. data/lib/vendor/active_entity/integration.rb +78 -0
  70. data/lib/vendor/active_entity/locale/en.yml +45 -0
  71. data/lib/vendor/active_entity/model_schema.rb +118 -0
  72. data/lib/vendor/active_entity/nested_attributes.rb +554 -0
  73. data/lib/vendor/active_entity/persistence.rb +20 -0
  74. data/lib/vendor/active_entity/railtie.rb +76 -0
  75. data/lib/vendor/active_entity/readonly_attributes.rb +55 -0
  76. data/lib/vendor/active_entity/reflection.rb +447 -0
  77. data/lib/vendor/active_entity/serialization.rb +28 -0
  78. data/lib/vendor/active_entity/store.rb +291 -0
  79. data/lib/vendor/active_entity/translation.rb +24 -0
  80. data/lib/vendor/active_entity/type/date.rb +9 -0
  81. data/lib/vendor/active_entity/type/date_time.rb +9 -0
  82. data/lib/vendor/active_entity/type/internal/timezone.rb +17 -0
  83. data/lib/vendor/active_entity/type/json.rb +30 -0
  84. data/lib/vendor/active_entity/type/modifiers/array.rb +72 -0
  85. data/lib/vendor/active_entity/type/modifiers/array_without_blank.rb +19 -0
  86. data/lib/vendor/active_entity/type/registry.rb +112 -0
  87. data/lib/vendor/active_entity/type/serialized.rb +71 -0
  88. data/lib/vendor/active_entity/type/text.rb +11 -0
  89. data/lib/vendor/active_entity/type/time.rb +21 -0
  90. data/lib/vendor/active_entity/type/unsigned_integer.rb +17 -0
  91. data/lib/vendor/active_entity/type.rb +75 -0
  92. data/lib/vendor/active_entity/validate_embeds_association.rb +300 -0
  93. data/lib/vendor/active_entity/validations/absence.rb +25 -0
  94. data/lib/vendor/active_entity/validations/associated.rb +60 -0
  95. data/lib/vendor/active_entity/validations/length.rb +26 -0
  96. data/lib/vendor/active_entity/validations/presence.rb +68 -0
  97. data/lib/vendor/active_entity/validations/subset.rb +76 -0
  98. data/lib/vendor/active_entity/validations/uniqueness_in_embeds.rb +91 -0
  99. data/lib/vendor/active_entity/validations/uniqueness_on_active_record.rb +234 -0
  100. data/lib/vendor/active_entity/validations.rb +47 -0
  101. data/lib/vendor/active_entity/version.rb +10 -0
  102. data/lib/vendor/active_entity.rb +99 -0
  103. data/lib/vendor/core_ext/array_without_blank.rb +48 -0
  104. metadata +175 -21
  105. data/lib/all_futures/all_futures.rb +0 -28
  106. data/lib/all_futures/config.rb +0 -14
@@ -0,0 +1,833 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/map"
4
+ require "active_support/core_ext/enumerable"
5
+
6
+ module ActiveEntity
7
+ module AMAttributeMethods
8
+ extend ActiveSupport::Concern
9
+
10
+ NAME_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?=]?\z/
11
+ CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/
12
+
13
+ included do
14
+ class_attribute :attribute_aliases, instance_writer: false, default: {}
15
+ class_attribute :attribute_method_matchers, instance_writer: false, default: [ ClassMethods::AttributeMethodMatcher.new ]
16
+ end
17
+
18
+ module ClassMethods
19
+ # Declares a method available for all attributes with the given prefix.
20
+ # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
21
+ #
22
+ # #{prefix}#{attr}(*args, &block)
23
+ #
24
+ # to
25
+ #
26
+ # #{prefix}attribute(#{attr}, *args, &block)
27
+ #
28
+ # An instance method <tt>#{prefix}attribute</tt> must exist and accept
29
+ # at least the +attr+ argument.
30
+ #
31
+ # class Person
32
+ # include ActiveModel::AttributeMethods
33
+ #
34
+ # attr_accessor :name
35
+ # attribute_method_prefix 'clear_'
36
+ # define_attribute_methods :name
37
+ #
38
+ # private
39
+ #
40
+ # def clear_attribute(attr)
41
+ # send("#{attr}=", nil)
42
+ # end
43
+ # end
44
+ #
45
+ # person = Person.new
46
+ # person.name = 'Bob'
47
+ # person.name # => "Bob"
48
+ # person.clear_name
49
+ # person.name # => nil
50
+ def attribute_method_prefix(*prefixes)
51
+ self.attribute_method_matchers += prefixes.map! { |prefix| AttributeMethodMatcher.new prefix: prefix }
52
+ undefine_attribute_methods
53
+ end
54
+
55
+ # Declares a method available for all attributes with the given suffix.
56
+ # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
57
+ #
58
+ # #{attr}#{suffix}(*args, &block)
59
+ #
60
+ # to
61
+ #
62
+ # attribute#{suffix}(#{attr}, *args, &block)
63
+ #
64
+ # An <tt>attribute#{suffix}</tt> instance method must exist and accept at
65
+ # least the +attr+ argument.
66
+ #
67
+ # class Person
68
+ # include ActiveModel::AttributeMethods
69
+ #
70
+ # attr_accessor :name
71
+ # attribute_method_suffix '_short?'
72
+ # define_attribute_methods :name
73
+ #
74
+ # private
75
+ #
76
+ # def attribute_short?(attr)
77
+ # send(attr).length < 5
78
+ # end
79
+ # end
80
+ #
81
+ # person = Person.new
82
+ # person.name = 'Bob'
83
+ # person.name # => "Bob"
84
+ # person.name_short? # => true
85
+ def attribute_method_suffix(*suffixes)
86
+ self.attribute_method_matchers += suffixes.map! { |suffix| AttributeMethodMatcher.new suffix: suffix }
87
+ undefine_attribute_methods
88
+ end
89
+
90
+ # Declares a method available for all attributes with the given prefix
91
+ # and suffix. Uses +method_missing+ and <tt>respond_to?</tt> to rewrite
92
+ # the method.
93
+ #
94
+ # #{prefix}#{attr}#{suffix}(*args, &block)
95
+ #
96
+ # to
97
+ #
98
+ # #{prefix}attribute#{suffix}(#{attr}, *args, &block)
99
+ #
100
+ # An <tt>#{prefix}attribute#{suffix}</tt> instance method must exist and
101
+ # accept at least the +attr+ argument.
102
+ #
103
+ # class Person
104
+ # include ActiveModel::AttributeMethods
105
+ #
106
+ # attr_accessor :name
107
+ # attribute_method_affix prefix: 'reset_', suffix: '_to_default!'
108
+ # define_attribute_methods :name
109
+ #
110
+ # private
111
+ #
112
+ # def reset_attribute_to_default!(attr)
113
+ # send("#{attr}=", 'Default Name')
114
+ # end
115
+ # end
116
+ #
117
+ # person = Person.new
118
+ # person.name # => 'Gem'
119
+ # person.reset_name_to_default!
120
+ # person.name # => 'Default Name'
121
+ def attribute_method_affix(*affixes)
122
+ self.attribute_method_matchers += affixes.map! { |affix| AttributeMethodMatcher.new prefix: affix[:prefix], suffix: affix[:suffix] }
123
+ undefine_attribute_methods
124
+ end
125
+
126
+ # Allows you to make aliases for attributes.
127
+ #
128
+ # class Person
129
+ # include ActiveModel::AttributeMethods
130
+ #
131
+ # attr_accessor :name
132
+ # attribute_method_suffix '_short?'
133
+ # define_attribute_methods :name
134
+ #
135
+ # alias_attribute :nickname, :name
136
+ #
137
+ # private
138
+ #
139
+ # def attribute_short?(attr)
140
+ # send(attr).length < 5
141
+ # end
142
+ # end
143
+ #
144
+ # person = Person.new
145
+ # person.name = 'Bob'
146
+ # person.name # => "Bob"
147
+ # person.nickname # => "Bob"
148
+ # person.name_short? # => true
149
+ # person.nickname_short? # => true
150
+ def alias_attribute(new_name, old_name)
151
+ self.attribute_aliases = attribute_aliases.merge(new_name.to_s => old_name.to_s)
152
+ CodeGenerator.batch(self, __FILE__, __LINE__) do |owner|
153
+ attribute_method_matchers.each do |matcher|
154
+ matcher_new = matcher.method_name(new_name).to_s
155
+ matcher_old = matcher.method_name(old_name).to_s
156
+ define_proxy_call false, owner, matcher_new, matcher_old
157
+ end
158
+ end
159
+ end
160
+
161
+ # Is +new_name+ an alias?
162
+ def attribute_alias?(new_name)
163
+ attribute_aliases.key? new_name.to_s
164
+ end
165
+
166
+ # Returns the original name for the alias +name+
167
+ def attribute_alias(name)
168
+ attribute_aliases[name.to_s]
169
+ end
170
+
171
+ # Declares the attributes that should be prefixed and suffixed by
172
+ # <tt>ActiveModel::AttributeMethods</tt>.
173
+ #
174
+ # To use, pass attribute names (as strings or symbols). Be sure to declare
175
+ # +define_attribute_methods+ after you define any prefix, suffix or affix
176
+ # methods, or they will not hook in.
177
+ #
178
+ # class Person
179
+ # include ActiveModel::AttributeMethods
180
+ #
181
+ # attr_accessor :name, :age, :address
182
+ # attribute_method_prefix 'clear_'
183
+ #
184
+ # # Call to define_attribute_methods must appear after the
185
+ # # attribute_method_prefix, attribute_method_suffix or
186
+ # # attribute_method_affix declarations.
187
+ # define_attribute_methods :name, :age, :address
188
+ #
189
+ # private
190
+ #
191
+ # def clear_attribute(attr)
192
+ # send("#{attr}=", nil)
193
+ # end
194
+ # end
195
+ def define_attribute_methods(*attr_names)
196
+ CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
197
+ attr_names.flatten.each { |attr_name| define_attribute_method(attr_name, _owner: owner) }
198
+ end
199
+ end
200
+
201
+ # Declares an attribute that should be prefixed and suffixed by
202
+ # <tt>ActiveModel::AttributeMethods</tt>.
203
+ #
204
+ # To use, pass an attribute name (as string or symbol). Be sure to declare
205
+ # +define_attribute_method+ after you define any prefix, suffix or affix
206
+ # method, or they will not hook in.
207
+ #
208
+ # class Person
209
+ # include ActiveModel::AttributeMethods
210
+ #
211
+ # attr_accessor :name
212
+ # attribute_method_suffix '_short?'
213
+ #
214
+ # # Call to define_attribute_method must appear after the
215
+ # # attribute_method_prefix, attribute_method_suffix or
216
+ # # attribute_method_affix declarations.
217
+ # define_attribute_method :name
218
+ #
219
+ # private
220
+ #
221
+ # def attribute_short?(attr)
222
+ # send(attr).length < 5
223
+ # end
224
+ # end
225
+ #
226
+ # person = Person.new
227
+ # person.name = 'Bob'
228
+ # person.name # => "Bob"
229
+ # person.name_short? # => true
230
+ def define_attribute_method(attr_name, _owner: generated_attribute_methods)
231
+ CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
232
+ attribute_method_matchers.each do |matcher|
233
+ method_name = matcher.method_name(attr_name)
234
+
235
+ unless instance_method_already_implemented?(method_name)
236
+ generate_method = "define_method_#{matcher.target}"
237
+
238
+ if respond_to?(generate_method, true)
239
+ send(generate_method, attr_name.to_s, owner: owner)
240
+ else
241
+ define_proxy_call true, owner, method_name, matcher.target, attr_name.to_s
242
+ end
243
+ end
244
+ end
245
+ attribute_method_matchers_cache.clear
246
+ end
247
+ end
248
+
249
+ # Removes all the previously dynamically defined methods from the class.
250
+ #
251
+ # class Person
252
+ # include ActiveModel::AttributeMethods
253
+ #
254
+ # attr_accessor :name
255
+ # attribute_method_suffix '_short?'
256
+ # define_attribute_method :name
257
+ #
258
+ # private
259
+ #
260
+ # def attribute_short?(attr)
261
+ # send(attr).length < 5
262
+ # end
263
+ # end
264
+ #
265
+ # person = Person.new
266
+ # person.name = 'Bob'
267
+ # person.name_short? # => true
268
+ #
269
+ # Person.undefine_attribute_methods
270
+ #
271
+ # person.name_short? # => NoMethodError
272
+ def undefine_attribute_methods
273
+ generated_attribute_methods.module_eval do
274
+ undef_method(*instance_methods)
275
+ end
276
+ attribute_method_matchers_cache.clear
277
+ end
278
+
279
+ private
280
+ class CodeGenerator
281
+ class << self
282
+ def batch(owner, path, line)
283
+ if owner.is_a?(CodeGenerator)
284
+ yield owner
285
+ else
286
+ instance = new(owner, path, line)
287
+ result = yield instance
288
+ instance.execute
289
+ result
290
+ end
291
+ end
292
+ end
293
+
294
+ def initialize(owner, path, line)
295
+ @owner = owner
296
+ @path = path
297
+ @line = line
298
+ @sources = ["# frozen_string_literal: true\n"]
299
+ @renames = {}
300
+ end
301
+
302
+ def <<(source_line)
303
+ @sources << source_line
304
+ end
305
+
306
+ def rename_method(old_name, new_name)
307
+ @renames[old_name] = new_name
308
+ end
309
+
310
+ def execute
311
+ @owner.module_eval(@sources.join(";"), @path, @line - 1)
312
+ @renames.each do |old_name, new_name|
313
+ @owner.alias_method new_name, old_name
314
+ @owner.undef_method old_name
315
+ end
316
+ end
317
+ end
318
+ private_constant :CodeGenerator
319
+
320
+ def generated_attribute_methods
321
+ @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
322
+ end
323
+
324
+ def instance_method_already_implemented?(method_name)
325
+ generated_attribute_methods.method_defined?(method_name)
326
+ end
327
+
328
+ # The methods +method_missing+ and +respond_to?+ of this module are
329
+ # invoked often in a typical rails, both of which invoke the method
330
+ # +matched_attribute_method+. The latter method iterates through an
331
+ # array doing regular expression matches, which results in a lot of
332
+ # object creations. Most of the time it returns a +nil+ match. As the
333
+ # match result is always the same given a +method_name+, this cache is
334
+ # used to alleviate the GC, which ultimately also speeds up the app
335
+ # significantly (in our case our test suite finishes 10% faster with
336
+ # this cache).
337
+ def attribute_method_matchers_cache
338
+ @attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4)
339
+ end
340
+
341
+ def attribute_method_matchers_matching(method_name)
342
+ attribute_method_matchers_cache.compute_if_absent(method_name) do
343
+ attribute_method_matchers.map { |matcher| matcher.match(method_name) }.compact
344
+ end
345
+ end
346
+
347
+ # Define a method `name` in `mod` that dispatches to `send`
348
+ # using the given `extra` args. This falls back on `define_method`
349
+ # and `send` if the given names cannot be compiled.
350
+ def define_proxy_call(include_private, code_generator, name, target, *extra)
351
+ defn = if NAME_COMPILABLE_REGEXP.match?(name)
352
+ "def #{name}(*args)"
353
+ else
354
+ "define_method(:'#{name}') do |*args|"
355
+ end
356
+
357
+ extra = (extra.map!(&:inspect) << "*args").join(", ")
358
+
359
+ body = if CALL_COMPILABLE_REGEXP.match?(target)
360
+ "#{"self." unless include_private}#{target}(#{extra})"
361
+ else
362
+ "send(:'#{target}', #{extra})"
363
+ end
364
+
365
+ code_generator <<
366
+ defn <<
367
+ body <<
368
+ "end" <<
369
+ "ruby2_keywords(:'#{name}') if respond_to?(:ruby2_keywords, true)"
370
+ end
371
+
372
+ class AttributeMethodMatcher #:nodoc:
373
+ attr_reader :prefix, :suffix, :target
374
+
375
+ AttributeMethodMatch = Struct.new(:target, :attr_name)
376
+
377
+ def initialize(options = {})
378
+ @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
379
+ @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
380
+ @target = "#{@prefix}attribute#{@suffix}"
381
+ @method_name = "#{prefix}%s#{suffix}"
382
+ end
383
+
384
+ def match(method_name)
385
+ if @regex =~ method_name
386
+ AttributeMethodMatch.new(target, $1)
387
+ end
388
+ end
389
+
390
+ def method_name(attr_name)
391
+ @method_name % attr_name
392
+ end
393
+ end
394
+ end
395
+
396
+ # Allows access to the object attributes, which are held in the hash
397
+ # returned by <tt>attributes</tt>, as though they were first-class
398
+ # methods. So a +Person+ class with a +name+ attribute can for example use
399
+ # <tt>Person#name</tt> and <tt>Person#name=</tt> and never directly use
400
+ # the attributes hash -- except for multiple assignments with
401
+ # <tt>ActiveRecord::Base#attributes=</tt>.
402
+ #
403
+ # It's also possible to instantiate related objects, so a <tt>Client</tt>
404
+ # class belonging to the +clients+ table with a +master_id+ foreign key
405
+ # can instantiate master through <tt>Client#master</tt>.
406
+ def method_missing(method, *args, &block)
407
+ if respond_to_without_attributes?(method, true)
408
+ super
409
+ else
410
+ match = matched_attribute_method(method.to_s)
411
+ match ? attribute_missing(match, *args, &block) : super
412
+ end
413
+ end
414
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
415
+
416
+ # +attribute_missing+ is like +method_missing+, but for attributes. When
417
+ # +method_missing+ is called we check to see if there is a matching
418
+ # attribute method. If so, we tell +attribute_missing+ to dispatch the
419
+ # attribute. This method can be overloaded to customize the behavior.
420
+ def attribute_missing(match, *args, &block)
421
+ __send__(match.target, match.attr_name, *args, &block)
422
+ end
423
+
424
+ # A +Person+ instance with a +name+ attribute can ask
425
+ # <tt>person.respond_to?(:name)</tt>, <tt>person.respond_to?(:name=)</tt>,
426
+ # and <tt>person.respond_to?(:name?)</tt> which will all return +true+.
427
+ alias :respond_to_without_attributes? :respond_to?
428
+ def respond_to?(method, include_private_methods = false)
429
+ if super
430
+ true
431
+ elsif !include_private_methods && super(method, true)
432
+ # If we're here then we haven't found among non-private methods
433
+ # but found among all methods. Which means that the given method is private.
434
+ false
435
+ else
436
+ !matched_attribute_method(method.to_s).nil?
437
+ end
438
+ end
439
+
440
+ private
441
+ def attribute_method?(attr_name)
442
+ respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
443
+ end
444
+
445
+ # Returns a struct representing the matching attribute method.
446
+ # The struct's attributes are prefix, base and suffix.
447
+ def matched_attribute_method(method_name)
448
+ matches = self.class.send(:attribute_method_matchers_matching, method_name)
449
+ matches.detect { |match| attribute_method?(match.attr_name) }
450
+ end
451
+
452
+ def missing_attribute(attr_name, stack)
453
+ raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack
454
+ end
455
+
456
+ def _read_attribute(attr)
457
+ __send__(attr)
458
+ end
459
+
460
+ module AttrNames # :nodoc:
461
+ DEF_SAFE_NAME = /\A[a-zA-Z_]\w*\z/
462
+
463
+ # We want to generate the methods via module_eval rather than
464
+ # define_method, because define_method is slower on dispatch.
465
+ # Evaluating many similar methods may use more memory as the instruction
466
+ # sequences are duplicated and cached (in MRI). define_method may
467
+ # be slower on dispatch, but if you're careful about the closure
468
+ # created, then define_method will consume much less memory.
469
+ #
470
+ # But sometimes the database might return columns with
471
+ # characters that are not allowed in normal method names (like
472
+ # 'my_column(omg)'. So to work around this we first define with
473
+ # the __temp__ identifier, and then use alias method to rename
474
+ # it to what we want.
475
+ #
476
+ # We are also defining a constant to hold the frozen string of
477
+ # the attribute name. Using a constant means that we do not have
478
+ # to allocate an object on each call to the attribute method.
479
+ # Making it frozen means that it doesn't get duped when used to
480
+ # key the @attributes in read_attribute.
481
+ def self.define_attribute_accessor_method(owner, attr_name, writer: false)
482
+ method_name = "#{attr_name}#{'=' if writer}"
483
+ if attr_name.ascii_only? && DEF_SAFE_NAME.match?(attr_name)
484
+ yield method_name, "'#{attr_name}'"
485
+ else
486
+ safe_name = attr_name.unpack1("h*")
487
+ const_name = "ATTR_#{safe_name}"
488
+ const_set(const_name, attr_name) unless const_defined?(const_name)
489
+ temp_method_name = "__temp__#{safe_name}#{'=' if writer}"
490
+ attr_name_expr = "::ActiveEntity::AMAttributeMethods::AttrNames::#{const_name}"
491
+ yield temp_method_name, attr_name_expr
492
+ owner.rename_method(temp_method_name, method_name)
493
+ end
494
+ end
495
+ end
496
+ end
497
+
498
+ # = Active Entity Attribute Methods
499
+ module AttributeMethods
500
+ extend ActiveSupport::Concern
501
+ include AMAttributeMethods # Some AM modules will include AM::AttributeMethods, we want override it
502
+
503
+ included do
504
+ initialize_generated_modules
505
+ include Read
506
+ include Write
507
+ include BeforeTypeCast
508
+ include Query
509
+ include PrimaryKey
510
+ include TimeZoneConversion
511
+ include Dirty
512
+ include Serialization
513
+ end
514
+
515
+ RESTRICTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass)
516
+
517
+ class GeneratedAttributeMethods < Module #:nodoc:
518
+ # mutex_m was removed from Ruby 3.4's default gems; use an internal
519
+ # mutex instead, mirroring the fix in Rails 7.1 (rails/rails#49371)
520
+ def initialize
521
+ super
522
+ @mutex = Mutex.new
523
+ end
524
+
525
+ def synchronize(&block)
526
+ @mutex.synchronize(&block)
527
+ end
528
+ end
529
+
530
+ class << self
531
+ def dangerous_attribute_methods # :nodoc:
532
+ @dangerous_attribute_methods ||= (
533
+ Base.instance_methods +
534
+ Base.private_instance_methods -
535
+ Base.superclass.instance_methods -
536
+ Base.superclass.private_instance_methods
537
+ ).map { |m| -m.to_s }.to_set.freeze
538
+ end
539
+ end
540
+
541
+ module ClassMethods
542
+ def inherited(child_class) #:nodoc:
543
+ child_class.initialize_generated_modules
544
+ super
545
+ end
546
+
547
+ def initialize_generated_modules # :nodoc:
548
+ @generated_attribute_methods = const_set(:GeneratedAttributeMethods, GeneratedAttributeMethods.new)
549
+ private_constant :GeneratedAttributeMethods
550
+ @attribute_methods_generated = false
551
+ include @generated_attribute_methods
552
+
553
+ super
554
+ end
555
+
556
+ # Generates all the attribute related methods for columns in the database
557
+ # accessors, mutators and query methods.
558
+ def define_attribute_methods # :nodoc:
559
+ return false if @attribute_methods_generated
560
+ # Use a mutex; we don't want two threads simultaneously trying to define
561
+ # attribute methods.
562
+ generated_attribute_methods.synchronize do
563
+ return false if @attribute_methods_generated
564
+ superclass.define_attribute_methods unless base_class?
565
+ super(attribute_names)
566
+ @attribute_methods_generated = true
567
+ end
568
+ end
569
+
570
+ def undefine_attribute_methods # :nodoc:
571
+ generated_attribute_methods.synchronize do
572
+ super if defined?(@attribute_methods_generated) && @attribute_methods_generated
573
+ @attribute_methods_generated = false
574
+ end
575
+ end
576
+
577
+ # Raises an ActiveEntity::DangerousAttributeError exception when an
578
+ # \Active \Record method is defined in the model, otherwise +false+.
579
+ #
580
+ # class Person < ActiveEntity::Base
581
+ # def save
582
+ # 'already defined by Active Entity'
583
+ # end
584
+ # end
585
+ #
586
+ # Person.instance_method_already_implemented?(:save)
587
+ # # => ActiveEntity::DangerousAttributeError: save is defined by Active Entity. Check to make sure that you don't have an attribute or method with the same name.
588
+ #
589
+ # Person.instance_method_already_implemented?(:name)
590
+ # # => false
591
+ def instance_method_already_implemented?(method_name)
592
+ if dangerous_attribute_method?(method_name)
593
+ raise DangerousAttributeError, "#{method_name} is defined by Active Entity. Check to make sure that you don't have an attribute or method with the same name."
594
+ end
595
+
596
+ if superclass == Base
597
+ super
598
+ else
599
+ # If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass
600
+ # defines its own attribute method, then we don't want to overwrite that.
601
+ defined = method_defined_within?(method_name, superclass, Base) &&
602
+ ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods)
603
+ defined || super
604
+ end
605
+ end
606
+
607
+ # A method name is 'dangerous' if it is already (re)defined by Active Entity, but
608
+ # not by any ancestors. (So 'puts' is not dangerous but 'save' is.)
609
+ def dangerous_attribute_method?(name) # :nodoc:
610
+ ::ActiveEntity::AttributeMethods.dangerous_attribute_methods.include?(name.to_s)
611
+ end
612
+
613
+ def method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc:
614
+ if klass.method_defined?(name) || klass.private_method_defined?(name)
615
+ if superklass.method_defined?(name) || superklass.private_method_defined?(name)
616
+ klass.instance_method(name).owner != superklass.instance_method(name).owner
617
+ else
618
+ true
619
+ end
620
+ else
621
+ false
622
+ end
623
+ end
624
+
625
+ # A class method is 'dangerous' if it is already (re)defined by Active Entity, but
626
+ # not by any ancestors. (So 'puts' is not dangerous but 'new' is.)
627
+ def dangerous_class_method?(method_name)
628
+ return true if RESTRICTED_CLASS_METHODS.include?(method_name.to_s)
629
+
630
+ if Base.respond_to?(method_name, true)
631
+ if Object.respond_to?(method_name, true)
632
+ Base.method(method_name).owner != Object.method(method_name).owner
633
+ else
634
+ true
635
+ end
636
+ else
637
+ false
638
+ end
639
+ end
640
+
641
+ # Returns an array of column names as strings if it's not an abstract class.
642
+ # Otherwise it returns an empty array.
643
+ #
644
+ # class Person < ActiveEntity::Base
645
+ # end
646
+ #
647
+ # Person.attribute_names
648
+ # # => ["id", "created_at", "updated_at", "name", "age"]
649
+ def attribute_names
650
+ @attribute_names ||= if !abstract_class?
651
+ attribute_types.keys
652
+ else
653
+ []
654
+ end
655
+ end
656
+
657
+ # Returns true if the given attribute exists, otherwise false.
658
+ #
659
+ # class Person < ActiveEntity::Base
660
+ # end
661
+ #
662
+ # Person.has_attribute?('name') # => true
663
+ # Person.has_attribute?('new_name') # => true
664
+ # Person.has_attribute?(:age) # => true
665
+ # Person.has_attribute?(:nothing) # => false
666
+ def has_attribute?(attr_name)
667
+ attr_name = attr_name.to_s
668
+ attr_name = attribute_aliases[attr_name] || attr_name
669
+ attribute_types.key?(attr_name)
670
+ end
671
+
672
+ def _has_attribute?(attr_name) # :nodoc:
673
+ attribute_types.key?(attr_name)
674
+ end
675
+
676
+ # https://github.com/rails/rails/blob/df475877efdcf74d7524f734ab8ad1d4704fd187/activemodel/lib/active_model/attribute_methods.rb#L208-L217
677
+ def alias_attribute(new_name, old_name)
678
+ self.attribute_aliases = attribute_aliases.merge(new_name.to_s => old_name.to_s)
679
+ CodeGenerator.batch(self, __FILE__, __LINE__) do |owner|
680
+ attribute_method_matchers.each do |matcher|
681
+ matcher_new = matcher.method_name(new_name).to_s
682
+ matcher_old = matcher.method_name(old_name).to_s
683
+ define_proxy_call false, owner, matcher_new, matcher_old
684
+ end
685
+ end
686
+ end
687
+ end
688
+
689
+ # Returns +true+ if the given attribute is in the attributes hash, otherwise +false+.
690
+ #
691
+ # class Person < ActiveEntity::Base
692
+ # alias_attribute :new_name, :name
693
+ # end
694
+ #
695
+ # person = Person.new
696
+ # person.has_attribute?(:name) # => true
697
+ # person.has_attribute?(:new_name) # => true
698
+ # person.has_attribute?('age') # => true
699
+ # person.has_attribute?(:nothing) # => false
700
+ def has_attribute?(attr_name)
701
+ attr_name = attr_name.to_s
702
+ attr_name = self.class.attribute_aliases[attr_name] || attr_name
703
+ @attributes.key?(attr_name)
704
+ end
705
+
706
+ def _has_attribute?(attr_name) # :nodoc:
707
+ @attributes.key?(attr_name)
708
+ end
709
+
710
+ # Returns an array of names for the attributes available on this object.
711
+ #
712
+ # class Person < ActiveEntity::Base
713
+ # end
714
+ #
715
+ # person = Person.new
716
+ # person.attribute_names
717
+ # # => ["id", "created_at", "updated_at", "name", "age"]
718
+ def attribute_names
719
+ @attributes.keys
720
+ end
721
+
722
+ # Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
723
+ #
724
+ # class Person < ActiveEntity::Base
725
+ # end
726
+ #
727
+ # person = Person.create(name: 'Francesco', age: 22)
728
+ # person.attributes
729
+ # # => {"id"=>3, "created_at"=>Sun, 21 Oct 2012 04:53:04, "updated_at"=>Sun, 21 Oct 2012 04:53:04, "name"=>"Francesco", "age"=>22}
730
+ def attributes
731
+ @attributes.to_hash
732
+ end
733
+
734
+ # Returns an <tt>#inspect</tt>-like string for the value of the
735
+ # attribute +attr_name+. String attributes are truncated up to 50
736
+ # characters, Date and Time attributes are returned in the
737
+ # <tt>:db</tt> format. Other attributes return the value of
738
+ # <tt>#inspect</tt> without modification.
739
+ #
740
+ # person = Person.create!(name: 'David Heinemeier Hansson ' * 3)
741
+ #
742
+ # person.attribute_for_inspect(:name)
743
+ # # => "\"David Heinemeier Hansson David Heinemeier Hansson ...\""
744
+ #
745
+ # person.attribute_for_inspect(:created_at)
746
+ # # => "\"2012-10-22 00:15:07\""
747
+ #
748
+ # person.attribute_for_inspect(:tag_ids)
749
+ # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]"
750
+ def attribute_for_inspect(attr_name)
751
+ attr_name = attr_name.to_s
752
+ value = _read_attribute(attr_name)
753
+ format_for_inspect(value)
754
+ end
755
+
756
+ # Returns +true+ if the specified +attribute+ has been set by the user or by a
757
+ # database load and is neither +nil+ nor <tt>empty?</tt> (the latter only applies
758
+ # to objects that respond to <tt>empty?</tt>, most notably Strings). Otherwise, +false+.
759
+ # Note that it always returns +true+ with boolean attributes.
760
+ #
761
+ # class Task < ActiveEntity::Base
762
+ # end
763
+ #
764
+ # task = Task.new(title: '', is_done: false)
765
+ # task.attribute_present?(:title) # => false
766
+ # task.attribute_present?(:is_done) # => true
767
+ # task.title = 'Buy milk'
768
+ # task.is_done = true
769
+ # task.attribute_present?(:title) # => true
770
+ # task.attribute_present?(:is_done) # => true
771
+ def attribute_present?(attr_name)
772
+ attr_name = attr_name.to_s
773
+ value = _read_attribute(attr_name)
774
+ !value.nil? && !(value.respond_to?(:empty?) && value.empty?)
775
+ end
776
+
777
+ # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
778
+ # "2004-12-12" in a date column is cast to a date object, like Date.new(2004, 12, 12)). It raises
779
+ # <tt>ActiveModel::MissingAttributeError</tt> if the identified attribute is missing.
780
+ #
781
+ # Note: +:id+ is always present.
782
+ #
783
+ # class Person < ActiveEntity::Base
784
+ # belongs_to :organization
785
+ # end
786
+ #
787
+ # person = Person.new(name: 'Francesco', age: '22')
788
+ # person[:name] # => "Francesco"
789
+ # person[:age] # => 22
790
+ #
791
+ # person = Person.select('id').first
792
+ # person[:name] # => ActiveModel::MissingAttributeError: missing attribute: name
793
+ # person[:organization_id] # => ActiveModel::MissingAttributeError: missing attribute: organization_id
794
+ def [](attr_name)
795
+ read_attribute(attr_name) { |n| missing_attribute(n, caller) }
796
+ end
797
+
798
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
799
+ # (Alias for the protected #write_attribute method).
800
+ #
801
+ # class Person < ActiveEntity::Base
802
+ # end
803
+ #
804
+ # person = Person.new
805
+ # person[:age] = '22'
806
+ # person[:age] # => 22
807
+ # person[:age].class # => Integer
808
+ def []=(attr_name, value)
809
+ write_attribute(attr_name, value)
810
+ end
811
+
812
+ private
813
+
814
+ def attribute_method?(attr_name)
815
+ # We check defined? because Syck calls respond_to? before actually calling initialize.
816
+ defined?(@attributes) && @attributes.key?(attr_name)
817
+ end
818
+
819
+ def format_for_inspect(value)
820
+ if value.is_a?(String) && value.length > 50
821
+ "#{value[0, 50]}...".inspect
822
+ elsif value.is_a?(Date) || value.is_a?(Time)
823
+ %("#{value.to_s(:db)}")
824
+ else
825
+ value.inspect
826
+ end
827
+ end
828
+
829
+ def pk_attribute?(name)
830
+ name == @primary_key
831
+ end
832
+ end
833
+ end