attr_json 0.2.0 → 0.3.0
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/README.md +12 -2
- data/attr_json.gemspec +2 -0
- data/doc_src/dirty_tracking.md +1 -1
- data/doc_src/forms.md +1 -1
- data/lib/attr_json.rb +1 -0
- data/lib/attr_json/attribute_definition/registry.rb +1 -1
- data/lib/attr_json/config.rb +45 -0
- data/lib/attr_json/model.rb +29 -6
- data/lib/attr_json/record.rb +47 -10
- data/lib/attr_json/record/dirty.rb +12 -12
- data/lib/attr_json/type/container_attribute.rb +14 -3
- data/lib/attr_json/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9a4f8e06e4c2ae8894433a2271d782e0aa023d3c
|
4
|
+
data.tar.gz: 4b4e44c1af5d89affd2f2aadde14610eda05aadd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1de9b6472284debc8aa8e60e26d7a8ba2463543b8515248470584ddf06b493d90c9fc4e4e221f244e00421a07703564e3ff3a3d7d6e35bead76adedaeeaabd3a
|
7
|
+
data.tar.gz: cb0567d3031ececf33c64895130cd78bd2295e382547fc3cb4f0ad17072602e6fa71ad24645564abc1ff3ed354a07655140cea7a57cc477e3887470c9e546be2
|
data/README.md
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
[](https://badge.fury.io/rb/attr_json)
|
4
4
|
|
5
5
|
|
6
|
-
ActiveRecord attributes stored serialized in a json column, super smooth. For Rails 5.0, 5.1, or 5.2.
|
6
|
+
ActiveRecord attributes stored serialized in a json column, super smooth. For Rails 5.0, 5.1, or 5.2. Ruby 2.4+.
|
7
7
|
|
8
8
|
Typed and cast like Active Record. Supporting [nested models](#nested), [dirty tracking](#dirty), some [querying](#querying) (with postgres [jsonb](https://www.postgresql.org/docs/9.5/static/datatype-json.html) contains), and [working smoothy with form builders](#forms).
|
9
9
|
|
@@ -85,7 +85,7 @@ class OtherModel < ActiveRecord::Base
|
|
85
85
|
include AttrJson::Record
|
86
86
|
|
87
87
|
# as a default for the model
|
88
|
-
|
88
|
+
attr_json_config(default_container_attribute: :some_other_column_name)
|
89
89
|
|
90
90
|
# now this is going to serialize to column 'some_other_column_name'
|
91
91
|
attr_json :my_int, :integer
|
@@ -410,6 +410,16 @@ Except for the jsonb_contains stuff using postgres jsonb contains operator, I do
|
|
410
410
|
* Could we make these attributes work in ordinary AR where, same
|
411
411
|
as they do in jsonb_contains? Maybe.
|
412
412
|
|
413
|
+
## Development
|
414
|
+
|
415
|
+
While `attr_json` depends only on `active_record`, we run integration tests in the context of a full Rails app, in order to test working with simple_form and cocoon, among other things. (Via [combustion](https://github.com/pat/combustion), with app skeleton at [./spec/internal](./spec/internal)).
|
416
|
+
|
417
|
+
At present this does mean that all our automated tests are run in a full Rails environment, which is not great (any suggestions or PR's to fix this while still running integration tests under CI with full Rails app).
|
418
|
+
|
419
|
+
Tests are in rspec, run tests simply with `./bin/rspec`.
|
420
|
+
|
421
|
+
There is a `./bin/console` that will give you a console in the context of attr_json and all it's dependencies, including the combustion rails app, and the models defined there.
|
422
|
+
|
413
423
|
## Acknowledements and Prior Art
|
414
424
|
|
415
425
|
* The excellent work [Sean Griffin](https://twitter.com/sgrif) did on ActiveModel::Type
|
data/attr_json.gemspec
CHANGED
@@ -40,6 +40,8 @@ attributes use as much of the existing ActiveRecord architecture as we can.}
|
|
40
40
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
41
41
|
spec.require_paths = ["lib"]
|
42
42
|
|
43
|
+
spec.required_ruby_version = '>= 2.4.0'
|
44
|
+
|
43
45
|
spec.add_runtime_dependency "activerecord", ">= 5.0.0", "< 5.3"
|
44
46
|
|
45
47
|
spec.add_development_dependency "bundler", "~> 1.14"
|
data/doc_src/dirty_tracking.md
CHANGED
@@ -133,7 +133,7 @@ model.attr_json_changes.merged.changes_to_save
|
|
133
133
|
# and attr_jsons, as applicable for changes.
|
134
134
|
```
|
135
135
|
|
136
|
-
This will ordinarily include your json container attributes (eg `
|
136
|
+
This will ordinarily include your json container attributes (eg `json_attributes`)
|
137
137
|
too, as they will show up in ordinary AR dirty tracking since they are just AR
|
138
138
|
columns.
|
139
139
|
|
data/doc_src/forms.md
CHANGED
@@ -17,7 +17,7 @@ You can look at our [stub app used for integration tests](../spec/internal) as a
|
|
17
17
|
Use with form builder just as you would anything else.
|
18
18
|
|
19
19
|
f.text_field :some_string
|
20
|
-
f.
|
20
|
+
f.datetime_field :some_datetime
|
21
21
|
|
22
22
|
It _will_ work with the weird rails multi-param setting used for date fields.
|
23
23
|
|
data/lib/attr_json.rb
CHANGED
@@ -23,7 +23,7 @@ module AttrJson
|
|
23
23
|
# All references in code to "definition" are to a AttrJson::AttributeDefinition instance.
|
24
24
|
class Registry
|
25
25
|
def initialize(hash = {})
|
26
|
-
@name_to_definition = hash
|
26
|
+
@name_to_definition = hash.dup
|
27
27
|
@store_key_to_definition = {}
|
28
28
|
definitions.each { |d| store_key_index!(d) }
|
29
29
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module AttrJson
|
2
|
+
# Intentionally non-mutable, to avoid problems with subclass inheritance
|
3
|
+
# and rails class_attribute. Instead, you set to new Config object
|
4
|
+
# changed with {#merge}.
|
5
|
+
class Config
|
6
|
+
RECORD_ALLOWED_KEYS = %i{default_container_attribute}
|
7
|
+
MODEL_ALLOWED_KEYS = %i{unknown_key}
|
8
|
+
DEFAULTS = {
|
9
|
+
default_container_attribute: "json_attributes",
|
10
|
+
unknown_key: :raise
|
11
|
+
}
|
12
|
+
|
13
|
+
(MODEL_ALLOWED_KEYS + RECORD_ALLOWED_KEYS).each do |key|
|
14
|
+
define_method(key) do
|
15
|
+
attributes[key]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :mode
|
20
|
+
|
21
|
+
def initialize(options = {})
|
22
|
+
@mode = options.delete(:mode)
|
23
|
+
unless mode == :record || mode == :model
|
24
|
+
raise ArgumentError, "required :mode argument must be :record or :model"
|
25
|
+
end
|
26
|
+
valid_keys = mode == :record ? RECORD_ALLOWED_KEYS : MODEL_ALLOWED_KEYS
|
27
|
+
options.assert_valid_keys(valid_keys)
|
28
|
+
|
29
|
+
options.reverse_merge!(DEFAULTS.slice(*valid_keys))
|
30
|
+
|
31
|
+
@attributes = options
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns a new Config object, with changes merged in.
|
35
|
+
def merge(changes = {})
|
36
|
+
self.class.new(attributes.merge(changes).merge(mode: mode))
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
def attributes
|
42
|
+
@attributes
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/attr_json/model.rb
CHANGED
@@ -21,12 +21,18 @@ module AttrJson
|
|
21
21
|
# @note Includes ActiveModel::Model whether you like it or not. TODO, should it?
|
22
22
|
#
|
23
23
|
# You can control what happens if you set an unknown key (one that you didn't
|
24
|
-
# register with `attr_json`) with the
|
24
|
+
# register with `attr_json`) with the config attribute `attr_json_config(unknown_key:)`.
|
25
25
|
# * :raise (default) raise ActiveModel::UnknownAttributeError
|
26
26
|
# * :strip Ignore the unknown key and do not include it, without raising.
|
27
27
|
# * :allow Allow the unknown key and it's value to be in the serialized hash,
|
28
28
|
# and written to the database. May be useful for legacy data or columns
|
29
29
|
# that other software touches, to let unknown keys just flow through.
|
30
|
+
#
|
31
|
+
# class Something
|
32
|
+
# include AttrJson::Model
|
33
|
+
# attr_json_config(unknown_key: :ignore)
|
34
|
+
# #...
|
35
|
+
# end
|
30
36
|
module Model
|
31
37
|
extend ActiveSupport::Concern
|
32
38
|
|
@@ -41,13 +47,30 @@ module AttrJson
|
|
41
47
|
|
42
48
|
class_attribute :attr_json_registry, instance_accessor: false
|
43
49
|
self.attr_json_registry = ::AttrJson::AttributeDefinition::Registry.new
|
44
|
-
|
45
|
-
# :raise, :strip, :allow. :raise is default. Is there some way to enforce this.
|
46
|
-
class_attribute :attr_json_unknown_key
|
47
|
-
self.attr_json_unknown_key ||= :raise
|
48
50
|
end
|
49
51
|
|
50
52
|
class_methods do
|
53
|
+
def attr_json_config(new_values = {})
|
54
|
+
if new_values.present?
|
55
|
+
# get one without new values, then merge new values into it, and
|
56
|
+
# set it locally for this class.
|
57
|
+
@attr_json_config = attr_json_config.merge(new_values)
|
58
|
+
else
|
59
|
+
if instance_variable_defined?("@attr_json_config")
|
60
|
+
# we have a custom one for this class, return it.
|
61
|
+
@attr_json_config
|
62
|
+
elsif superclass.respond_to?(:attr_json_config)
|
63
|
+
# return superclass without setting it locally, so changes in superclass
|
64
|
+
# will continue effecting us.
|
65
|
+
superclass.attr_json_config
|
66
|
+
else
|
67
|
+
# no superclass, no nothing, set it to blank one.
|
68
|
+
@attr_json_config = Config.new(mode: :model)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
|
51
74
|
# Like `.new`, but translate store keys in hash
|
52
75
|
def new_from_serializable(attributes = {})
|
53
76
|
attributes = attributes.transform_keys do |key|
|
@@ -247,7 +270,7 @@ module AttrJson
|
|
247
270
|
|
248
271
|
|
249
272
|
def _attr_json_write_unknown_attribute(key, value)
|
250
|
-
case
|
273
|
+
case self.class.attr_json_config.unknown_key
|
251
274
|
when :strip
|
252
275
|
# drop it, no-op
|
253
276
|
when :allow
|
data/lib/attr_json/record.rb
CHANGED
@@ -17,8 +17,6 @@ module AttrJson
|
|
17
17
|
module Record
|
18
18
|
extend ActiveSupport::Concern
|
19
19
|
|
20
|
-
DEFAULT_CONTAINER_ATTRIBUTE = :json_attributes
|
21
|
-
|
22
20
|
included do
|
23
21
|
unless self < ActiveRecord::Base
|
24
22
|
raise TypeError, "AttrJson::Record can only be used with an ActiveRecord::Base model. #{self} does not appear to be one. Are you looking for ::AttrJson::Model?"
|
@@ -26,12 +24,45 @@ module AttrJson
|
|
26
24
|
|
27
25
|
class_attribute :attr_json_registry, instance_accessor: false
|
28
26
|
self.attr_json_registry = AttrJson::AttributeDefinition::Registry.new
|
29
|
-
|
30
|
-
class_attribute :default_json_container_attribute, instance_acessor: false
|
31
|
-
self.default_json_container_attribute ||= DEFAULT_CONTAINER_ATTRIBUTE
|
32
27
|
end
|
33
28
|
|
34
29
|
class_methods do
|
30
|
+
# Access or set class-wide json_attribute_config. Inherited by sub-classes,
|
31
|
+
# but setting on sub-classes is unique to subclass. Similar to how
|
32
|
+
# rails class_attribute's are used.
|
33
|
+
#
|
34
|
+
# @example access config
|
35
|
+
# SomeClass.attr_json_config
|
36
|
+
#
|
37
|
+
# @example set config variables
|
38
|
+
# class SomeClass < ActiveRecordBase
|
39
|
+
# include JsonAttribute::Record
|
40
|
+
#
|
41
|
+
# attr_json_config(default_container_attribute: "some_column")
|
42
|
+
# end
|
43
|
+
# TODO make Model match please.
|
44
|
+
def attr_json_config(new_values = {})
|
45
|
+
if new_values.present?
|
46
|
+
# get one without new values, then merge new values into it, and
|
47
|
+
# set it locally for this class.
|
48
|
+
@attr_json_config = attr_json_config.merge(new_values)
|
49
|
+
else
|
50
|
+
if instance_variable_defined?("@attr_json_config")
|
51
|
+
# we have a custom one for this class, return it.
|
52
|
+
@attr_json_config
|
53
|
+
elsif superclass.respond_to?(:attr_json_config)
|
54
|
+
# return superclass without setting it locally, so changes in superclass
|
55
|
+
# will continue effecting us.
|
56
|
+
superclass.attr_json_config
|
57
|
+
else
|
58
|
+
# no superclass, no nothing, set it to blank one.
|
59
|
+
@attr_json_config = Config.new(mode: :record)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
|
35
66
|
# Type can be a symbol that will be looked up in `ActiveModel::Type.lookup`,
|
36
67
|
# or an ActiveModel:::Type::Value).
|
37
68
|
#
|
@@ -47,9 +78,9 @@ module AttrJson
|
|
47
78
|
# @option options [String,Symbol] :store_key (nil) Serialize to JSON using
|
48
79
|
# given store_key, rather than name as would be usual.
|
49
80
|
#
|
50
|
-
# @option options [Symbol,String] :container_attribute (
|
81
|
+
# @option options [Symbol,String] :container_attribute (attr_json_config.default_container_attribute, normally `json_attributes`) The real
|
51
82
|
# json(b) ActiveRecord attribute/column to serialize as a key in. Defaults to
|
52
|
-
# `
|
83
|
+
# `attr_json_config.default_container_attribute`, which defaults to `:json_attributes`
|
53
84
|
#
|
54
85
|
# @option options [Boolean] :validate (true) Create an ActiveRecord::Validations::AssociatedValidator so
|
55
86
|
# validation errors on the attributes post up to self.
|
@@ -62,7 +93,7 @@ module AttrJson
|
|
62
93
|
options = {
|
63
94
|
rails_attribute: false,
|
64
95
|
validate: true,
|
65
|
-
container_attribute: self.
|
96
|
+
container_attribute: self.attr_json_config.default_container_attribute
|
66
97
|
}.merge!(options)
|
67
98
|
options.assert_valid_keys(AttributeDefinition::VALID_OPTIONS + [:validate, :rails_attribute])
|
68
99
|
container_attribute = options[:container_attribute]
|
@@ -74,9 +105,15 @@ module AttrJson
|
|
74
105
|
# only if it hasn't already been done. WARNING we are using internal
|
75
106
|
# Rails API here, but only way to do this lazily, which I thought was
|
76
107
|
# worth it. On the other hand, I think .attribute is idempotent, maybe we don't need it...
|
108
|
+
#
|
109
|
+
# We set default to empty hash, because that 'tricks' AR into knowing any
|
110
|
+
# application of defaults is a change that needs to be saved.
|
77
111
|
unless attributes_to_define_after_schema_loads[container_attribute.to_s] &&
|
78
|
-
attributes_to_define_after_schema_loads[container_attribute.to_s].first.is_a?(AttrJson::Type::ContainerAttribute)
|
79
|
-
|
112
|
+
attributes_to_define_after_schema_loads[container_attribute.to_s].first.is_a?(AttrJson::Type::ContainerAttribute) &&
|
113
|
+
attributes_to_define_after_schema_loads[container_attribute.to_s].first.model == self
|
114
|
+
# If this is already defined, but was for superclass, we need to define it again for
|
115
|
+
# this class.
|
116
|
+
attribute container_attribute.to_sym, AttrJson::Type::ContainerAttribute.new(self, container_attribute), default: -> { {} }
|
80
117
|
end
|
81
118
|
|
82
119
|
self.attr_json_registry = attr_json_registry.with(
|
@@ -137,20 +137,20 @@ module AttrJson
|
|
137
137
|
end
|
138
138
|
|
139
139
|
def saved_changes
|
140
|
-
|
141
|
-
return {} if
|
140
|
+
original_saved_changes = model.saved_changes
|
141
|
+
return {} if original_saved_changes.blank?
|
142
142
|
|
143
143
|
json_attr_changes = registry.definitions.collect do |definition|
|
144
|
-
if container_change =
|
145
|
-
old_v = container_change
|
146
|
-
new_v = container_change
|
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
147
|
if old_v != new_v
|
148
148
|
[ definition.name.to_s, formatted_before_after(old_v, new_v, definition) ]
|
149
149
|
end
|
150
150
|
end
|
151
151
|
end.compact.to_h
|
152
152
|
|
153
|
-
prepared_changes(json_attr_changes,
|
153
|
+
prepared_changes(json_attr_changes, original_saved_changes)
|
154
154
|
end
|
155
155
|
|
156
156
|
def saved_changes?
|
@@ -198,21 +198,21 @@ module AttrJson
|
|
198
198
|
end
|
199
199
|
|
200
200
|
def changes_to_save
|
201
|
-
|
201
|
+
original_changes_to_save = model.changes_to_save
|
202
202
|
|
203
|
-
return {} if
|
203
|
+
return {} if original_changes_to_save.blank?
|
204
204
|
|
205
205
|
json_attr_changes = registry.definitions.collect do |definition|
|
206
|
-
if container_change =
|
207
|
-
old_v = container_change
|
208
|
-
new_v = container_change
|
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
209
|
if old_v != new_v
|
210
210
|
[ definition.name.to_s, formatted_before_after(old_v, new_v, definition) ]
|
211
211
|
end
|
212
212
|
end
|
213
213
|
end.compact.to_h
|
214
214
|
|
215
|
-
prepared_changes(json_attr_changes,
|
215
|
+
prepared_changes(json_attr_changes, original_changes_to_save)
|
216
216
|
end
|
217
217
|
|
218
218
|
def has_changes_to_save?
|
@@ -38,19 +38,30 @@ module AttrJson
|
|
38
38
|
[key, attr_def ? attr_def.serialize(value) : value]
|
39
39
|
end.to_h)
|
40
40
|
end
|
41
|
-
|
42
|
-
|
41
|
+
|
42
|
+
# optional with_defaults arg is our own, not part of ActiveModel::Type API,
|
43
|
+
# used by {#changed_in_place?} so we can consider default application to
|
44
|
+
# be a change.
|
45
|
+
def deserialize(v, with_defaults: true)
|
46
|
+
h = super(v) || {}
|
43
47
|
model.attr_json_registry.definitions.each do |attr_def|
|
44
48
|
next unless container_attribute.to_s == attr_def.container_attribute.to_s
|
45
49
|
|
46
50
|
if h.has_key?(attr_def.store_key)
|
47
51
|
h[attr_def.store_key] = attr_def.deserialize(h[attr_def.store_key])
|
48
|
-
elsif attr_def.has_default?
|
52
|
+
elsif with_defaults && attr_def.has_default?
|
49
53
|
h[attr_def.store_key] = attr_def.provide_default!
|
50
54
|
end
|
51
55
|
end
|
52
56
|
h
|
53
57
|
end
|
58
|
+
|
59
|
+
# Just like superclass, but we tell deserialize to NOT apply defaults,
|
60
|
+
# so we can consider default-application to be a change.
|
61
|
+
def changed_in_place?(raw_old_value, new_value)
|
62
|
+
deserialize(raw_old_value, with_defaults: false) != new_value
|
63
|
+
end
|
64
|
+
|
54
65
|
end
|
55
66
|
end
|
56
67
|
end
|
data/lib/attr_json/version.rb
CHANGED
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: 0.
|
4
|
+
version: 0.3.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: 2018-05-
|
11
|
+
date: 2018-05-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -133,6 +133,7 @@ files:
|
|
133
133
|
- lib/attr_json.rb
|
134
134
|
- lib/attr_json/attribute_definition.rb
|
135
135
|
- lib/attr_json/attribute_definition/registry.rb
|
136
|
+
- lib/attr_json/config.rb
|
136
137
|
- lib/attr_json/model.rb
|
137
138
|
- lib/attr_json/model/cocoon_compat.rb
|
138
139
|
- lib/attr_json/nested_attributes.rb
|
@@ -163,7 +164,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
163
164
|
requirements:
|
164
165
|
- - ">="
|
165
166
|
- !ruby/object:Gem::Version
|
166
|
-
version:
|
167
|
+
version: 2.4.0
|
167
168
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
168
169
|
requirements:
|
169
170
|
- - ">="
|