attr_json 1.4.1 → 2.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +4 -21
- data/.github/workflows/future_rails_ci.yml +2 -2
- data/Appraisals +0 -27
- data/CHANGELOG.md +50 -1
- data/Gemfile +2 -2
- data/README.md +62 -54
- 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 +158 -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:
|
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
|