attr_json 1.1.0 → 1.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
  SHA256:
3
- metadata.gz: be1175039b6fbd78d5a9c1bcaae7c4ce003d29f403ef1e86d71d3c226e659a0b
4
- data.tar.gz: 588710deb7c2b6076ba1cf1094f9b4ab67fb7bd74cecfc8c560de5b0ef50eafb
3
+ metadata.gz: b6bbeb0707de928eae27b0bceafa5691486a2784d3065c71f91757e825bbaa94
4
+ data.tar.gz: 64b4d70c31b1b9499ee74f764940b283d45231ec6f4809cec35459e7d6ed9288
5
5
  SHA512:
6
- metadata.gz: d71520892d16915465eab7c9e373f5e5ecabb8efee7a295da6532fbedd0c9817890da7e689e31f1bf1c7b3c1cad73f071f3563d7eb6d5637516de40174fce627
7
- data.tar.gz: ea225c01754abfa5dec0e8749fd91ff68e1a329b54e684a27fee14655de7eb4eddf832538a961b14bbf83e222e0b90bfe554a8fdac001ca95aefb456353ed763
6
+ metadata.gz: 91aab7ce45bf723d4c76d9b2756ad65650bef1b223b2214bc9091118a8e4a04cb347fbac2438e7aaab4ed302f92c26f88b23f602e626b494f1aa5251f81473d7
7
+ data.tar.gz: 7862793a46004520e65cbb8b98a4857ec5441d6f0456dd8e368c4af7be8fafaefd8f0ef780f38f7361ea45b1117dbee4fe384492e44f5044aa295ee56488a99b
@@ -4,7 +4,26 @@ Notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [Unreleased](https://github.com/jrochkind/attr_json/compare/v1.1.0...HEAD)
7
+ ## [Unreleased](https://github.com/jrochkind/attr_json/compare/v1.2.0...HEAD)
8
+
9
+
10
+
11
+ ## [1.2.0](https://github.com/jrochkind/attr_json/compare/v1.1.0...v1.2.0)
12
+
13
+ ### Added
14
+
15
+ * attr_json_config(bad_cast: :as_nil) to avoid raising on data that can't be cast to a
16
+ AttrJson::Model, instead just casting to nil. https://github.com/jrochkind/attr_json/pull/95
17
+
18
+ * Documented and tested support for using ActiveRecord serialize to map one AttrJson::Model
19
+ to an entire column on it's own. https://github.com/jrochkind/attr_json/pull/89 and
20
+ https://github.com/jrochkind/attr_json/pull/93
21
+
22
+ * Better synchronization with ActiveRecord attributes when using rails_attribute:true, and a configurable true default_rails_attribute. Thanks @volkanunsal . https://github.com/jrochkind/attr_json/pull/94
23
+
24
+ ### Changed
25
+
26
+ * AttrJson::Model#== now requires same class for equality. And doesn't raise on certain arguments. https://github.com/jrochkind/attr_json/pull/90 Thanks @caiofilipemr for related bug report.
8
27
 
9
28
  ## [1.1.0](https://github.com/jrochkind/attr_json/compare/v1.0.0...v1.1.0)
10
29
 
data/README.md CHANGED
@@ -228,6 +228,26 @@ m.attr_jsons_before_type_cast
228
228
 
229
229
  You can nest AttrJson::Model objects inside each other, as deeply as you like.
230
230
 
231
+ ### Model-type defaults
232
+
233
+ If you want to set a default for an AttrJson::Model type, you should use a proc argument for
234
+ the default, to avoid accidentally re-using a shared global default value, similar to issues
235
+ people have with ruby Hash default.
236
+
237
+ ```ruby
238
+ attr_json :lang_and_value, LangAndValue.to_type, default: -> { LangAndValue.new(lang: "en", value: "default") }
239
+ ```
240
+
241
+ You can also use a Hash value that will be cast to your model, no need for proc argument
242
+ in this case.
243
+
244
+ ```ruby
245
+ attr_json :lang_and_value, LangAndValue.to_type, default: { lang: "en", value: "default" }
246
+ ```
247
+
248
+
249
+ ### Polymorphic model types
250
+
231
251
  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).
232
252
 
233
253
 
@@ -286,6 +306,63 @@ always mean 'contains' -- the previous query needs a `my_labels.hello`
286
306
  which is a hash that includes the key/value, `lang: en`, it can have
287
307
  other key/values in it too. String values will need to match exactly.
288
308
 
