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 +4 -4
- data/README.md +65 -3
- data/lib/active_entity/attribute_methods/dirty.rb +168 -1
- data/lib/active_entity/attribute_methods/read.rb +1 -1
- data/lib/active_entity/attribute_methods/write.rb +1 -1
- data/lib/active_entity/attribute_methods.rb +505 -1
- data/lib/active_entity/attributes.rb +1 -0
- data/lib/active_entity/gem_version.rb +2 -2
- data/lib/active_entity/inheritance.rb +2 -2
- data/lib/active_entity/model_schema.rb +25 -10
- data/lib/active_entity/type/registry.rb +32 -12
- data/lib/active_entity/validate_embeds_association.rb +8 -13
- data/lib/active_entity/validations/subset.rb +1 -1
- data/lib/active_entity/validations/uniqueness_in_embeds.rb +4 -12
- metadata +11 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e3b04bbfb0b0a237a4aeae8c4009182ec920bd76eec58aa377493c872fcf2e13
|
4
|
+
data.tar.gz: 48395a80ea845a7b5fd7892fc9176df4beebebe0b0c82fe801a3dbfd200e11c4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 :
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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+.
|
@@ -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 =
|
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 =
|
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
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
descendant
|
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
|
9
|
-
def
|
10
|
-
registrations
|
6
|
+
class Registry # :nodoc:
|
7
|
+
def initialize
|
8
|
+
@registrations = []
|
11
9
|
end
|
12
10
|
|
13
|
-
|
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
|
-
|
16
|
-
|
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
|
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
|
-
|
282
|
-
errors.
|
283
|
-
|
284
|
-
|
285
|
-
|
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
|
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
|
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
|
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.
|
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:
|
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: '
|
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: '
|
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: '
|
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: '
|
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.
|
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.
|