activeentity 6.1.1 → 6.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a48deda3be4192f640dfdf3feebc0152f65cd41eb7483e9c0d082a1565071227
4
- data.tar.gz: 79b7877eee5cf2c3f9ae976533f15a0651b2cc0257174b2f7ea8a93034123212
3
+ metadata.gz: e3b04bbfb0b0a237a4aeae8c4009182ec920bd76eec58aa377493c872fcf2e13
4
+ data.tar.gz: 48395a80ea845a7b5fd7892fc9176df4beebebe0b0c82fe801a3dbfd200e11c4
5
5
  SHA512:
6
- metadata.gz: 718bced85e2be2eb42ad37daca8ba3a9d1a1c63f3e91f6fedcea297c57abce9c596ab2bad681ecaad2d13b001edb7d695872021ebab5a57a954a9b63a409ebb5
7
- data.tar.gz: 37eafc74002e3a8b5e4af2c017f070ba782c6b347dd40d92dcf25f867cf9be79b72fd4dba6e378766422741325e9e9df9c09dad43a35deb5e9ab1e496cdfb1f3
6
+ metadata.gz: da361c64ae3c545bf011bdf2c14f4ca975644547739a28a5c492146c08c61c39179b7a34eedf82b75b39af6d079d2ff7724eedd2743cf2830b0f152cf15cfb63
7
+ data.tar.gz: fd4a9c90772398ea9ba37d86c030a77908e2f17eaeb7afebf1a4d9e5713f1aa74f527a41c78d1dbf1dce070ee7daeeaa4705ed45b9cb79fee3a10f2a9e8c3d7c
data/README.md CHANGED
@@ -93,11 +93,11 @@ class Reviewer < ActiveEntity::Base
93
93
  end
94
94
 
95
95
  class Book < ActiveEntity::Base
96
- embeds_many :categories
96
+ embeds_many :categories, index_errors: true
97
97
  validates :categories, uniqueness_in_embeds: {key: :name}
98
98
 
99
99
  embeds_many :reviewers
100
- validates :categories, uniqueness_in_embeds: {key: [:first_name, :last_name]}
100
+ validates :reviewers, uniqueness_in_embeds: {key: [:first_name, :last_name]}
101
101
  end
