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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 72f8c96e1a2f0a885c25f6d606cf22a00b813245
4
- data.tar.gz: 6b038484149947ae8458addd45d54eabbcff2447
3
+ metadata.gz: 1033d8a923d5935a8b18e10612883d6756279089
4
+ data.tar.gz: 8c8b5faf3c24cc27d1d6e382b44c3a63b180826a
5
5
  SHA512:
6
- metadata.gz: 893ca2a4eabb9457df9abeaaee9b4ac09a068bdd86eede605c76ded7dc3c1f41a39d80fb2ab1de29091808b35b31af749cbf68163ee59df7a006bd133e0c4c9d
7
- data.tar.gz: ea4a9e617dabb6b4d5e5779e302f7fb55722588314740509b81ca9d3af64a98523ed41199219608e627b24c35b3673dc59fafaec57568cc6a431d60bfd25c392
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
- [![Build Status](https://travis-ci.org/jrochkind/attr_json.svg?branch=master)](https://travis-ci.org/jrochkind/attr_json)
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. Has not yet been tested.
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.attr_jsons_before_type_cast
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
- ## store key different than attribute name
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 with the `store_key` argument.
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.attr_jsons # => {"__my_string"=>"foo"}
112
+ model.json_attributes # => {"__my_string"=>"foo"}
110
113
  model.save!
111
- model.attr_jsons_before_type_cast # => string containing: {"__my_string":"foo"}
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 handlesany `store_key` you have set -- you should specify
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 `accept_nested_attributes_for`, instead `include AttrJson::NestedAttributes` and use `attr_json_accepts_nested_attributes_for`. Multiple levels of nesting are supported.
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module AttrJson
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
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.1.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-28 00:00:00.000000000 Z
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