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.
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: 1.5.0
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-18 00:00:00.000000000 Z
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: 5.0.0
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: 5.0.0
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
- For Rails 5.0, 5.1, or 5.2. Typed and cast like Active Record. Supporting nested models,
134
- dirty tracking, some querying (with postgres jsonb contains), and working smoothy with form builders.
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.4.0
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: '0'
207
+ version: 1.3.1
213
208
  requirements: []
214
209
  rubygems_version: 3.2.33
215
210
  signing_key:
@@ -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
- ```
@@ -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: "../"
@@ -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: "../"
@@ -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