102
102
  ```
103
103
 
@@ -123,7 +123,6 @@ end
123
123
  These Active Record feature also available in Active Entity
124
124
 
125
125
  - [`composed_of`](https://api.rubyonrails.org/classes/ActiveRecord/Aggregations/ClassMethods.html)
126
- - [`enum`](https://api.rubyonrails.org/v5.2.2/classes/ActiveRecord/Enum.html) (You must declare the attribute before using `enum`)
127
126
  - [`serializable_hash`](https://api.rubyonrails.org/classes/ActiveModel/Serialization.html#method-i-serializable_hash)
128
127
  - [`serialize`](https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html#method-i-serialize)
129
128
  - [`store`](https://api.rubyonrails.org/classes/ActiveRecord/Store.html)
@@ -132,6 +131,69 @@ These Active Record feature also available in Active Entity
132
131
 
133
132
  Same to [Active Record I18n](https://guides.rubyonrails.org/i18n.html#translations-for-active-record-models), the only different is the root of locale YAML is `active_entity` instead of `activerecord`
134
133
 
134
+ #### Enum
135
+
136
+ You can use the `enum` class method to define a set of possible values for an attribute. It is similar to the `enum` functionality in Active Model, but has significant enough quirks that you should think of them as distinct.
137
+
138
+ ```rb
139
+ class Example < ActiveEntity::Base
140
+ attribute :steve, :integer
141
+ enum steve: [:martin, :carell, :buscemi]
142
+ end
143
+
144
+ example = Example.new
145
+ example.attributes # => {"steve"=>nil}
146
+ example.steve = :carell
147
+ example.carell? # => true
148
+ example.attributes # => {"steve"=>"carell"}
149
+ example.steve = 2
150
+ example.attributes # => {"steve"=>"buscemi"}
151
+
152
+ # IMPORTANT: the next line will only work if you implement an update! method
153
+ example.martin! # => {"steve"=>"martin"}
154
+
155
+ example.steve = :bannon # ArgumentError ('bannon' is not a valid steve)
156
+ ```
157
+
158
+ The first thing you'll notice about the `:steve` attribute is that it is an "Integer", even though it might seem logical to define it as a String... TL;DR: don't do this. Internally enum tracks the possible values based on their index position in the array.
159
+
160
+ It's also possible to provide a Hash of possible values:
161
+
162
+ ```rb
163
+ class Example < ActiveEntity::Base
164
+ attribute :steve, :integer, default: 9
165
+ enum steve: {martin: 5, carell: 12, buscemi: 9}
166
+ end
167
+
168
+ example = Example.new
169
+ example.attributes # => {"steve"=>"buscemi"}
170
+ ```
171
+
172
+ The other quirk of this implementation is that you must create your attribute before you call enum.
173
+ enum does not create the search scopes that might be familar to Active Model users, since there is no search or where concept in Active Entity. You can, however, access the mapping directly to obtain the index number for a given value:
174
+
175
+ ```rb
176
+ Example.steves[:buscemi] # => 9
177
+ ```
178
+
179
+ You can define prefixes and suffixes for your `enum` attributes. Note the underscores:
180
+
181
+ ```rb
182
+ class Conversation < ActiveEntity::Base
183
+ attribute :status, :integer
184
+ attribute :comments_status, :integer
185
+ enum status: [ :active, :archived ], _suffix: true
186
+ enum comments_status: [ :active, :inactive ], _prefix: :comments
187
+ end
188
+
189
+ conversation = Conversation.new
190
+ conversation.active_status! # only if you have an update! method
191
+ conversation.archived_status? # => false
192
+
193
+ conversation.comments_inactive! # only if you have an update! method
194
+ conversation.comments_active? # => false
195
+ ```
196
+
135
197
  #### Read-only attributes
136
198
 
137
199
  You can use `attr_readonly :title, :author` to prevent assign value to attribute after initialized.
@@ -6,8 +6,175 @@ module ActiveEntity
6
6
  module AttributeMethods
7
7
  module Dirty
8
8
  extend ActiveSupport::Concern
9
+ include ActiveEntity::AMAttributeMethods
9
10
 
10
- include ActiveModel::Dirty
11
+ included do
12
+ attribute_method_suffix "_changed?", "_change", "_will_change!", "_was"
13
+ attribute_method_suffix "_previously_changed?", "_previous_change", "_previously_was"
14
+ attribute_method_affix prefix: "restore_", suffix: "!"
15
+ attribute_method_affix prefix: "clear_", suffix: "_change"
16
+ end
17
+
18
+ def initialize_dup(other) # :nodoc:
19
+ super
20
+ if self.class.respond_to?(:_default_attributes)
21
+ @attributes = self.class._default_attributes.map do |attr|
22
+ attr.with_value_from_user(@attributes.fetch_value(attr.name))
23
+ end
24
+ end
25
+ @mutations_from_database = nil
26
+ end
27
+
28
+ def as_json(options = {}) # :nodoc:
29
+ options[:except] = [*options[:except], "mutations_from_database", "mutations_before_last_save"]
30
+ super(options)
31
+ end
32
+
33
+ # Clears dirty data and moves +changes+ to +previous_changes+ and
34
+ # +mutations_from_database+ to +mutations_before_last_save+ respectively.
35
+ def changes_applied
36
+ unless defined?(@attributes)
37
+ mutations_from_database.finalize_changes
38
+ end
39
+ @mutations_before_last_save = mutations_from_database
40
+ forget_attribute_assignments
41
+ @mutations_from_database = nil
42
+ end
43
+
44
+ # Returns +true+ if any of the attributes has unsaved changes, +false+ otherwise.
45
+ #
46
+ # person.changed? # => false
47
+ # person.name = 'bob'
48
+ # person.changed? # => true
49
+ def changed?
50
+ mutations_from_database.any_changes?
51
+ end
52
+
53
+ # Returns an array with the name of the attributes with unsaved changes.
54
+ #
55
+ # person.changed # => []
56
+ # person.name = 'bob'
57
+ # person.changed # => ["name"]
58
+ def changed
59
+ mutations_from_database.changed_attribute_names
60
+ end
61
+
62
+ # Dispatch target for <tt>*_changed?</tt> attribute methods.
63
+ def attribute_changed?(attr_name, **options) # :nodoc:
64
+ mutations_from_database.changed?(attr_name.to_s, **options)
65
+ end
66
+
67
+ # Dispatch target for <tt>*_was</tt> attribute methods.
68
+ def attribute_was(attr_name) # :nodoc:
69
+ mutations_from_database.original_value(attr_name.to_s)
70
+ end
71
+
72
+ # Dispatch target for <tt>*_previously_changed?</tt> attribute methods.
73
+ def attribute_previously_changed?(attr_name, **options) # :nodoc:
74
+ mutations_before_last_save.changed?(attr_name.to_s, **options)
75
+ end
76
+
77
+ # Dispatch target for <tt>*_previously_was</tt> attribute methods.
78
+ def attribute_previously_was(attr_name) # :nodoc:
79
+ mutations_before_last_save.original_value(attr_name.to_s)
80
+ end
81
+
82
+ # Restore all previous data of the provided attributes.
83
+ def restore_attributes(attr_names = changed)
84
+ attr_names.each { |attr_name| restore_attribute!(attr_name) }
85
+ end
86
+
87
+ # Clears all dirty data: current changes and previous changes.
88
+ def clear_changes_information
89
+ @mutations_before_last_save = nil
90
+ forget_attribute_assignments
91
+ @mutations_from_database = nil
92
+ end
93
+
94
+ def clear_attribute_changes(attr_names)
95
+ attr_names.each do |attr_name|
96
+ clear_attribute_change(attr_name)
97
+ end
98
+ end
99
+
100
+ # Returns a hash of the attributes with unsaved changes indicating their original
101
+ # values like <tt>attr => original value</tt>.
102
+ #
103
+ # person.name # => "bob"
104
+ # person.name = 'robert'
105
+ # person.changed_attributes # => {"name" => "bob"}
106
+ def changed_attributes
107
+ mutations_from_database.changed_values
108
+ end
109
+
110
+ # Returns a hash of changed attributes indicating their original
111
+ # and new values like <tt>attr => [original value, new value]</tt>.
112
+ #
113
+ # person.changes # => {}
114
+ # person.name = 'bob'
115
+ # person.changes # => { "name" => ["bill", "bob"] }
116
+ def changes
117
+ mutations_from_database.changes
118
+ end
119
+
120
+ # Returns a hash of attributes that were changed before the model was saved.
121
+ #
122
+ # person.name # => "bob"
123
+ # person.name = 'robert'
124
+ # person.save
125
+ # person.previous_changes # => {"name" => ["bob", "robert"]}
126
+ def previous_changes
127
+ mutations_before_last_save.changes
128
+ end
129
+
130
+ def attribute_changed_in_place?(attr_name) # :nodoc:
131
+ mutations_from_database.changed_in_place?(attr_name.to_s)
132
+ end
133
+
134
+ private
135
+ def clear_attribute_change(attr_name)
136
+ mutations_from_database.forget_change(attr_name.to_s)
137
+ end
138
+
139
+ def mutations_from_database
140
+ @mutations_from_database ||= if defined?(@attributes)
141
+ ActiveModel::AttributeMutationTracker.new(@attributes)
142
+ else
143
+ ActiveModel::ForcedMutationTracker.new(self)
144
+ end
145
+ end
146
+
147
+ def forget_attribute_assignments
148
+ @attributes = @attributes.map(&:forgetting_assignment) if defined?(@attributes)
149
+ end
150
+
151
+ def mutations_before_last_save
152
+ @mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance
153
+ end
154
+
155
+ # Dispatch target for <tt>*_change</tt> attribute methods.
156
+ def attribute_change(attr_name)
157
+ mutations_from_database.change_to_attribute(attr_name.to_s)
158
+ end
159
+
160
+ # Dispatch target for <tt>*_previous_change</tt> attribute methods.
161
+ def attribute_previous_change(attr_name)
162
+ mutations_before_last_save.change_to_attribute(attr_name.to_s)
163
+ end
164
+
165
+ # Dispatch target for <tt>*_will_change!</tt> attribute methods.
166
+ def attribute_will_change!(attr_name)
167
+ mutations_from_database.force_change(attr_name.to_s)
168
+ end
169
+
170
+ # Dispatch target for <tt>restore_*!</tt> attribute methods.
171
+ def restore_attribute!(attr_name)
172
+ attr_name = attr_name.to_s
173
+ if attribute_changed?(attr_name)
174
+ __send__("#{attr_name}=", attribute_was(attr_name))
175
+ clear_attribute_change(attr_name)
176
+ end
177
+ end
11
178
  end
12
179
  end
13
180
  end
@@ -9,7 +9,7 @@ module ActiveEntity
9
9
  private
10
10
 
11
11
  def define_method_attribute(name, owner:)
12
- ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
12
+ ActiveEntity::AMAttributeMethods::AttrNames.define_attribute_accessor_method(
13
13
  owner, name
14
14
  ) do |temp_method_name, attr_name_expr|
15
15
  owner <<
@@ -13,7 +13,7 @@ module ActiveEntity
13
13
  private
14
14
 
15
15
  def define_method_attribute=(name, owner:)
16
- ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
16
+ ActiveEntity::AMAttributeMethods::AttrNames.define_attribute_accessor_method(
17
17
  owner, name, writer: true,
18
18
  ) do |temp_method_name, attr_name_expr|
19
19
  owner <<
@@ -1,13 +1,505 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "concurrent/map"
3
4
  require "mutex_m"
4
5
  require "active_support/core_ext/enumerable"
5
6
 
6
7
  module ActiveEntity
8
+ module AMAttributeMethods
9
+ extend ActiveSupport::Concern
10
+
11
+ NAME_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?=]?\z/
12
+ CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/
13
+
14
+ included do
15
+ class_attribute :attribute_aliases, instance_writer: false, default: {}
16
+ class_attribute :attribute_method_matchers, instance_writer: false, default: [ ClassMethods::AttributeMethodMatcher.new ]
17
+ end
18
+
19
+ module ClassMethods
20
+ # Declares a method available for all attributes with the given prefix.
21
+ # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
22
+ #
23
+ # #{prefix}#{attr}(*args, &block)
24
+ #
25
+ # to
26
+ #
27
+ # #{prefix}attribute(#{attr}, *args, &block)
28
+ #
29
+ # An instance method <tt>#{prefix}attribute</tt> must exist and accept
30
+ # at least the +attr+ argument.
31
+ #
32
+ # class Person
33
+ # include ActiveModel::AttributeMethods
34
+ #
35
+ # attr_accessor :name
36
+ # attribute_method_prefix 'clear_'
37
+ # define_attribute_methods :name
38
+ #
39
+ # private
40
+ #
41
+ # def clear_attribute(attr)
42
+ # send("#{attr}=", nil)
43
+ # end
44
+ # end
45
+ #
46
+ # person = Person.new
47
+ # person.name = 'Bob'
48
+ # person.name # => "Bob"
49
+ # person.clear_name
50
+ # person.name # => nil
51
+ def attribute_method_prefix(*prefixes)
52
+ self.attribute_method_matchers += prefixes.map! { |prefix| AttributeMethodMatcher.new prefix: prefix }
53
+ undefine_attribute_methods
54
+ end
55
+
56
+ # Declares a method available for all attributes with the given suffix.
57
+ # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
58
+ #
59
+ # #{attr}#{suffix}(*args, &block)
60
+ #
61
+ # to
62
+ #
63
+ # attribute#{suffix}(#{attr}, *args, &block)
64
+ #
65
+ # An <tt>attribute#{suffix}</tt> instance method must exist and accept at
66
+ # least the +attr+ argument.
67
+ #
68
+ # class Person
69
+ # include ActiveModel::AttributeMethods
70
+ #
71
+ # attr_accessor :name
72
+ # attribute_method_suffix '_short?'
73
+ # define_attribute_methods :name
74
+ #
75
+ # private
76
+ #
77
+ # def attribute_short?(attr)
78
+ # send(attr).length < 5
79
+ # end
80
+ # end
81
+ #
82
+ # person = Person.new
83
+ # person.name = 'Bob'
84
+ # person.name # => "Bob"
85
+ # person.name_short? # => true
86
+ def attribute_method_suffix(*suffixes)
87
+ self.attribute_method_matchers += suffixes.map! { |suffix| AttributeMethodMatcher.new suffix: suffix }
88
+ undefine_attribute_methods
89
+ end
90
+
91
+ # Declares a method available for all attributes with the given prefix
92
+ # and suffix. Uses +method_missing+ and <tt>respond_to?</tt> to rewrite
93
+ # the method.
94
+ #
95
+ # #{prefix}#{attr}#{suffix}(*args, &block)
96
+ #
97
+ # to
98
+ #
99
+ # #{prefix}attribute#{suffix}(#{attr}, *args, &block)
100
+ #
101
+ # An <tt>#{prefix}attribute#{suffix}</tt> instance method must exist and
102
+ # accept at least the +attr+ argument.
103
+ #
104
+ # class Person
105
+ # include ActiveModel::AttributeMethods
106
+ #
107
+ # attr_accessor :name
108
+ # attribute_method_affix prefix: 'reset_', suffix: '_to_default!'
109
+ # define_attribute_methods :name
110
+ #
111
+ # private
112
+ #
113
+ # def reset_attribute_to_default!(attr)
114
+ # send("#{attr}=", 'Default Name')
115
+ # end
116
+ # end
117
+ #
118
+ # person = Person.new
119
+ # person.name # => 'Gem'
120
+ # person.reset_name_to_default!
121
+ # person.name # => 'Default Name'
122
+ def attribute_method_affix(*affixes)
123
+ self.attribute_method_matchers += affixes.map! { |affix| AttributeMethodMatcher.new prefix: affix[:prefix], suffix: affix[:suffix] }
124
+ undefine_attribute_methods
125
+ end
126
+
127
+ # Allows you to make aliases for attributes.
128
+ #
129
+ # class Person
130
+ # include ActiveModel::AttributeMethods
131
+ #
132
+ # attr_accessor :name
133
+ # attribute_method_suffix '_short?'
134
+ # define_attribute_methods :name
135
+ #
136
+ # alias_attribute :nickname, :name
137
+ #
138
+ # private
139
+ #
140
+ # def attribute_short?(attr)
141
+ # send(attr).length < 5
142
+ # end
143
+ # end
144
+ #
145
+ # person = Person.new
146
+ # person.name = 'Bob'
147
+ # person.name # => "Bob"
148
+ # person.nickname # => "Bob"
149
+ # person.name_short? # => true
150
+ # person.nickname_short? # => true
151
+ def alias_attribute(new_name, old_name)
152
+ self.attribute_aliases = attribute_aliases.merge(new_name.to_s => old_name.to_s)
153
+ CodeGenerator.batch(self, __FILE__, __LINE__) do |owner|
154
+ attribute_method_matchers.each do |matcher|
155
+ matcher_new = matcher.method_name(new_name).to_s
156
+ matcher_old = matcher.method_name(old_name).to_s
157
+ define_proxy_call false, owner, matcher_new, matcher_old
158
+ end
159
+ end
160
+ end
161
+
162
+ # Is +new_name+ an alias?
163
+ def attribute_alias?(new_name)
164
+ attribute_aliases.key? new_name.to_s
165
+ end
166
+
167
+ # Returns the original name for the alias +name+
168
+ def attribute_alias(name)
169
+ attribute_aliases[name.to_s]
170
+ end
171
+
172
+ # Declares the attributes that should be prefixed and suffixed by
173
+ # <tt>ActiveModel::AttributeMethods</tt>.
174
+ #
175
+ # To use, pass attribute names (as strings or symbols). Be sure to declare
176
+ # +define_attribute_methods+ after you define any prefix, suffix or affix
177
+ # methods, or they will not hook in.
178
+ #
179
+ # class Person
180
+ # include ActiveModel::AttributeMethods
181
+ #
182
+ # attr_accessor :name, :age, :address
183
+ # attribute_method_prefix 'clear_'
184
+ #
185
+ # # Call to define_attribute_methods must appear after the
186
+ # # attribute_method_prefix, attribute_method_suffix or
187
+ # # attribute_method_affix declarations.
188
+ # define_attribute_methods :name, :age, :address
189
+ #
190
+ # private
191
+ #
192
+ # def clear_attribute(attr)
193
+ # send("#{attr}=", nil)
194
+ # end
195
+ # end
196
+ def define_attribute_methods(*attr_names)
197
+ CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
198
+ attr_names.flatten.each { |attr_name| define_attribute_method(attr_name, _owner: owner) }
199
+ end
200
+ end
201
+
202
+ # Declares an attribute that should be prefixed and suffixed by
203
+ # <tt>ActiveModel::AttributeMethods</tt>.
204
+ #
205
+ # To use, pass an attribute name (as string or symbol). Be sure to declare
206
+ # +define_attribute_method+ after you define any prefix, suffix or affix
207
+ # method, or they will not hook in.
208
+ #
209
+ # class Person
210
+ # include ActiveModel::AttributeMethods
211
+ #
212
+ # attr_accessor :name
213
+ # attribute_method_suffix '_short?'
214
+ #
215
+ # # Call to define_attribute_method must appear after the
216
+ # # attribute_method_prefix, attribute_method_suffix or
217
+ # # attribute_method_affix declarations.
218
+ # define_attribute_method :name
219
+ #
220
+ # private
221
+ #
222
+ # def attribute_short?(attr)
223
+ # send(attr).length < 5
224
+ # end
225
+ # end
226
+ #
227
+ # person = Person.new
228
+ # person.name = 'Bob'
229
+ # person.name # => "Bob"
230
+ # person.name_short? # => true
231
+ def define_attribute_method(attr_name, _owner: generated_attribute_methods)
232
+ CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
233
+ attribute_method_matchers.each do |matcher|
234
+ method_name = matcher.method_name(attr_name)
235
+
236
+ unless instance_method_already_implemented?(method_name)
237
+ generate_method = "define_method_#{matcher.target}"
238
+
239
+ if respond_to?(generate_method, true)
240
+ send(generate_method, attr_name.to_s, owner: owner)
241
+ else
242
+ define_proxy_call true, owner, method_name, matcher.target, attr_name.to_s
243
+ end
244
+ end
245
+ end
246
+ attribute_method_matchers_cache.clear
247
+ end
248
+ end
249
+
250
+ # Removes all the previously dynamically defined methods from the class.
251
+ #
252
+ # class Person
253
+ # include ActiveModel::AttributeMethods
254
+ #
255
+ # attr_accessor :name
256
+ # attribute_method_suffix '_short?'
257
+ # define_attribute_method :name
258
+ #
259
+ # private
260
+ #
261
+ # def attribute_short?(attr)
262
+ # send(attr).length < 5
263
+ # end
264
+ # end
265
+ #
266
+ # person = Person.new
267
+ # person.name = 'Bob'
268
+ # person.name_short? # => true
269
+ #
270
+ # Person.undefine_attribute_methods
271
+ #
272
+ # person.name_short? # => NoMethodError
273
+ def undefine_attribute_methods
274
+ generated_attribute_methods.module_eval do
275
+ undef_method(*instance_methods)
276
+ end
277
+ attribute_method_matchers_cache.clear
278
+ end
279
+
280
+ private
281
+ class CodeGenerator
282
+ class << self
283
+ def batch(owner, path, line)
284
+ if owner.is_a?(CodeGenerator)
285
+ yield owner
286
+ else
287
+ instance = new(owner, path, line)
288
+ result = yield instance
289
+ instance.execute
290
+ result
291
+ end
292
+ end
293
+ end
294
+
295
+ def initialize(owner, path, line)
296
+ @owner = owner
297
+ @path = path
298
+ @line = line
299
+ @sources = ["# frozen_string_literal: true\n"]
300
+ @renames = {}
301
+ end
302
+
303
+ def <<(source_line)
304
+ @sources << source_line
305
+ end
306
+
307
+ def rename_method(old_name, new_name)
308
+ @renames[old_name] = new_name
309
+ end
310
+
311
+ def execute
312
+ @owner.module_eval(@sources.join(";"), @path, @line - 1)
313
+ @renames.each do |old_name, new_name|
314
+ @owner.alias_method new_name, old_name
315
+ @owner.undef_method old_name
316
+ end
317
+ end
318
+ end
319
+ private_constant :CodeGenerator
320
+
321
+ def generated_attribute_methods
322
+ @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
323
+ end
324
+
325
+ def instance_method_already_implemented?(method_name)
326
+ generated_attribute_methods.method_defined?(method_name)
327
+ end
328
+
329
+ # The methods +method_missing+ and +respond_to?+ of this module are
330
+ # invoked often in a typical rails, both of which invoke the method
331
+ # +matched_attribute_method+. The latter method iterates through an
332
+ # array doing regular expression matches, which results in a lot of
333
+ # object creations. Most of the time it returns a +nil+ match. As the
334
+ # match result is always the same given a +method_name+, this cache is
335
+ # used to alleviate the GC, which ultimately also speeds up the app
336
+ # significantly (in our case our test suite finishes 10% faster with
337
+ # this cache).
338
+ def attribute_method_matchers_cache
339
+ @attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4)
340
+ end
341
+
342
+ def attribute_method_matchers_matching(method_name)
343
+ attribute_method_matchers_cache.compute_if_absent(method_name) do
344
+ attribute_method_matchers.map { |matcher| matcher.match(method_name) }.compact
345
+ end
346
+ end
347
+
348
+ # Define a method `name` in `mod` that dispatches to `send`
349
+ # using the given `extra` args. This falls back on `define_method`
350
+ # and `send` if the given names cannot be compiled.
351
+ def define_proxy_call(include_private, code_generator, name, target, *extra)
352
+ defn = if NAME_COMPILABLE_REGEXP.match?(name)
353
+ "def #{name}(*args)"
354
+ else
355
+ "define_method(:'#{name}') do |*args|"
356
+ end
357
+
358
+ extra = (extra.map!(&:inspect) << "*args").join(", ")
359
+
360
+ body = if CALL_COMPILABLE_REGEXP.match?(target)
361
+ "#{"self." unless include_private}#{target}(#{extra})"
362
+ else
363
+ "send(:'#{target}', #{extra})"
364
+ end
365
+
366
+ code_generator <<
367
+ defn <<
368
+ body <<
369
+ "end" <<
370
+ "ruby2_keywords(:'#{name}') if respond_to?(:ruby2_keywords, true)"
371
+ end
372
+
373
+ class AttributeMethodMatcher #:nodoc:
374
+ attr_reader :prefix, :suffix, :target
375
+
376
+ AttributeMethodMatch = Struct.new(:target, :attr_name)
377
+
378
+ def initialize(options = {})
379
+ @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
380
+ @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
381
+ @target = "#{@prefix}attribute#{@suffix}"
382
+ @method_name = "#{prefix}%s#{suffix}"
383
+ end
384
+
385
+ def match(method_name)
386
+ if @regex =~ method_name
387
+ AttributeMethodMatch.new(target, $1)
388
+ end
389
+ end
390
+
391
+ def method_name(attr_name)
392
+ @method_name % attr_name
393
+ end
394
+ end
395
+ end
396
+
397
+ # Allows access to the object attributes, which are held in the hash
398
+ # returned by <tt>attributes</tt>, as though they were first-class
399
+ # methods. So a +Person+ class with a +name+ attribute can for example use
400
+ # <tt>Person#name</tt> and <tt>Person#name=</tt> and never directly use
401
+ # the attributes hash -- except for multiple assignments with
402
+ # <tt>ActiveRecord::Base#attributes=</tt>.
403
+ #
404
+ # It's also possible to instantiate related objects, so a <tt>Client</tt>
405
+ # class belonging to the +clients+ table with a +master_id+ foreign key
406
+ # can instantiate master through <tt>Client#master</tt>.
407
+ def method_missing(method, *args, &block)
408
+ if respond_to_without_attributes?(method, true)
409
+ super
410
+ else
411
+ match = matched_attribute_method(method.to_s)
412
+ match ? attribute_missing(match, *args, &block) : super
413
+ end
414
+ end
415
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
416
+
417
+ # +attribute_missing+ is like +method_missing+, but for attributes. When
418
+ # +method_missing+ is called we check to see if there is a matching
419
+ # attribute method. If so, we tell +attribute_missing+ to dispatch the
420
+ # attribute. This method can be overloaded to customize the behavior.
421
+ def attribute_missing(match, *args, &block)
422
+ __send__(match.target, match.attr_name, *args, &block)
423
+ end
424
+
425
+ # A +Person+ instance with a +name+ attribute can ask
426
+ # <tt>person.respond_to?(:name)</tt>, <tt>person.respond_to?(:name=)</tt>,
427
+ # and <tt>person.respond_to?(:name?)</tt> which will all return +true+.
428
+ alias :respond_to_without_attributes? :respond_to?
429
+ def respond_to?(method, include_private_methods = false)
430
+ if super
431
+ true
432
+ elsif !include_private_methods && super(method, true)
433
+ # If we're here then we haven't found among non-private methods
434
+ # but found among all methods. Which means that the given method is private.
435
+ false
436
+ else
437
+ !matched_attribute_method(method.to_s).nil?
438
+ end
439
+ end
440
+
441
+ private
442
+ def attribute_method?(attr_name)
443
+ respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
444
+ end
445
+
446
+ # Returns a struct representing the matching attribute method.
447
+ # The struct's attributes are prefix, base and suffix.
448
+ def matched_attribute_method(method_name)
449
+ matches = self.class.send(:attribute_method_matchers_matching, method_name)
450
+ matches.detect { |match| attribute_method?(match.attr_name) }
451
+ end
452
+
453
+ def missing_attribute(attr_name, stack)
454
+ raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack
455
+ end
456
+
457
+ def _read_attribute(attr)
458
+ __send__(attr)
459
+ end
460
+
461
+ module AttrNames # :nodoc:
462
+ DEF_SAFE_NAME = /\A[a-zA-Z_]\w*\z/
463
+
464
+ # We want to generate the methods via module_eval rather than
465
+ # define_method, because define_method is slower on dispatch.
466
+ # Evaluating many similar methods may use more memory as the instruction
467
+ # sequences are duplicated and cached (in MRI). define_method may
468
+ # be slower on dispatch, but if you're careful about the closure
469
+ # created, then define_method will consume much less memory.
470
+ #
471
+ # But sometimes the database might return columns with
472
+ # characters that are not allowed in normal method names (like
473
+ # 'my_column(omg)'. So to work around this we first define with
474
+ # the __temp__ identifier, and then use alias method to rename
475
+ # it to what we want.
476
+ #
477
+ # We are also defining a constant to hold the frozen string of
478
+ # the attribute name. Using a constant means that we do not have
479
+ # to allocate an object on each call to the attribute method.
480
+ # Making it frozen means that it doesn't get duped when used to
481
+ # key the @attributes in read_attribute.
482
+ def self.define_attribute_accessor_method(owner, attr_name, writer: false)
483
+ method_name = "#{attr_name}#{'=' if writer}"
484
+ if attr_name.ascii_only? && DEF_SAFE_NAME.match?(attr_name)
485
+ yield method_name, "'#{attr_name}'"
486
+ else
487
+ safe_name = attr_name.unpack1("h*")
488
+ const_name = "ATTR_#{safe_name}"
489
+ const_set(const_name, attr_name) unless const_defined?(const_name)
490
+ temp_method_name = "__temp__#{safe_name}#{'=' if writer}"
491
+ attr_name_expr = "::ActiveEntity::AMAttributeMethods::AttrNames::#{const_name}"
492
+ yield temp_method_name, attr_name_expr
493
+ owner.rename_method(temp_method_name, method_name)
494
+ end
495
+ end
496
+ end
497
+ end
498
+
7
499
  # = Active Entity Attribute Methods
8
500
  module AttributeMethods
9
501
  extend ActiveSupport::Concern
10
- include ActiveModel::AttributeMethods
502
+ include AMAttributeMethods # Some AM modules will include AM::AttributeMethods, we want override it
11
503
 
12
504
  included do
13
505
  initialize_generated_modules
@@ -172,6 +664,18 @@ module ActiveEntity
172
664
  def _has_attribute?(attr_name) # :nodoc:
173
665
  attribute_types.key?(attr_name)
174
666
  end
667
+
668
+ # https://github.com/rails/rails/blob/df475877efdcf74d7524f734ab8ad1d4704fd187/activemodel/lib/active_model/attribute_methods.rb#L208-L217
669
+ def alias_attribute(new_name, old_name)
670
+ self.attribute_aliases = attribute_aliases.merge(new_name.to_s => old_name.to_s)
671
+ CodeGenerator.batch(self, __FILE__, __LINE__) do |owner|
672
+ attribute_method_matchers.each do |matcher|
673
+ matcher_new = matcher.method_name(new_name).to_s
674
+ matcher_old = matcher.method_name(old_name).to_s
675
+ define_proxy_call false, owner, matcher_new, matcher_old
676
+ end
677
+ end
678
+ end
175
679
  end
176
680
 
177
681
  # Returns +true+ if the given attribute is in the attributes hash, otherwise +false+.
@@ -201,6 +201,7 @@ module ActiveEntity
201
201
  )
202
202
  attribute_types[name] = cast_type
203
203
  define_default_attribute(name, default, cast_type)
204
+ define_attribute_method(name)
204
205
  end
205
206
 
206
207
  def load_schema! # :nodoc:
@@ -8,8 +8,8 @@ module ActiveEntity
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 6
11
- MINOR = 1
12
- TINY = 1
11
+ MINOR = 3
12
+ TINY = 0
13
13
  PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
@@ -151,7 +151,7 @@ module ActiveEntity
151
151
  ActiveSupport::Dependencies.constantize(type_name)
152
152
  else
153
153
  type_candidate = @_type_candidates_cache[type_name]
154
- if type_candidate && type_constant = ActiveSupport::Dependencies.safe_constantize(type_candidate)
154
+ if type_candidate && type_constant = type_candidate.safe_constantize
155
155
  return type_constant
156
156
  end
157
157
 
@@ -161,7 +161,7 @@ module ActiveEntity
161
161
  candidates << type_name
162
162
 
163
163
  candidates.each do |candidate|
164
- constant = ActiveSupport::Dependencies.safe_constantize(candidate)
164
+ constant = candidate.safe_constantize
165
165
  if candidate == constant.to_s
166
166
  @_type_candidates_cache[type_name] = candidate
167
167
  return constant
@@ -86,16 +86,31 @@ module ActiveEntity
86
86
  @load_schema_invoked = true
87
87
  end
88
88
 
89
- def reload_schema_from_cache
90
- @attribute_types = nil
91
- @default_attributes = nil
92
- @attributes_builder = nil
93
- @schema_loaded = false
94
- @load_schema_invoked = false
95
- @attribute_names = nil
96
- @yaml_encoder = nil
97
- direct_descendants.each do |descendant|
98
- descendant.send(:reload_schema_from_cache)
89
+ if ActiveSupport::VERSION::MAJOR >= 7
90
+ def reload_schema_from_cache
91
+ @attribute_types = nil
92
+ @default_attributes = nil
93
+ @attributes_builder = nil
94
+ @schema_loaded = false
95
+ @load_schema_invoked = false
96
+ @attribute_names = nil
97
+ @yaml_encoder = nil
98
+ subclasses.each do |descendant|
99
+ descendant.send(:reload_schema_from_cache)
100
+ end
101
+ end
102
+ else
103
+ def reload_schema_from_cache
104
+ @attribute_types = nil
105
+ @default_attributes = nil
106
+ @attributes_builder = nil
107
+ @schema_loaded = false
108
+ @load_schema_invoked = false
109
+ @attribute_names = nil
110
+ @yaml_encoder = nil
111
+ direct_descendants.each do |descendant|
112
+ descendant.send(:reload_schema_from_cache)
113
+ end
99
114
  end
100
115
  end
101
116
  end
@@ -1,20 +1,42 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_model/type/registry"
4
-
5
3
  module ActiveEntity
6
4
  # :stopdoc:
7
5
  module Type
8
- class Registry < ActiveModel::Type::Registry
9
- def add_modifier(options, klass, **args)
10
- registrations << DecorationRegistration.new(options, klass, **args)
6
+ class Registry # :nodoc:
7
+ def initialize
8
+ @registrations = []
11
9
  end
12
10
 
13
- private
11
+ def initialize_copy(_other)
12
+ @registrations = @registrations.dup
13
+ end
14
+
15
+ def add_modifier(options, klass, **_args)
16
+ registrations << DecorationRegistration.new(options, klass)
17
+ end
14
18
 
15
- def registration_klass
16
- Registration
19
+ def register(type_name, klass = nil, **options, &block)
20
+ unless block_given?
21
+ block = proc { |_, *args| klass.new(*args) }
22
+ block.ruby2_keywords if block.respond_to?(:ruby2_keywords)
17
23
  end
24
+ registrations << Registration.new(type_name, block, **options)
25
+ end
26
+
27
+ def lookup(symbol, *args, **kwargs)
28
+ registration = find_registration(symbol, *args, **kwargs)
29
+
30
+ if registration
31
+ registration.call(self, symbol, *args, **kwargs)
32
+ else
33
+ raise ArgumentError, "Unknown type #{symbol.inspect}"
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :registrations
18
40
 
19
41
  def find_registration(symbol, *args, **kwargs)
20
42
  registrations
@@ -23,7 +45,7 @@ module ActiveEntity
23
45
  end
24
46
  end
25
47
 
26
- class Registration
48
+ class Registration # :nodoc:
27
49
  def initialize(name, block, override: nil)
28
50
  @name = name
29
51
  @block = block
@@ -55,7 +77,7 @@ module ActiveEntity
55
77
  end
56
78
  end
57
79
 
58
- class DecorationRegistration < Registration
80
+ class DecorationRegistration < Registration # :nodoc:
59
81
  def initialize(options, klass, **)
60
82
  @options = options
61
83
  @klass = klass
@@ -86,7 +108,5 @@ module ActiveEntity
86
108
  end
87
109
  end
88
110
 
89
- class TypeConflictError < StandardError
90
- end
91
111
  # :startdoc:
92
112
  end
@@ -268,21 +268,16 @@ module ActiveEntity
268
268
  unless valid = record.valid?(context)
269
269
  indexed_attribute = !index.nil? && (reflection.options[:index_errors] || ActiveEntity::Base.index_nested_attribute_errors)
270
270
 
271
- record.errors.each do |attribute, message|
271
+ record.errors.group_by_attribute.each { |attribute, errors|
272
272
  attribute = normalize_reflection_attribute(indexed_attribute, reflection, index, attribute)
273
- errors[attribute] << message
274
- errors[attribute].uniq!
275
- end
276
-
277
- record.errors.details.each_key do |attribute|
278
- reflection_attribute =
279
- normalize_reflection_attribute(indexed_attribute, reflection, index, attribute).to_sym
280
273
 
281
- record.errors.details[attribute].each do |error|
282
- errors.details[reflection_attribute] << error
283
- errors.details[reflection_attribute].uniq!
284
- end
285
- end
274
+ errors.each { |error|
275
+ self.errors.import(
276
+ error,
277
+ attribute: attribute
278
+ )
279
+ }
280
+ }
286
281
  end
287
282
 
288
283
  valid
@@ -18,7 +18,7 @@ module ActiveEntity
18
18
  end
19
19
 
20
20
  unless subset?(record, value)
21
- record.errors.add(attribute, :non_subset, options.except(:in, :within).merge!(value: value))
21
+ record.errors.add(attribute, :non_subset, **options.except(:in, :within).merge(value: value))
22
22
  end
23
23
  end
24
24
 
@@ -48,26 +48,18 @@ module ActiveEntity
48
48
 
49
49
  duplicate_records.each do |r|
50
50
  if key.is_a? Symbol
51
- r.errors.add(key, :duplicated, options)
51
+ r.errors.add(key, :duplicated, **options)
52
52
 
53
53
  # Hack the record
54
54
  normalized_attribute = normalize_attribute(attribute, indexed_attribute, association_or_value.index(r), key)
55
- record.errors[normalized_attribute].concat r.errors.messages[key]
56
- record.errors[normalized_attribute].uniq!
57
-
58
- record.errors.details[normalized_attribute.to_sym].concat r.errors.details[key]
59
- record.errors.details[normalized_attribute.to_sym].uniq!
55
+ record.errors.import r.errors.where(key).first, attribute: normalized_attribute
60
56
  elsif key.is_a? Array
61
57
  key.each do |attr|
62
- r.errors.add(attr, :duplicated, options)
58
+ r.errors.add(attr, :duplicated, **options)
63
59
 
64
60
  # Hack the record
65
61
  normalized_attribute = normalize_attribute(attribute, indexed_attribute, association_or_value.index(r), attr)
66
- record.errors[normalized_attribute].concat r.errors.messages[attr]
67
- record.errors[normalized_attribute].uniq!
68
-
69
- record.errors.details[normalized_attribute.to_sym].concat r.errors.details[attr]
70
- record.errors.details[normalized_attribute.to_sym].uniq!
62
+ record.errors.import r.errors.where(key).first, attribute: normalized_attribute
71
63
  end
72
64
  end
73
65
  end
metadata CHANGED
@@ -1,55 +1,55 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activeentity
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.1.1
4
+ version: 6.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - jasl
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-12 00:00:00.000000000 Z
11
+ date: 2022-01-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '6.0'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '7.0'
22
+ version: '8'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
- - - "~>"
27
+ - - ">="
28
28
  - !ruby/object:Gem::Version
29
29
  version: '6.0'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '7.0'
32
+ version: '8'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: activemodel
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - "~>"
37
+ - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: '6.0'
40
40
  - - "<"
41
41
  - !ruby/object:Gem::Version
42
- version: '7.0'
42
+ version: '8'
43
43
  type: :runtime
44
44
  prerelease: false
45
45
  version_requirements: !ruby/object:Gem::Requirement
46
46
  requirements:
47
- - - "~>"
47
+ - - ">="
48
48
  - !ruby/object:Gem::Version
49
49
  version: '6.0'
50
50
  - - "<"
51
51
  - !ruby/object:Gem::Version
52
- version: '7.0'
52
+ version: '8'
53
53
  description: Rails virtual model solution based on ActiveModel design for Rails 6+.
54
54
  email:
55
55
  - jasl9187@hotmail.com
@@ -150,7 +150,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
150
150
  - !ruby/object:Gem::Version
151
151
  version: '0'
152
152
  requirements: []
153
- rubygems_version: 3.1.4
153
+ rubygems_version: 3.3.4
154
154
  signing_key:
155
155
  specification_version: 4
156
156
  summary: Rails virtual model solution based on ActiveModel.