attr_json 0.1.0 → 0.2.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 +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
|
+
[![Build Status](https://travis-ci.org/jrochkind/attr_json.svg?branch=master)](https://travis-ci.org/jrochkind/attr_json)
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/attr_json.svg)](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
|