attr_json 1.1.0 → 1.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 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: []