attr_json 1.5.0 → 2.0.0.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +0 -21
- data/Appraisals +0 -27
- data/CHANGELOG.md +40 -1
- data/README.md +60 -52
- data/attr_json.gemspec +4 -4
- data/doc_src/forms.md +3 -14
- data/lib/attr_json/attribute_definition.rb +49 -13
- data/lib/attr_json/config.rb +1 -2
- data/lib/attr_json/model.rb +127 -16
- data/lib/attr_json/nested_attributes/writer.rb +4 -6
- data/lib/attr_json/nested_attributes.rb +5 -1
- data/lib/attr_json/record.rb +95 -74
- data/lib/attr_json/type/array.rb +10 -3
- data/lib/attr_json/type/model.rb +13 -4
- data/lib/attr_json/version.rb +1 -1
- data/lib/attr_json.rb +0 -5
- data/playground_models.rb +2 -2
- metadata +9 -14
- data/doc_src/dirty_tracking.md +0 -155
- data/gemfiles/rails_5_0.gemfile +0 -20
- data/gemfiles/rails_5_1.gemfile +0 -19
- data/gemfiles/rails_5_2.gemfile +0 -19
- data/lib/attr_json/record/dirty.rb +0 -287
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: attr_json
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0.rc1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jonathan Rochkind
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-01-
|
11
|
+
date: 2023-01-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -16,7 +16,7 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 6.0.0
|
20
20
|
- - "<"
|
21
21
|
- !ruby/object:Gem::Version
|
22
22
|
version: '7.1'
|
@@ -26,7 +26,7 @@ dependencies:
|
|
26
26
|
requirements:
|
27
27
|
- - ">="
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version:
|
29
|
+
version: 6.0.0
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
32
|
version: '7.1'
|
@@ -130,8 +130,8 @@ dependencies:
|
|
130
130
|
version: '1.0'
|
131
131
|
description: |-
|
132
132
|
ActiveRecord attributes stored serialized in a json column, super smooth.
|
133
|
-
|
134
|
-
|
133
|
+
Typed and cast like Active Record. Supporting nested models, dirty tracking, some querying
|
134
|
+
(with postgres jsonb contains), and working smoothy with form builders.
|
135
135
|
|
136
136
|
Use your database as a typed object store via ActiveRecord, in the same models right next to
|
137
137
|
ordinary ActiveRecord column-backed attributes and associations. Your json-serialized attr_json
|
@@ -159,12 +159,8 @@ files:
|
|
159
159
|
- bin/rspec
|
160
160
|
- bin/setup
|
161
161
|
- config.ru
|
162
|
-
- doc_src/dirty_tracking.md
|
163
162
|
- doc_src/forms.md
|
164
163
|
- gemfiles/.bundle/config
|
165
|
-
- gemfiles/rails_5_0.gemfile
|
166
|
-
- gemfiles/rails_5_1.gemfile
|
167
|
-
- gemfiles/rails_5_2.gemfile
|
168
164
|
- gemfiles/rails_6_0.gemfile
|
169
165
|
- gemfiles/rails_6_1.gemfile
|
170
166
|
- gemfiles/rails_7_0.gemfile
|
@@ -180,7 +176,6 @@ files:
|
|
180
176
|
- lib/attr_json/nested_attributes/multiparameter_attribute_writer.rb
|
181
177
|
- lib/attr_json/nested_attributes/writer.rb
|
182
178
|
- lib/attr_json/record.rb
|
183
|
-
- lib/attr_json/record/dirty.rb
|
184
179
|
- lib/attr_json/record/query_builder.rb
|
185
180
|
- lib/attr_json/record/query_scopes.rb
|
186
181
|
- lib/attr_json/serialization_coder_from_type.rb
|
@@ -204,12 +199,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
204
199
|
requirements:
|
205
200
|
- - ">="
|
206
201
|
- !ruby/object:Gem::Version
|
207
|
-
version: 2.
|
202
|
+
version: 2.6.0
|
208
203
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
209
204
|
requirements:
|
210
|
-
- - "
|
205
|
+
- - ">"
|
211
206
|
- !ruby/object:Gem::Version
|
212
|
-
version:
|
207
|
+
version: 1.3.1
|
213
208
|
requirements: []
|
214
209
|
rubygems_version: 3.2.33
|
215
210
|
signing_key:
|
data/doc_src/dirty_tracking.md
DELETED
@@ -1,155 +0,0 @@
|
|
1
|
-
# Dirty Tracking Support in AttrJson
|
2
|
-
|
3
|
-
In ordinary ActiveRecord, there is dirty/change-tracking support for attributes,
|
4
|
-
that lets you see what changes currently exist in the model compared to what
|
5
|
-
was fetched from the db, as well as what changed on the most recent save operation.
|
6
|
-
|
7
|
-
```ruby
|
8
|
-
model = SomeModel.new
|
9
|
-
model.str_value = "some value"
|
10
|
-
model.changes_to_save
|
11
|
-
# => { 'str_value' => 'some_value'}
|
12
|
-
model.will_save_change_to_str_value?
|
13
|
-
# => true
|
14
|
-
model.save
|
15
|
-
model.saved_changes
|
16
|
-
# => { 'str_value' => 'some_value'}
|
17
|
-
model.str_value_before_last_save
|
18
|
-
# => nil
|
19
|
-
# and more
|
20
|
-
```
|
21
|
-
|
22
|
-
You may be used to an older style of AR change-tracking methods,
|
23
|
-
involving `changes` and `previous_changes`. These older-style methods were
|
24
|
-
deprecated in Rails 5.1 and removed in Rails 5.2. It's a bit confusing and not
|
25
|
-
fully documented in AR, see more at
|
26
|
-
[these](https://www.levups.com/en/blog/2017/undocumented-dirty-attributes-activerecord-changes-rails51.html)
|
27
|
-
blog [posts](https://www.ombulabs.com/blog/rails/upgrades/active-record-5-1-api-changes.html),
|
28
|
-
and the initial [AR pull request](https://github.com/rails/rails/pull/25337).
|
29
|
-
|
30
|
-
AttrJson supports all of these new-style dirty-tracking methods, only
|
31
|
-
in Rails 5.1+. (*Sorry, our dirty tracking support does not work with Rails 5.0,
|
32
|
-
or old-style dirty API in Rails 5.1. Only new-style API in Rails 5.1+*). I wasn't
|
33
|
-
able to find a good way to get changes in the default Rails dirty tracking methods,
|
34
|
-
so instead **they are available off a separate `attr_json_changes` method**,
|
35
|
-
which also allows customization of if host record changes are also included.
|
36
|
-
|
37
|
-
To include the AttrJson dirty-tracking features, include the
|
38
|
-
`AttrJson::Record::Dirty` module in your active record model already including
|
39
|
-
`AttrJson::Record`:
|
40
|
-
|
41
|
-
```ruby
|
42
|
-
class MyEmbeddedModel
|
43
|
-
include AttrJson::Model
|
44
|
-
|
45
|
-
attr_json :str, :string
|
46
|
-
end
|
47
|
-
|
48
|
-
class MyModel < ActiveRecord::Base
|
49
|
-
include AttrJson::Record
|
50
|
-
include AttrJson::Record::Dirty
|
51
|
-
|
52
|
-
attr_json :str, :string
|
53
|
-
attr_json :str_array, :string, array: true
|
54
|
-
attr_json :array_of_models, MyEmbeddedModel.to_type, array: true
|
55
|
-
end
|
56
|
-
```
|
57
|
-
|
58
|
-
Now dirty changes are available off a `attr_json_changes` method.
|
59
|
-
The full suite of (new, Rails 5.1+) ActiveRecord dirty methods are supported,
|
60
|
-
both ones that take the attribute-name as an argument, and synthetic attribute-specific
|
61
|
-
methods. All top-level `attr_json`s are supported, including those that
|
62
|
-
include arrays and/or complex/nested/compound models.
|
63
|
-
|
64
|
-
```ruby
|
65
|
-
model = MyModel.new
|
66
|
-
model.str = "some value"
|
67
|
-
model.attr_json_changes.will_save_change_to_str? #=> true
|
68
|
-
model.str_array = ["original1", "original2"]
|
69
|
-
model.array_of_models = [MyEmbeddedModel.new(str: "value")]
|
70
|
-
model.save
|
71
|
-
|
72
|
-
model.attr_json_changes.saved_changes
|
73
|
-
# => {"str"=>[nil, "some value"], "str_array"=>[nil, ["original1", "original2"]], "array_of_models"=>[nil, [#<MyEmbeddedModel:0x00007fb285d12330 @attributes={"str"=>"value"}, @validation_context=nil, @errors=#<ActiveModel::Errors:0x00007fb285d00400 @base=#<MyEmbeddedModel:0x00007fb285d12330 ...>, @messages={}, @details={}>>]]
|
74
|
-
|
75
|
-
model.str_array << "new1"
|
76
|
-
|
77
|
-
model.attr_json_changes.will_save_change_to_str_array? # => true
|
78
|
-
model.attr_json_changes.str_array_change_to_be_saved
|
79
|
-
# => [["original1", "original2"], ["original1", "original2", "new1"]]
|
80
|
-
```
|
81
|
-
|
82
|
-
## Cast representation vs Json representation
|
83
|
-
|
84
|
-
If you ask to see changes, you are going to see the changes reported as _cast_ values,
|
85
|
-
not _json_ values. For instance, you'll see your actual `AttrJson::Model`
|
86
|
-
objects instead of the hashes they serialize to, and ruby DateTime objects instead
|
87
|
-
of the ISO 8601 strings they serialize to.
|
88
|
-
|
89
|
-
If you'd like to see the the JSON-compat data structures instead, just tag
|
90
|
-
on the `as_json` modifier. For simple strings and ints and similar primitives,
|
91
|
-
it won't make a difference, for some types it will:
|
92
|
-
|
93
|
-
```ruby
|
94
|
-
model.attr_json_changes.changes_to_save
|
95
|
-
#=> {
|
96
|
-
# json_str: [nil, "some value"]
|
97
|
-
# embedded_model: [nil, #<TestModel:0x00007fee25a04bf8 @attributes={"str"=>"foo"}>]
|
98
|
-
# json_date: [nil, {{ruby Date object}}]
|
99
|
-
# }
|
100
|
-
|
101
|
-
model.attr_json_changes.as_json.changes_to_save
|
102
|
-
#=> {
|
103
|
-
# json_str: [nil, "some_value"]
|
104
|
-
# embedded_model: [nil, {'str' => 'foo'}]
|
105
|
-
# json_date: [nil, "2018-03-23"]
|
106
|
-
# }
|
107
|
-
|
108
|
-
```
|
109
|
-
|
110
|
-
All existing values are serialized every time you call this, since they are stored
|
111
|
-
in cast form internally. So there _could_ be perf implications, but generally it is looking fine.
|
112
|
-
|
113
|
-
## Merge in ordinary AR attribute dirty tracking
|
114
|
-
|
115
|
-
Now you have one place to track 'ordinary' AR attribute "dirtyness"
|
116
|
-
(`model.some_attribute_will_change?`), and another place to track attr_json
|
117
|
-
dirty-ness (`my_model.attr_json_changes.some_json_attr_will_change?`).
|
118
|
-
|
119
|
-
You may wish you could have one place that tracked both, so your calling code
|
120
|
-
doesn't need to care if a given attribute is jsonb-backed or ordinary-column, and
|
121
|
-
is resilient if an attribute switches from one to another.
|
122
|
-
|
123
|
-
While we couldn't get this on the built-in dirty attributes, you *can* optionally
|
124
|
-
tell the `attr_json_changes` to include 'ordinary' changes from model too,
|
125
|
-
all in one place, by adding on the method `merged`.
|
126
|
-
|
127
|
-
```ruby
|
128
|
-
model.attr_json_changes.merged.ordinary_attribute_will_change?
|
129
|
-
model.attr_json_changes.merged.attr_json_will_change?
|
130
|
-
model.attr_json_changes.merged.attr_json_will_change?
|
131
|
-
model.attr_json_changes.merged.changes_to_save
|
132
|
-
# => includes a hash with keys that are both ordinary AR attributes
|
133
|
-
# and attr_jsons, as applicable for changes.
|
134
|
-
```
|
135
|
-
|
136
|
-
This will ordinarily include your json container attributes (eg `json_attributes`)
|
137
|
-
too, as they will show up in ordinary AR dirty tracking since they are just AR
|
138
|
-
columns.
|
139
|
-
|
140
|
-
If you'd like to exclude these from the merged dirty tracking, pretend the json
|
141
|
-
container attributes don't exist and just focus on the individual `attr_json`s,
|
142
|
-
we got you covered:
|
143
|
-
|
144
|
-
```ruby
|
145
|
-
model.attr_json_changes.merged(containers: false).attr_jsons_will_change?
|
146
|
-
# => always returns `nil`, the 'real' `attr_jsons` attribute is dead to us.
|
147
|
-
```
|
148
|
-
|
149
|
-
## Combine both of these modifiers at once no problem
|
150
|
-
|
151
|
-
```ruby
|
152
|
-
model.attr_json_changes.as_json.merged.saved_changes
|
153
|
-
model.attr_json_changes.as_json.merged(containers: false).saved_changes
|
154
|
-
model.attr_json_changes.merged(containers: true).as_json.saved_changes
|
155
|
-
```
|
data/gemfiles/rails_5_0.gemfile
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
# This file was generated by Appraisal
|
2
|
-
|
3
|
-
source "https://rubygems.org"
|
4
|
-
|
5
|
-
gem "combustion", "~> 0.9.0"
|
6
|
-
gem "rails", "~> 5.0.0"
|
7
|
-
gem "pg", "~> 0.18"
|
8
|
-
gem "rspec-rails", "~> 4.0"
|
9
|
-
gem "simple_form", ">= 4.0"
|
10
|
-
gem "cocoon", ">= 1.2"
|
11
|
-
gem "jquery-rails"
|
12
|
-
gem "coffee-rails"
|
13
|
-
gem "sprockets-rails"
|
14
|
-
gem "capybara", "~> 3.0"
|
15
|
-
gem "webdrivers", "~> 4.0"
|
16
|
-
gem "selenium-webdriver"
|
17
|
-
gem "byebug"
|
18
|
-
gem "rails-ujs", require: false
|
19
|
-
|
20
|
-
gemspec path: "../"
|
data/gemfiles/rails_5_1.gemfile
DELETED
@@ -1,19 +0,0 @@
|
|
1
|
-
# This file was generated by Appraisal
|
2
|
-
|
3
|
-
source "https://rubygems.org"
|
4
|
-
|
5
|
-
gem "combustion", "~> 0.9.0"
|
6
|
-
gem "rails", "~> 5.1.0"
|
7
|
-
gem "pg", "~> 1.0"
|
8
|
-
gem "rspec-rails", "~> 4.0"
|
9
|
-
gem "simple_form", ">= 4.0"
|
10
|
-
gem "cocoon", ">= 1.2"
|
11
|
-
gem "jquery-rails"
|
12
|
-
gem "coffee-rails"
|
13
|
-
gem "sprockets-rails"
|
14
|
-
gem "capybara", "~> 3.0"
|
15
|
-
gem "webdrivers", "~> 4.0"
|
16
|
-
gem "selenium-webdriver"
|
17
|
-
gem "byebug"
|
18
|
-
|
19
|
-
gemspec path: "../"
|
data/gemfiles/rails_5_2.gemfile
DELETED
@@ -1,19 +0,0 @@
|
|
1
|
-
# This file was generated by Appraisal
|
2
|
-
|
3
|
-
source "https://rubygems.org"
|
4
|
-
|
5
|
-
gem "combustion", "~> 0.9.0"
|
6
|
-
gem "rails", "~> 5.2.0"
|
7
|
-
gem "pg", "~> 1.0"
|
8
|
-
gem "rspec-rails", "~> 4.0"
|
9
|
-
gem "simple_form", ">= 4.0"
|
10
|
-
gem "cocoon", ">= 1.2"
|
11
|
-
gem "jquery-rails"
|
12
|
-
gem "coffee-rails"
|
13
|
-
gem "sprockets-rails"
|
14
|
-
gem "capybara", "~> 3.0"
|
15
|
-
gem "webdrivers", "~> 4.0"
|
16
|
-
gem "selenium-webdriver"
|
17
|
-
gem "byebug"
|
18
|
-
|
19
|
-
gemspec path: "../"
|
@@ -1,287 +0,0 @@
|
|
1
|
-
module AttrJson
|
2
|
-
module Record
|
3
|
-
# This only works in Rails 5.1+, and only uses the 'new style' dirty
|
4
|
-
# tracking methods, available in Rails 5.1+.
|
5
|
-
#
|
6
|
-
# Add into an ActiveRecord object with AttrJson::Record,
|
7
|
-
# to track dirty changes to attr_jsons, off the attr_json_changes
|
8
|
-
# object.
|
9
|
-
#
|
10
|
-
# some_model.attr_json_changes.saved_changes
|
11
|
-
# some_model.attr_json_changes.json_attr_before_last_save
|
12
|
-
#
|
13
|
-
# All methods ordinarily in ActiveRecord::Attributes::Dirty should be available,
|
14
|
-
# including synthetic attribute-specific ones like `will_save_change_to_attribute_name?`.
|
15
|
-
# By default, they _only_ report changes from json attributes.
|
16
|
-
# To have a merged list also including ordinary AR changes, add on `merged`:
|
17
|
-
#
|
18
|
-
# some_model.attr_json_changes.merged.saved_changes
|
19
|
-
# some_model.attr_json_changes.merged.ordinary_attr_before_last_save
|
20
|
-
#
|
21
|
-
# Complex nested models will show up in changes as the cast models. If you want
|
22
|
-
# the raw json instead, use `as_json`:
|
23
|
-
#
|
24
|
-
# some_model.attr_json_changes.as_json.saved_changes
|
25
|
-
#
|
26
|
-
# You can combine as_json and merged if you like:
|
27
|
-
#
|
28
|
-
# some_model.attr_json_changes.as_json.merged.saved_changes
|
29
|
-
#
|
30
|
-
# See more in [separate documentation guide](../../../doc_src/dirty_tracking.md)
|
31
|
-
#
|
32
|
-
# See what methods are available off of the object returned by {attr_json_changes}
|
33
|
-
# in {Dirty::Implementation} -- should be the AR dirty-tracking methods you expect.
|
34
|
-
module Dirty
|
35
|
-
def attr_json_changes
|
36
|
-
Implementation.new(self)
|
37
|
-
end
|
38
|
-
|
39
|
-
|
40
|
-
class Implementation
|
41
|
-
# The attribute_method stuff is copied from ActiveRecord::Dirty,
|
42
|
-
# to give you all the same synthetic per-attribute methods.
|
43
|
-
# We make it work with overridden #matched_attribute_method below.
|
44
|
-
include ActiveModel::AttributeMethods
|
45
|
-
|
46
|
-
# Attribute methods for "changed in last call to save?"
|
47
|
-
attribute_method_affix(prefix: "saved_change_to_", suffix: "?")
|
48
|
-
attribute_method_prefix("saved_change_to_")
|
49
|
-
attribute_method_suffix("_before_last_save")
|
50
|
-
|
51
|
-
# Attribute methods for "will change if I call save?"
|
52
|
-
attribute_method_affix(prefix: "will_save_change_to_", suffix: "?")
|
53
|
-
attribute_method_suffix("_change_to_be_saved", "_in_database")
|
54
|
-
|
55
|
-
attr_reader :model
|
56
|
-
|
57
|
-
def initialize(model, merged: false, merge_containers: false, as_json: false)
|
58
|
-
@model = model
|
59
|
-
@merged = !!merged
|
60
|
-
@merge_containers = !!merge_containers
|
61
|
-
@as_json = !!as_json
|
62
|
-
end
|
63
|
-
|
64
|
-
# return a copy with `merged` attribute true, so dirty tracking
|
65
|
-
# will include ordinary AR attributes too, and you can do things like:
|
66
|
-
#
|
67
|
-
# model.attr_json_changes.merged.saved_change_to_attribute?(ordinary_or_attr_json)
|
68
|
-
#
|
69
|
-
# By default, the json container attributes are included too. If you
|
70
|
-
# instead want our dirty tracking to pretend they don't exist:
|
71
|
-
#
|
72
|
-
# model.attr_json_changes.merged(containers: false).etc
|
73
|
-
#
|
74
|
-
def merged(containers: true)
|
75
|
-
self.class.new(model, merged: true, merge_containers: containers,
|
76
|
-
as_json: as_json?)
|
77
|
-
end
|
78
|
-
|
79
|
-
# return a copy with as_json parameter set to true, so change diffs
|
80
|
-
# will be the json structures serialized, not the cast models.
|
81
|
-
# for 'primitive' types will be the same, but for AttrJson::Models
|
82
|
-
# very different.
|
83
|
-
def as_json
|
84
|
-
self.class.new(model, as_json: true,
|
85
|
-
merged: merged?,
|
86
|
-
merge_containers: merge_containers?)
|
87
|
-
end
|
88
|
-
|
89
|
-
# should we handle ordinary AR attributes too in one merged
|
90
|
-
# change tracker?
|
91
|
-
def merged?
|
92
|
-
@merged
|
93
|
-
end
|
94
|
-
|
95
|
-
# if we're `merged?` and `merge_containers?` is **false**, we
|
96
|
-
# _omit_ our json container attributes from our dirty tracking.
|
97
|
-
# only has meaning if `merged?` is true. Defaults to true.
|
98
|
-
def merge_containers?
|
99
|
-
@merge_containers
|
100
|
-
end
|
101
|
-
|
102
|
-
def as_json?
|
103
|
-
@as_json
|
104
|
-
end
|
105
|
-
|
106
|
-
|
107
|
-
def saved_change_to_attribute(attr_name)
|
108
|
-
attribute_def = registry[attr_name.to_sym]
|
109
|
-
if ! attribute_def
|
110
|
-
if merged? && (merge_containers? || ! registry.container_attributes.include?(attr_name.to_s))
|
111
|
-
return model.saved_change_to_attribute(attr_name)
|
112
|
-
else
|
113
|
-
return nil
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
json_container = attribute_def.container_attribute
|
118
|
-
|
119
|
-
(before_container, after_container) = model.saved_change_to_attribute(json_container)
|
120
|
-
|
121
|
-
formatted_before_after(
|
122
|
-
before_container.try(:[], attribute_def.store_key),
|
123
|
-
after_container.try(:[], attribute_def.store_key),
|
124
|
-
attribute_def)
|
125
|
-
end
|
126
|
-
|
127
|
-
def attribute_before_last_save(attr_name)
|
128
|
-
saved_change = saved_change_to_attribute(attr_name)
|
129
|
-
return nil if saved_change.nil?
|
130
|
-
|
131
|
-
saved_change[0]
|
132
|
-
end
|
133
|
-
|
134
|
-
def saved_change_to_attribute?(attr_name)
|
135
|
-
return nil unless registry[attr_name.to_sym] || merged? && (merge_containers? || ! registry.container_attributes.include?(attr_name.to_s))
|
136
|
-
! saved_change_to_attribute(attr_name).nil?
|
137
|
-
end
|
138
|
-
|
139
|
-
def saved_changes
|
140
|
-
original_saved_changes = model.saved_changes
|
141
|
-
return {} if original_saved_changes.blank?
|
142
|
-
|
143
|
-
json_attr_changes = registry.definitions.collect do |definition|
|
144
|
-
if container_change = original_saved_changes[definition.container_attribute]
|
145
|
-
old_v = container_change.dig(0, definition.store_key)
|
146
|
-
new_v = container_change.dig(1, definition.store_key)
|
147
|
-
if old_v != new_v
|
148
|
-
[ definition.name.to_s, formatted_before_after(old_v, new_v, definition) ]
|
149
|
-
end
|
150
|
-
end
|
151
|
-
end.compact.to_h
|
152
|
-
|
153
|
-
prepared_changes(json_attr_changes, original_saved_changes)
|
154
|
-
end
|
155
|
-
|
156
|
-
def saved_changes?
|
157
|
-
saved_changes.present?
|
158
|
-
end
|
159
|
-
|
160
|
-
|
161
|
-
def attribute_in_database(attr_name)
|
162
|
-
to_be_saved = attribute_change_to_be_saved(attr_name)
|
163
|
-
if to_be_saved.nil?
|
164
|
-
if merged? && (merge_containers? || ! registry.container_attributes.include?(attr_name.to_s))
|
165
|
-
return model.attribute_change_to_be_saved(attr_name)
|
166
|
-
else
|
167
|
-
return nil
|
168
|
-
end
|
169
|
-
end
|
170
|
-
|
171
|
-
to_be_saved[0]
|
172
|
-
end
|
173
|
-
|
174
|
-
def attribute_change_to_be_saved(attr_name)
|
175
|
-
attribute_def = registry[attr_name.to_sym]
|
176
|
-
if ! attribute_def
|
177
|
-
if merged? && (merge_containers? || ! registry.container_attributes.include?(attr_name.to_s))
|
178
|
-
return model.attribute_change_to_be_saved(attr_name)
|
179
|
-
else
|
180
|
-
return nil
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
json_container = attribute_def.container_attribute
|
185
|
-
|
186
|
-
(before_container, after_container) = model.attribute_change_to_be_saved(json_container)
|
187
|
-
|
188
|
-
formatted_before_after(
|
189
|
-
before_container.try(:[], attribute_def.store_key),
|
190
|
-
after_container.try(:[], attribute_def.store_key),
|
191
|
-
attribute_def
|
192
|
-
)
|
193
|
-
end
|
194
|
-
|
195
|
-
def will_save_change_to_attribute?(attr_name)
|
196
|
-
return nil unless registry[attr_name.to_sym] || merged? && (merge_containers? || ! registry.container_attributes.include?(attr_name.to_s))
|
197
|
-
! attribute_change_to_be_saved(attr_name).nil?
|
198
|
-
end
|
199
|
-
|
200
|
-
def changes_to_save
|
201
|
-
original_changes_to_save = model.changes_to_save
|
202
|
-
|
203
|
-
return {} if original_changes_to_save.blank?
|
204
|
-
|
205
|
-
json_attr_changes = registry.definitions.collect do |definition|
|
206
|
-
if container_change = original_changes_to_save[definition.container_attribute]
|
207
|
-
old_v = container_change.dig(0, definition.store_key)
|
208
|
-
new_v = container_change.dig(1, definition.store_key)
|
209
|
-
if old_v != new_v
|
210
|
-
[ definition.name.to_s, formatted_before_after(old_v, new_v, definition) ]
|
211
|
-
end
|
212
|
-
end
|
213
|
-
end.compact.to_h
|
214
|
-
|
215
|
-
prepared_changes(json_attr_changes, original_changes_to_save)
|
216
|
-
end
|
217
|
-
|
218
|
-
def has_changes_to_save?
|
219
|
-
changes_to_save.present?
|
220
|
-
end
|
221
|
-
|
222
|
-
def changed_attribute_names_to_save
|
223
|
-
changes_to_save.keys
|
224
|
-
end
|
225
|
-
|
226
|
-
def attributes_in_database
|
227
|
-
changes_to_save.transform_values(&:first)
|
228
|
-
end
|
229
|
-
|
230
|
-
private
|
231
|
-
|
232
|
-
# returns an array of before and after, possibly formatted with as_json.
|
233
|
-
# if both before and after are nil, returns nil.
|
234
|
-
def formatted_before_after(before_v, after_v, attribute_def)
|
235
|
-
return nil if before_v.nil? && after_v.nil?
|
236
|
-
|
237
|
-
if as_json?
|
238
|
-
before_v = attribute_def.type.serialize(before_v) unless before_v.nil?
|
239
|
-
after_v = attribute_def.type.serialize(after_v) unless after_v.nil?
|
240
|
-
end
|
241
|
-
|
242
|
-
[
|
243
|
-
before_v,
|
244
|
-
after_v
|
245
|
-
]
|
246
|
-
|
247
|
-
end
|
248
|
-
|
249
|
-
# Takes a hash of _our_ attr_json changes, and possibly
|
250
|
-
# merges them into the hash of all changes from the parent record,
|
251
|
-
# depending on values of `merged?` and `merge_containers?`.
|
252
|
-
def prepared_changes(json_attr_changes, all_changes)
|
253
|
-
if merged?
|
254
|
-
all_changes.merge(json_attr_changes).tap do |merged|
|
255
|
-
unless merge_containers?
|
256
|
-
merged.except!(*registry.container_attributes)
|
257
|
-
end
|
258
|
-
end
|
259
|
-
else
|
260
|
-
json_attr_changes
|
261
|
-
end
|
262
|
-
end
|
263
|
-
|
264
|
-
def registry
|
265
|
-
model.class.attr_json_registry
|
266
|
-
end
|
267
|
-
|
268
|
-
# Override from ActiveModel::AttributeMethods
|
269
|
-
# to not require class-static define_attribute, but instead dynamically
|
270
|
-
# find it from currently declared attributes.
|
271
|
-
# https://github.com/rails/rails/blob/6aa5cf03ea8232180ffbbae4c130b051f813c670/activemodel/lib/active_model/attribute_methods.rb#L463-L468
|
272
|
-
def matched_attribute_method(method_name)
|
273
|
-
if self.class.respond_to?(:attribute_method_patterns_matching, true)
|
274
|
-
# Rails 7.1+
|
275
|
-
matches = self.class.send(:attribute_method_patterns_matching, method_name)
|
276
|
-
else
|
277
|
-
matches = self.class.send(:attribute_method_matchers_matching, method_name)
|
278
|
-
end
|
279
|
-
|
280
|
-
matches.detect do |match|
|
281
|
-
registry.has_attribute?(match.attr_name)
|
282
|
-
end
|
283
|
-
end
|
284
|
-
end
|
285
|
-
end
|
286
|
-
end
|
287
|
-
end
|