309
+ ## Single AttrJson::Model serialized to an entire json column
310
+
311
+ The main use case of the gem is set up to let you combine multiple primitives and nested models
312
+ under different keys combined in a single json or jsonb column.
313
+
314
+ But you may also want to have one AttrJson::Model class that serializes to map one model class, as
315
+ a hash, to an entire json column on it's own.
316
+
317
+ `AttrJson::Model` can supply a simple coder for the [ActiveRecord serialization](https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html)
318
+ feature to easily do that.
319
+
320
+ ```ruby
321
+ class MyModel
322
+ include AttrJson::Model
323
+
324
+ attr_json :some_string, :string
325
+ attr_json :some_int, :int
326
+ end
327
+
328
+ class MyTable < ApplicationRecord
329
+ serialize :some_json_column, MyModel.to_serialization_coder
330
+ end
331
+
332
+ MyTable.create(some_json_column: MyModel.new(some_string: "string"))
333
+
334
+ # will cast from hash for you
335
+ MyTable.create(some_json_column: { some_int: 12 })
336
+
337
+ # etc
338
+ ```
339
+
340
+ To avoid errors raised at inconvenient times, we recommend you set these settings to make 'bad'
341
+ data turn into `nil`, consistent with most ActiveRecord types:
342
+
343
+ ```ruby
344
+ class MyModel
345
+ include AttrJson::Model
346
+
347
+ attr_json_config(bad_cast: :as_nil, unknown_key: :strip)
348
+ # ...
349
+ end
350
+ ```
351
+
352
+ And/or define a setter method to cast, and raise early on data problems:
353
+
354
+ ```ruby
355
+ class MyTable < ApplicationRecord
356
+ serialize :some_json_column, MyModel.to_serialization_coder
357
+
358
+ def some_json_column=(val)
359
+ super( )
360
+ end
361
+ end
362
+ ```
363
+
364
+ Serializing a model to an entire json column is a relatively recent feature, please let us know how it's working for you.
365
+
289
366
  <a name="arbitrary-json-data"></a>
290
367
  ## Storing Arbitrary JSON data
291
368
 
