attr_json 0.1.0 → 0.2.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 +21 -15
- data/{json_attribute.gemspec → attr_json.gemspec} +0 -0
- data/lib/attr_json.rb +1 -0
- data/lib/attr_json/type/polymorphic_model.rb +171 -0
- 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: 1033d8a923d5935a8b18e10612883d6756279089
|
4
|
+
data.tar.gz: 8c8b5faf3c24cc27d1d6e382b44c3a63b180826a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 57dec15a2438c7d5fa8b2460afc3c3fcedef01aad80a78bf9bb1511b9be68bb6b7166acb7dc9b1aa25d5614c8cd7eb688fc55433ed9bce1fdb8eab8d1061e22d
|
7
|
+
data.tar.gz: 76f0397e93f14b3ca5ff38c01bf4da3b6d4ea11080414d60e7da2126ecffc78926e087245fe82cc8d30a5d8bc6ffa41d2092b096ed8bcb2ad91149960610a85d
|
data/README.md
CHANGED
@@ -1,17 +1,20 @@
|
|
1
1
|
# AttrJson
|
2
|
+
[](https://travis-ci.org/jrochkind/attr_json)
|
3
|
+
[](https://badge.fury.io/rb/attr_json)
|
4
|
+
|
2
5
|
|
3
6
|
ActiveRecord attributes stored serialized in a json column, super smooth. For Rails 5.0, 5.1, or 5.2.
|
4
7
|
|
5
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).
|
6
9
|
|
7
|
-
Use your database as a typed object store via ActiveRecord, in the same models right next to ordinary ActiveRecord column-backed attributes and associations. Your json-serialized `attr_json` attributes use as much of the existing ActiveRecord architecture as we can
|
10
|
+
*Use your database as a typed object store via ActiveRecord, in the same models right next to ordinary ActiveRecord column-backed attributes and associations. Your json-serialized `attr_json` attributes use as much of the existing ActiveRecord architecture as we can.*
|
8
11
|
|
9
|
-
[
|
12
|
+
[Why might you want or not want this?](#why)
|
10
13
|
|
11
14
|
AttrJson is pre-1.0. The functionality that is documented here _is_ already implemented (these docs are real, not vaporware) and seems pretty solid. It may still have backwards-incompat changes before 1.0 release. Review and feedback is very welcome.
|
12
15
|
|
13
|
-
Developed for postgres, but most features should work with MySQL json columns too
|
14
|
-
|
16
|
+
Developed for postgres, but most features should work with MySQL json columns too, although
|
17
|
+
has not yet been tested with MySQL.
|
15
18
|
|
16
19
|
## Basic Use
|
17
20
|
|
@@ -67,7 +70,7 @@ save the record and then use the standard Rails `*_before_type_cast` method.
|
|
67
70
|
|
68
71
|
```ruby
|
69
72
|
model.save!
|
70
|
-
model.
|
73
|
+
model.json_attributes_before_type_cast
|
71
74
|
# => string containing: {"my_integer":12,"int_array":[12],"my_datetime":"2016-01-01T17:45:00.000Z"}
|
72
75
|
```
|
73
76
|
|
@@ -75,7 +78,7 @@ model.attr_jsons_before_type_cast
|
|
75
78
|
|
76
79
|
While the default is to assume you want to serialize in a column called
|
77
80
|
`json_attributes`, no worries, of course you can pick whatever named
|
78
|
-
jsonb column you like.
|
81
|
+
jsonb column you like, class-wide or per-attribute.
|
79
82
|
|
80
83
|
```ruby
|
81
84
|
class OtherModel < ActiveRecord::Base
|
@@ -92,10 +95,10 @@ class OtherModel < ActiveRecord::Base
|
|
92
95
|
end
|
93
96
|
```
|
94
97
|
|
95
|
-
##
|
98
|
+
## Store key different than attribute name/methods
|
96
99
|
|
97
100
|
You can also specify that the serialized JSON key
|
98
|
-
should be different than the attribute name
|
101
|
+
should be different than the attribute name/methods, by using the `store_key` argument.
|
99
102
|
|
100
103
|
```ruby
|
101
104
|
class MyModel < ActiveRecord::Base
|
@@ -106,9 +109,9 @@ end
|
|
106
109
|
|
107
110
|
model = MyModel.new
|
108
111
|
model.special_string = "foo"
|
109
|
-
model.
|
112
|
+
model.json_attributes # => {"__my_string"=>"foo"}
|
110
113
|
model.save!
|
111
|
-
model.
|
114
|
+
model.json_attributes_before_type_cast # => string containing: {"__my_string":"foo"}
|
112
115
|
```
|
113
116
|
|
114
117
|
You can of course combine `array`, `default`, `store_key`, and `container_attribute`
|
@@ -143,7 +146,7 @@ MyModel.jsonb_contains(int_array: [10, 20]) # it contains both, so still finds i
|
|
143
146
|
MyModel.jsonb_contains(int_array: [10, 1000]) # nope, returns nil, has to contain ALL listed in query for array args
|
144
147
|
```
|
145
148
|
|
146
|
-
`jsonb_contains` will
|
149
|
+
`jsonb_contains` will handle any `store_key` you have set -- you should specify
|
147
150
|
attribute name, it'll actually query on store_key. And properly handles any
|
148
151
|
`container_attribute` -- it'll look in the proper jsonb column.
|
149
152
|
|
@@ -218,6 +221,9 @@ m.attr_jsons_before_type_cast
|
|
218
221
|
|
219
222
|
You can nest AttrJson::Model objects inside each other, as deeply as you like.
|
220
223
|
|
224
|
+
There is some support for "polymorphic" attributes that can hetereogenously contain instances of different AttrJson::Model classes, see comment docs at [AttrJson::Type::PolymorphicModel](./lib/attr_json/type/polymorphic_model.rb).
|
225
|
+
|
226
|
+
|
221
227
|
```ruby
|
222
228
|
class SomeLabels
|
223
229
|
include AttrJson::Model
|
@@ -279,7 +285,7 @@ other key/values in it too. String values will need to match exactly.
|
|
279
285
|
|
280
286
|
Use with Rails form builders is supported pretty painlessly. Including with [simple_form](https://github.com/plataformatec/simple_form) and [cocoon](https://github.com/nathanvda/cocoon) (integration-tested in CI).
|
281
287
|
|
282
|
-
If you have nested AttrJson::Models you'd like to use in your forms much like Rails associated records: Where you would use Rails `
|
288
|
+
If you have nested AttrJson::Models you'd like to use in your forms much like Rails associated records: Where you would use Rails `accepts_nested_attributes_for`, instead `include AttrJson::NestedAttributes` and use `attr_json_accepts_nested_attributes_for`. Multiple levels of nesting are supported.
|
283
289
|
|
284
290
|
To get simple_form to properly detect your attribute types, define your attributes with `rails_attribute: true`.
|
285
291
|
|
@@ -318,6 +324,7 @@ Change-tracking methods are available off the `attr_json_changes` method.
|
|
318
324
|
More options are available, including merging changes from 'ordinary'
|
319
325
|
ActiveRecord attributes in. See docs on [Dirty Tracking](./doc_src/dirty_tracking.md)
|
320
326
|
|
327
|
+
<a name="why"></a>
|
321
328
|
## Do you want this?
|
322
329
|
|
323
330
|
Why might you want this?
|
@@ -334,7 +341,8 @@ Why might you want this?
|
|
334
341
|
* A "content management system" type project, where you need complex
|
335
342
|
structured data of various types, maybe needs to be vary depending
|
336
343
|
on plugins or configuration, or for different article types -- but
|
337
|
-
doesn't need to be very queryable generally
|
344
|
+
doesn't need to be very queryable generally -- or you have means of querying
|
345
|
+
other than a normalized rdbms schema.
|
338
346
|
|
339
347
|
* You want to version your models, which is tricky with associations between models.
|
340
348
|
Minimize associations by inlining the complex data into one table row.
|
@@ -381,8 +389,6 @@ Except for the jsonb_contains stuff using postgres jsonb contains operator, I do
|
|
381
389
|
|
382
390
|
### Possible future features:
|
383
391
|
|
384
|
-
* Polymorphic JSON attributes.
|
385
|
-
|
386
392
|
* partial updates for json hashes would be really nice: Using postgres jsonb merge operators to only overwrite what changed. In my initial attempts, AR doesn't make it easy to customize this.
|
387
393
|
|
388
394
|
* seamless compatibility with ransack
|
File without changes
|
data/lib/attr_json.rb
CHANGED
@@ -7,6 +7,7 @@ require 'attr_json/record'
|
|
7
7
|
require 'attr_json/model'
|
8
8
|
require 'attr_json/nested_attributes'
|
9
9
|
require 'attr_json/record/query_scopes'
|
10
|
+
require 'attr_json/type/polymorphic_model'
|
10
11
|
|
11
12
|
# Dirty not supported on Rails 5.0
|
12
13
|
if Gem.loaded_specs["activerecord"].version.release >= Gem::Version.new('5.1')
|
@@ -0,0 +1,171 @@
|
|
1
|
+
module AttrJson
|
2
|
+
module Type
|
3
|
+
# AttrJson::Type::PolymorphicModel can be used to create attr_json attributes
|
4
|
+
# that can hold any of various specified AttrJson::Model models. It is a
|
5
|
+
# _somewhat_ experimental feature.
|
6
|
+
#
|
7
|
+
# "polymorphic" may not be quite the right word, but we use it out of analogy
|
8
|
+
# with ActiveRecord [polymorphic assocications](http://guides.rubyonrails.org/association_basics.html#polymorphic-associations),
|
9
|
+
# which it resembles, as well as ActiveRecord [Single-Table Inheritance](http://guides.rubyonrails.org/association_basics.html#single-table-inheritance).
|
10
|
+
#
|
11
|
+
# Similar to these AR features, a PolymorphicModel-typed attribute will serialize the
|
12
|
+
# _model name_ of a given value in a `type` json hash key, so it can deserialize
|
13
|
+
# to the same correct model class.
|
14
|
+
#
|
15
|
+
# It can be used for single-model attributes, or arrays (which can be hetereogenous),
|
16
|
+
# in either AttrJson::Record or nested AttrJson::Models. If `CD`, `Book`, `Person`,
|
17
|
+
# and `Corporation` are all AttrJson::Model classes:
|
18
|
+
#
|
19
|
+
# attr_json :favorite, AttrJson::Type::PolymorphicAttribute.new(CD, Book)
|
20
|
+
# attr_json :authors, AttrJson::Type::PolymorphicAttribute.new(Person, Corporation), array: true
|
21
|
+
#
|
22
|
+
# Currently, you need a specific enumerated list of allowed types, and they all
|
23
|
+
# need to be AttrJson::Model classes. You can't at the moment have an "open" polymorphic
|
24
|
+
# type that can accept any AttrJson::Model.
|
25
|
+
#
|
26
|
+
# You can change the json key that the "type" (class name) for a value is stored to,
|
27
|
+
# when creating the type:
|
28
|
+
#
|
29
|
+
# attr_json, :author, AttrJson::Type::PolymorphicAttribute.new(Person, Corporation, type_key: "__type__")
|
30
|
+
#
|
31
|
+
# But if you already have existing data in the db, that's gonna be problematic to change on the fly.
|
32
|
+
#
|
33
|
+
# You can set attributes with a hash, but it needs to have an appropriate `type` key
|
34
|
+
# (or other as set by `type_key` arg). If it does not, or you try to set a non-hash
|
35
|
+
# value, you will get a AttrJson::Type::PolymorphicModel::TypeError. (maybe a validation
|
36
|
+
# error would be better? but it's not what it does now.)
|
37
|
+
#
|
38
|
+
# **Note** this
|
39
|
+
# also applies to loading non-compliant data from the database. If you have non-compliant
|
40
|
+
# data in the db, the only way to look at it will be as a serialized json string in top-level
|
41
|
+
# {#json_attributes_before_cast} (or other relevant container attribute.)
|
42
|
+
#
|
43
|
+
# There is no built-in form support for PolymorphicModels, you'll have to work it out.
|
44
|
+
#
|
45
|
+
# ## jsonb_contains support
|
46
|
+
#
|
47
|
+
# There is basic jsonb_contains support, but no sophisticated type-casting like normal, beyond
|
48
|
+
# the polymorphic attribute. But you can do:
|
49
|
+
#
|
50
|
+
# MyRecord.jsonb_contains(author: { name: "foo"})
|
51
|
+
# MyRecord.jsonb_contains(author: { name: "foo", type: "Corporation"})
|
52
|
+
# MyRecord.jsonb_contains(author: Corporation.new(name: "foo"))
|
53
|
+
#
|
54
|
+
class PolymorphicModel < ActiveModel::Type::Value
|
55
|
+
class TypeError < ::TypeError ; end
|
56
|
+
|
57
|
+
attr_reader :type_key, :unrecognized_type, :model_type_lookup
|
58
|
+
def initialize(*args)
|
59
|
+
options = { type_key: "type", unrecognized_type: :raise}.merge(
|
60
|
+
args.extract_options!.assert_valid_keys(:type_key, :unrecognized_type)
|
61
|
+
)
|
62
|
+
@type_key = options[:type_key]
|
63
|
+
@unrecognized_type = options[:unrecognized_type]
|
64
|
+
|
65
|
+
model_types = args
|
66
|
+
|
67
|
+
model_types.collect! do |m|
|
68
|
+
if m.respond_to?(:ancestors) && m.ancestors.include?(AttrJson::Model)
|
69
|
+
m.to_type
|
70
|
+
else
|
71
|
+
m
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
if bad_arg = model_types.find { |m| !m.is_a? AttrJson::Type::Model }
|
76
|
+
raise ArgumentError, "#{self.class.name} only works with AttrJson::Model / AttrJson::Type::Model, not '#{bad_arg.inspect}'"
|
77
|
+
end
|
78
|
+
if type_key_conflict = model_types.find { |m| m.model.attr_json_registry.has_attribute?(@type_key) }
|
79
|
+
raise ArgumentError, "conflict between type_key '#{@type_key}' and an existing attr_json in #{type_key_conflict.model}"
|
80
|
+
end
|
81
|
+
|
82
|
+
@model_type_lookup = model_types.collect do |type|
|
83
|
+
[type.model.name, type]
|
84
|
+
end.to_h
|
85
|
+
end
|
86
|
+
|
87
|
+
def model_names
|
88
|
+
model_type_lookup.keys
|
89
|
+
end
|
90
|
+
|
91
|
+
def model_types
|
92
|
+
model_type_lookup.values
|
93
|
+
end
|
94
|
+
|
95
|
+
# ActiveModel method, symbol type label
|
96
|
+
def type
|
97
|
+
@type ||= "any_of_#{model_types.collect(&:type).collect(&:to_s).join('_')}".to_sym
|
98
|
+
end
|
99
|
+
|
100
|
+
def cast(v)
|
101
|
+
if v.nil?
|
102
|
+
v
|
103
|
+
elsif model_names.include?(v.class.name)
|
104
|
+
v
|
105
|
+
elsif v.respond_to?(:to_hash)
|
106
|
+
cast_from_hash(v.to_hash)
|
107
|
+
elsif v.respond_to?(:to_h)
|
108
|
+
cast_from_hash(v.to_h)
|
109
|
+
else
|
110
|
+
raise_bad_model_name(v.class, v)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def serialize(v)
|
115
|
+
model_name = v.class.name
|
116
|
+
type = type_for_model_name(model_name)
|
117
|
+
|
118
|
+
raise_bad_model_name(model_name, v) if type.nil?
|
119
|
+
|
120
|
+
type.serialize(v).merge(type_key => model_name)
|
121
|
+
end
|
122
|
+
|
123
|
+
def deserialize(v)
|
124
|
+
cast(v)
|
125
|
+
end
|
126
|
+
|
127
|
+
def type_for_model_name(model_name)
|
128
|
+
model_type_lookup[model_name]
|
129
|
+
end
|
130
|
+
|
131
|
+
# This is used only by our own keypath-chaining query stuff.
|
132
|
+
# For PolymorphicModel type, it does no type casting, just
|
133
|
+
# sticks whatever you gave it in, which needs to be json-compat
|
134
|
+
# values.
|
135
|
+
def value_for_contains_query(key_path_arr, value)
|
136
|
+
hash_arg = {}
|
137
|
+
key_path_arr.each.with_index.inject(hash_arg) do |hash, (n, i)|
|
138
|
+
if i == key_path_arr.length - 1
|
139
|
+
hash[n] = value
|
140
|
+
else
|
141
|
+
hash[n] = {}
|
142
|
+
end
|
143
|
+
end
|
144
|
+
hash_arg
|
145
|
+
end
|
146
|
+
|
147
|
+
protected
|
148
|
+
|
149
|
+
def raise_missing_type_key(value)
|
150
|
+
raise TypeError, "AttrJson::Type::Polymorphic can't cast without '#{type_key}' key: #{value}"
|
151
|
+
end
|
152
|
+
|
153
|
+
def raise_bad_model_name(name, value)
|
154
|
+
raise TypeError, "This AttrJson::Type::PolymorphicType can only include {#{model_names.join(', ')}}, not '#{name}': #{value.inspect}"
|
155
|
+
end
|
156
|
+
|
157
|
+
def cast_from_hash(hash)
|
158
|
+
new_hash = hash.stringify_keys
|
159
|
+
model_name = new_hash.delete(type_key.to_s)
|
160
|
+
|
161
|
+
raise_missing_type_key(hash) if model_name.nil?
|
162
|
+
|
163
|
+
type = type_for_model_name(model_name)
|
164
|
+
|
165
|
+
raise_bad_model_name(model_name, hash) if type.nil?
|
166
|
+
|
167
|
+
type.cast(new_hash)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
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.2.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-04
|
11
|
+
date: 2018-05-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -122,6 +122,7 @@ files:
|
|
122
122
|
- LICENSE.txt
|
123
123
|
- README.md
|
124
124
|
- Rakefile
|
125
|
+
- attr_json.gemspec
|
125
126
|
- bin/console
|
126
127
|
- bin/rake
|
127
128
|
- bin/rspec
|
@@ -129,7 +130,6 @@ files:
|
|
129
130
|
- config.ru
|
130
131
|
- doc_src/dirty_tracking.md
|
131
132
|
- doc_src/forms.md
|
132
|
-
- json_attribute.gemspec
|
133
133
|
- lib/attr_json.rb
|
134
134
|
- lib/attr_json/attribute_definition.rb
|
135
135
|
- lib/attr_json/attribute_definition/registry.rb
|
@@ -146,6 +146,7 @@ files:
|
|
146
146
|
- lib/attr_json/type/array.rb
|
147
147
|
- lib/attr_json/type/container_attribute.rb
|
148
148
|
- lib/attr_json/type/model.rb
|
149
|
+
- lib/attr_json/type/polymorphic_model.rb
|
149
150
|
- lib/attr_json/version.rb
|
150
151
|
- playground_models.rb
|
151
152
|
homepage: https://github.com/jrochkind/attr_json
|