activeentity 0.0.1.beta17 → 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/before_type_cast.rb +5 -5
- data/lib/active_entity/attribute_methods/dirty.rb +168 -1
- data/lib/active_entity/attribute_methods/primary_key.rb +6 -8
- data/lib/active_entity/attribute_methods/query.rb +2 -6
- data/lib/active_entity/attribute_methods/read.rb +10 -12
- data/lib/active_entity/attribute_methods/write.rb +14 -20
- data/lib/active_entity/attribute_methods.rb +544 -12
- data/lib/active_entity/attributes.rb +1 -0
- data/lib/active_entity/core.rb +1 -0
- data/lib/active_entity/gem_version.rb +4 -4
- 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
- data/lib/active_entity.rb +1 -0
- metadata +16 -16
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.
|
@@ -46,7 +46,7 @@ module ActiveEntity
|
|
46
46
|
# task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
|
47
47
|
# task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21"
|
48
48
|
def read_attribute_before_type_cast(attr_name)
|
49
|
-
|
49
|
+
attribute_before_type_cast(attr_name.to_s)
|
50
50
|
end
|
51
51
|
|
52
52
|
# Returns a hash of attributes before typecasting and deserialization.
|
@@ -66,12 +66,12 @@ module ActiveEntity
|
|
66
66
|
private
|
67
67
|
|
68
68
|
# Dispatch target for <tt>*_before_type_cast</tt> attribute methods.
|
69
|
-
def attribute_before_type_cast(
|
70
|
-
|
69
|
+
def attribute_before_type_cast(attr_name)
|
70
|
+
@attributes[attr_name].value_before_type_cast
|
71
71
|
end
|
72
72
|
|
73
|
-
def attribute_came_from_user?(
|
74
|
-
@attributes[
|
73
|
+
def attribute_came_from_user?(attr_name)
|
74
|
+
@attributes[attr_name].came_from_user?
|
75
75
|
end
|
76
76
|
end
|
77
77
|
end
|
@@ -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
|
@@ -16,29 +16,27 @@ module ActiveEntity
|
|
16
16
|
|
17
17
|
# Returns the primary key column's value.
|
18
18
|
def id
|
19
|
-
primary_key
|
20
|
-
_read_attribute(primary_key) if primary_key
|
19
|
+
_read_attribute(@primary_key)
|
21
20
|
end
|
22
21
|
|
23
22
|
# Sets the primary key column's value.
|
24
23
|
def id=(value)
|
25
|
-
primary_key
|
26
|
-
_write_attribute(primary_key, value) if primary_key
|
24
|
+
_write_attribute(@primary_key, value)
|
27
25
|
end
|
28
26
|
|
29
27
|
# Queries the primary key column's value.
|
30
28
|
def id?
|
31
|
-
query_attribute(
|
29
|
+
query_attribute(@primary_key)
|
32
30
|
end
|
33
31
|
|
34
32
|
# Returns the primary key column's value before type cast.
|
35
33
|
def id_before_type_cast
|
36
|
-
read_attribute_before_type_cast(
|
34
|
+
read_attribute_before_type_cast(@primary_key)
|
37
35
|
end
|
38
36
|
|
39
37
|
# Returns the primary key column's previous value.
|
40
38
|
def id_was
|
41
|
-
attribute_was(
|
39
|
+
attribute_was(@primary_key)
|
42
40
|
end
|
43
41
|
|
44
42
|
private
|
@@ -48,7 +46,7 @@ module ActiveEntity
|
|
48
46
|
end
|
49
47
|
|
50
48
|
module ClassMethods
|
51
|
-
ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was
|
49
|
+
ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was).to_set
|
52
50
|
|
53
51
|
def instance_method_already_implemented?(method_name)
|
54
52
|
super || primary_key && ID_ATTRIBUTE_METHODS.include?(method_name)
|
@@ -31,12 +31,8 @@ module ActiveEntity
|
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
|
-
|
35
|
-
|
36
|
-
# Dispatch target for <tt>*?</tt> attribute methods.
|
37
|
-
def attribute?(attribute_name)
|
38
|
-
query_attribute(attribute_name)
|
39
|
-
end
|
34
|
+
alias :attribute? :query_attribute
|
35
|
+
private :attribute?
|
40
36
|
end
|
41
37
|
end
|
42
38
|
end
|
@@ -8,17 +8,14 @@ module ActiveEntity
|
|
8
8
|
module ClassMethods # :nodoc:
|
9
9
|
private
|
10
10
|
|
11
|
-
def define_method_attribute(name)
|
12
|
-
|
13
|
-
|
11
|
+
def define_method_attribute(name, owner:)
|
12
|
+
ActiveEntity::AMAttributeMethods::AttrNames.define_attribute_accessor_method(
|
13
|
+
owner, name
|
14
14
|
) do |temp_method_name, attr_name_expr|
|
15
|
-
|
16
|
-
#
|
17
|
-
|
18
|
-
|
19
|
-
_read_attribute(name) { |n| missing_attribute(n, caller) }
|
20
|
-
end
|
21
|
-
RUBY
|
15
|
+
owner <<
|
16
|
+
"def #{temp_method_name}" <<
|
17
|
+
" _read_attribute(#{attr_name_expr}) { |n| missing_attribute(n, caller) }" <<
|
18
|
+
"end"
|
22
19
|
end
|
23
20
|
end
|
24
21
|
end
|
@@ -30,13 +27,14 @@ module ActiveEntity
|
|
30
27
|
name = attr_name.to_s
|
31
28
|
name = self.class.attribute_aliases[name] || name
|
32
29
|
|
33
|
-
|
30
|
+
name = @primary_key if name == "id" && @primary_key
|
31
|
+
@attributes.fetch_value(name, &block)
|
34
32
|
end
|
35
33
|
|
36
34
|
# This method exists to avoid the expensive primary_key check internally, without
|
37
35
|
# breaking compatibility with the read_attribute API
|
38
36
|
def _read_attribute(attr_name, &block) # :nodoc
|
39
|
-
@attributes.fetch_value(attr_name
|
37
|
+
@attributes.fetch_value(attr_name, &block)
|
40
38
|
end
|
41
39
|
|
42
40
|
alias :attribute :_read_attribute
|
@@ -12,17 +12,14 @@ module ActiveEntity
|
|
12
12
|
module ClassMethods # :nodoc:
|
13
13
|
private
|
14
14
|
|
15
|
-
def define_method_attribute=(name)
|
16
|
-
|
17
|
-
|
15
|
+
def define_method_attribute=(name, owner:)
|
16
|
+
ActiveEntity::AMAttributeMethods::AttrNames.define_attribute_accessor_method(
|
17
|
+
owner, name, writer: true,
|
18
18
|
) do |temp_method_name, attr_name_expr|
|
19
|
-
|
20
|
-
#
|
21
|
-
|
22
|
-
|
23
|
-
_write_attribute(name, value)
|
24
|
-
end
|
25
|
-
RUBY
|
19
|
+
owner <<
|
20
|
+
"def #{temp_method_name}(value)" <<
|
21
|
+
" _write_attribute(#{attr_name_expr}, value)" <<
|
22
|
+
"end"
|
26
23
|
end
|
27
24
|
end
|
28
25
|
end
|
@@ -34,26 +31,23 @@ module ActiveEntity
|
|
34
31
|
name = attr_name.to_s
|
35
32
|
name = self.class.attribute_aliases[name] || name
|
36
33
|
|
37
|
-
|
34
|
+
name = @primary_key if name == "id" && @primary_key
|
35
|
+
@attributes.write_from_user(name, value)
|
38
36
|
end
|
39
37
|
|
40
38
|
# This method exists to avoid the expensive primary_key check internally, without
|
41
39
|
# breaking compatibility with the write_attribute API
|
42
40
|
def _write_attribute(attr_name, value) # :nodoc:
|
43
|
-
@attributes.write_from_user(attr_name
|
44
|
-
value
|
41
|
+
@attributes.write_from_user(attr_name, value)
|
45
42
|
end
|
46
43
|
|
44
|
+
alias :attribute= :_write_attribute
|
45
|
+
private :attribute=
|
46
|
+
|
47
47
|
private
|
48
48
|
|
49
49
|
def write_attribute_without_type_cast(attr_name, value)
|
50
|
-
@attributes.write_cast_value(attr_name
|
51
|
-
value
|
52
|
-
end
|
53
|
-
|
54
|
-
# Dispatch target for <tt>*=</tt> attribute methods.
|
55
|
-
def attribute=(attribute_name, value)
|
56
|
-
_write_attribute(attribute_name, value)
|
50
|
+
@attributes.write_cast_value(attr_name, value)
|
57
51
|
end
|
58
52
|
end
|
59
53
|
end
|