attr_json 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Gem Version](https://badge.fury.io/rb/attr_json.svg)](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
|
- - ">="
|