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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 66d3aead74bfd3d5a8162e8da72f63337a598842e7aaabf5d3ddc8bcf03ccba7
4
- data.tar.gz: 4a5bde329a590399e6c890279b9d702463bec7da3cd9df0fae84eb188949b6a8
3
+ metadata.gz: e3b04bbfb0b0a237a4aeae8c4009182ec920bd76eec58aa377493c872fcf2e13
4
+ data.tar.gz: 48395a80ea845a7b5fd7892fc9176df4beebebe0b0c82fe801a3dbfd200e11c4
5
5
  SHA512:
6
- metadata.gz: 5e268f4ff1d308b223e767a5836c9eb158ff4d8648dfdd960426434da4caff1a7ff278923a79d4f1f73f97e4bbeabfdac9e69dbf437f893c59dfac91ab80b868
7
- data.tar.gz: '049773f1db811952efeec0f186c6c7306937783b38c719f466f2c3ffc93c353da1b9f8be16f445223b4582e08e731fefe2217689bccb2ea9c1a3bf597561bbc8'
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.
@@ -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
- @attributes[attr_name.to_s].value_before_type_cast
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(attribute_name)
70
- read_attribute_before_type_cast(attribute_name)
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?(attribute_name)
74
- @attributes[attribute_name].came_from_user?
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
- 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
@@ -16,29 +16,27 @@ module ActiveEntity
16
16
 
17
17
  # Returns the primary key column's value.
18
18
  def id
19
- primary_key = self.class.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 = self.class.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(self.class.primary_key)
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(self.class.primary_key)
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(self.class.primary_key)
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 id_in_database).to_set
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
- private
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
- ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
13
- generated_attribute_methods, name
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
- generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
16
- # frozen_string_literal: true
17
- def #{temp_method_name}
18
- name = #{attr_name_expr}
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
- _read_attribute(name, &block)
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.to_s, &block)
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
- ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
17
- generated_attribute_methods, name, writer: true,
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
- generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
20
- # frozen_string_literal: true
21
- def #{temp_method_name}(value)
22
- name = #{attr_name_expr}
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
- _write_attribute(name, value)
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.to_s, value)
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.to_s, value)
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