attr_json 1.5.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
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-30 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,14 +199,14 @@ 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
207
  version: '0'
213
208
  requirements: []
214
- rubygems_version: 3.2.33
209
+ rubygems_version: 3.4.5
215
210
  signing_key:
216
211
  specification_version: 4
217
212
  summary: ActiveRecord attributes stored serialized in a json column, super smooth.
@@ -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