@@ -308,7 +385,7 @@ Use with Rails form builders is supported pretty painlessly. Including with [sim
308
385
 
309
386
  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.
310
387
 
311
- To get simple_form to properly detect your attribute types, define your attributes with `rails_attribute: true`.
388
+ To get simple_form to properly detect your attribute types, define your attributes with `rails_attribute: true`. You can default rails_attribute to true with `attr_json_config(default_rails_attribute: true)`
312
389
 
313
390
  For more info, see doc page on [Use with Forms and Form Builders](doc_src/forms.md).
314
391
 
@@ -78,6 +78,12 @@
78
78
  @default != NO_DEFAULT_PROVIDED
79
79
  end
80
80
 
81
+ # Can be value or proc!
82
+ def default_argument
83
+ return nil unless has_default?
84
+ @default
85
+ end
86
+
81
87
  def provide_default!
82
88
  unless has_default?
83
89
  raise ArgumentError.new("This #{self.class.name} does not have a default defined!")
@@ -3,10 +3,20 @@ module AttrJson
3
3
  # and rails class_attribute. Instead, you set to new Config object
4
4
  # changed with {#merge}.
5
5
  class Config
6
- RECORD_ALLOWED_KEYS = %i{default_container_attribute default_accepts_nested_attributes}
7
- MODEL_ALLOWED_KEYS = %i{unknown_key}
6
+ RECORD_ALLOWED_KEYS = %i{
7
+ default_container_attribute
8
+ default_rails_attribute
9
+ default_accepts_nested_attributes
10
+ }
11
+
12
+ MODEL_ALLOWED_KEYS = %i{
13
+ unknown_key
14
+ bad_cast
15
+ }
16
+
8
17
  DEFAULTS = {
9
18
  default_container_attribute: "json_attributes",
19
+ default_rails_attribute: false,
10
20
  unknown_key: :raise
11
21
  }
12
22
 
@@ -7,6 +7,8 @@ require 'attr_json/attribute_definition/registry'
7
7
  require 'attr_json/type/model'
8
8
  require 'attr_json/model/cocoon_compat'
9
9
 
10
+ require 'attr_json/serialization_coder_from_type'
11
+
10
12
  module AttrJson
11
13
 
12
14
  # Meant for use in a plain class, turns it into an ActiveModel::Model
@@ -30,9 +32,39 @@ module AttrJson
30
32
  #
31
33
  # class Something
32
34
  # include AttrJson::Model
33
- # attr_json_config(unknown_key: :ignore)
35
+ # attr_json_config(unknown_key: :allow)
36
+ # #...
37
+ # end
38
+ #
39
+ # Similarly, trying to set a Model-valued attribute with an object that
40
+ # can't be cast to a Hash or Model at all will normally raise a
41
+ # AttrJson::Type::Model::BadCast error, but you can set config `bad_cast: :as_nil`
42
+ # to make it cast to nil, more like typical ActiveRecord cast.
43
+ #
44
+ # class Something
45
+ # include AttrJson::Model
46
+ # attr_json_config(bad_cast: :as_nil)
34
47
  # #...
35
48
  # end
49
+ #
50
+ # ## ActiveRecord `serialize`
51
+ #
52
+ # If you want to map a single AttrJson::Model to a json/jsonb column, you
53
+ # can use ActiveRecord `serialize` feature.
54
+ #
55
+ # https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html
56
+ #
57
+ # We provide a simple shim to give you the right API for a "coder" for AR serialize:
58
+ #
59
+ # class ValueModel
60
+ # include AttrJson::Model
61
+ # attr_json :some_string, :string
62
+ # end
63
+ #
64
+ # class SomeModel < ApplicationRecord
65
+ # serialize :some_json_column, ValueModel.to_serialize_coder
66
+ # end
67
+ #
36
68
  module Model
37
69
  extend ActiveSupport::Concern
38
70
 
@@ -88,6 +120,10 @@ module AttrJson
88
120
  @type ||= AttrJson::Type::Model.new(self)
89
121
  end
90
122
 
123
+ def to_serialization_coder
124
+ @serialization_coder ||= AttrJson::SerializationCoderFromType.new(to_type)
125
+ end
126
+
91
127
  # Type can be an instance of an ActiveModel::Type::Value subclass, or a symbol that will
92
128
  # be looked up in `ActiveModel::Type.lookup`
93
129
  #
@@ -240,12 +276,9 @@ module AttrJson
240
276
  end
241
277
 
242
278
  # Two AttrJson::Model objects are equal if they are the same class
243
- # or one is a subclass of the other, AND their #attributes are equal.
244
- # TODO: Should we allow subclasses to be equal, or should they have to be the
245
- # exact same class?
279
+ # AND their #attributes are equal.
246
280
  def ==(other_object)
247
- (other_object.is_a?(self.class) || self.is_a?(other_object.class)) &&
248
- other_object.attributes == self.attributes
281
+ other_object.class == self.class && other_object.attributes == self.attributes
249
282
  end
250
283
 
251
284
  # ActiveRecord objects [have a](https://github.com/rails/rails/blob/v5.1.5/activerecord/lib/active_record/nested_attributes.rb#L367-L374)
@@ -119,10 +119,11 @@ module AttrJson
119
119
  # @option options [Boolean] :rails_attribute (false) Create an actual ActiveRecord
120
120
  # `attribute` for name param. A Rails attribute isn't needed for our functionality,
121
121
  # but registering thusly will let the type be picked up by simple_form and
122
- # other tools that may look for it via Rails attribute APIs.
122
+ # other tools that may look for it via Rails attribute APIs. Default can be changed
123
+ # with `attr_json_config(default_rails_attribute: true)`
123
124
  def attr_json(name, type, **options)
124
125
  options = {
125
- rails_attribute: false,
126
+ rails_attribute: self.attr_json_config.default_rails_attribute,
126
127
  validate: true,
127
128
  container_attribute: self.attr_json_config.default_container_attribute,
128
129
  accepts_nested_attributes: self.attr_json_config.default_accepts_nested_attributes
@@ -160,7 +161,21 @@ module AttrJson
160
161
  # We don't actually use this for anything, we provide our own covers. But registering
161
162
  # it with usual system will let simple_form and maybe others find it.
162
163
  if options[:rails_attribute]
163
- self.attribute name.to_sym, self.attr_json_registry.fetch(name).type
164
+ attr_json_definition = attr_json_registry[name]
165
+
166
+ attribute_args = attr_json_definition.has_default? ? { default: attr_json_definition.default_argument } : {}
167
+ self.attribute name.to_sym, attr_json_definition.type, **attribute_args
168
+
169
+ # Ensure that rails attributes tracker knows about value we just fetched
170
+ # for this particular attribute. Yes, we are registering an after_find for each
171
+ # attr_json registered with rails_attribute:true, using the `name` from above under closure. .
172
+ after_find do
173
+ value = public_send(name)
174
+ if value && has_attribute?(name.to_sym)
175
+ write_attribute(name.to_sym, value)
176
+ self.send(:clear_attribute_changes, [name.to_sym])
177
+ end
178
+ end
164
179
  end
165
180
 
166
181
  _attr_jsons_module.module_eval do
@@ -173,6 +188,7 @@ module AttrJson
173
188
  # this simple way.
174
189
 
175
190
  define_method("#{name}=") do |value|
191
+ super(value) if defined?(super)
176
192
  attribute_def = self.class.attr_json_registry.fetch(name.to_sym)
177
193
  public_send(attribute_def.container_attribute)[attribute_def.store_key] = attribute_def.cast(value)
178
194
  end
@@ -0,0 +1,40 @@
1
+ module AttrJson
2
+
3
+ # A little wrapper to provide an object that provides #dump and #load method for use
4
+ # as a coder second-argument for [ActiveRecord Serialization](https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html),
5
+ # that simply delegates to #serialize and #deserialize from a ActiveModel::Type object.
6
+ #
7
+ # Created to be used with an AttrJson::Model type (AttrJson::Type::Model), but hypothetically
8
+ # could be a shim from anything with serialize/deserialize to dump/load instead.
9
+ #
10
+ # class ValueModel
11
+ # include AttrJson::Model
12
+ # attr_json :some_string, :string
13
+ # end
14
+ #
15
+ # class SomeModel < ApplicationRecord
16
+ # serialize :some_json_column, ValueModel.to_serialize_coder
17
+ # end
18
+ #
19
+ # Note when used with an AttrJson::Model, it will dump/load from a HASH, not a
20
+ # string. It assumes it's writing to a Json(b) column that wants/provides hashes,
21
+ # not strings.
22
+ class SerializationCoderFromType
23
+ attr_reader :type
24
+ def initialize(type)
25
+ @type = type
26
+ end
27
+
28
+ # Dump and load methods to support ActiveRecord Serialization
29
+ # too.
30
+ def dump(value)
31
+ type.serialize(value)
32
+ end
33
+
34
+ # Dump and load methods to support ActiveRecord Serialization
35
+ # too. https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html
36
+ def load(value)
37
+ type.deserialize(value)
38
+ end
39
+ end
40
+ end
@@ -7,6 +7,7 @@ module AttrJson
7
7
  # You create one with AttrJson::Model::Type.new(attr_json_model_class),
8
8
  # but normally that's only done in AttrJson::Model.to_type, there isn't
9
9
  # an anticipated need to create from any other place.
10
+ #
10
11
  class Model < ::ActiveModel::Type::Value
11
12
  class BadCast < ArgumentError ; end
12
13
 
@@ -35,11 +36,13 @@ module AttrJson
35
36
  elsif v.respond_to?(:to_h)
36
37
  # TODO Maybe we ought not to do this on #to_h?
37
38
  model.new_from_serializable(v.to_h)
39
+ elsif model.attr_json_config.bad_cast == :as_nil
40
+ # This was originally default behavior, to be like existing ActiveRecord
41
+ # which kind of silently does this for non-castable basic values. That
42
+ # ended up being confusing in the basic case, so now we raise by default,
43
+ # but this is still configurable.
44
+ nil
38
45
  else
39
- # Bad input. Originally we were trying to return nil, to be like
40
- # existing ActiveRecord which kind of silently does a basic value
41
- # with null input. But that ended up making things confusing, let's
42
- # just raise.
43
46
  raise BadCast.new("Can not cast from #{v.inspect} to #{self.type}")
44
47
  end
45
48
  end
@@ -50,7 +53,7 @@ module AttrJson
50
53
  elsif v.kind_of?(model)
51
54
  v.serializable_hash
52
55
  else
53
- cast(v).serializable_hash
56
+ (cast_v = cast(v)) && cast_v.serializable_hash
54
57
  end
55
58
  end
56
59
 
@@ -1,3 +1,3 @@
1
1
  module AttrJson
2
- VERSION = "1.1.0"
2
+ VERSION = "1.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: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Rochkind
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-06-02 00:00:00.000000000 Z
11
+ date: 2020-06-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -167,6 +167,7 @@ files:
167
167
  - lib/attr_json/record/dirty.rb
168
168
  - lib/attr_json/record/query_builder.rb
169
169
  - lib/attr_json/record/query_scopes.rb
170
+ - lib/attr_json/serialization_coder_from_type.rb
170
171
  - lib/attr_json/type/array.rb
171
172
  - lib/attr_json/type/container_attribute.rb
172
173
  - lib/attr_json/type/model.rb
@@ -179,7 +180,7 @@ licenses:
179
180
  metadata:
180
181
  homepage_uri: https://github.com/jrochkind/attr_json
181
182
  source_code_uri: https://github.com/jrochkind/attr_json
182
- post_install_message:
183
+ post_install_message:
183
184
  rdoc_options: []
184
185
  require_paths:
185
186
  - lib
@@ -194,9 +195,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
194
195
  - !ruby/object:Gem::Version
195
196
  version: '0'
196
197
  requirements: []
197
- rubyforge_project:
198
- rubygems_version: 2.7.6
199
- signing_key:
198
+ rubygems_version: 3.0.3
199
+ signing_key:
200
200
  specification_version: 4
201
201
  summary: ActiveRecord attributes stored serialized in a json column, super smooth.
202
202
  test_files: []