attr_json 1.4.0 → 2.6.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +22 -22
  3. data/.github/workflows/future_rails_ci.yml +3 -3
  4. data/Appraisals +40 -29
  5. data/CHANGELOG.md +144 -1
  6. data/Gemfile +8 -2
  7. data/README.md +83 -63
  8. data/attr_json.gemspec +8 -7
  9. data/doc_src/forms.md +3 -14
  10. data/gemfiles/rails_6_0.gemfile +3 -2
  11. data/gemfiles/rails_6_1.gemfile +2 -2
  12. data/gemfiles/rails_7_0.gemfile +2 -3
  13. data/gemfiles/{rails_5_1.gemfile → rails_7_1.gemfile} +4 -4
  14. data/gemfiles/{rails_5_2.gemfile → rails_7_2.gemfile} +4 -4
  15. data/gemfiles/{rails_5_0.gemfile → rails_8_0.gemfile} +5 -6
  16. data/gemfiles/rails_8_1.gemfile +18 -0
  17. data/gemfiles/rails_edge.gemfile +4 -4
  18. data/lib/attr_json/attribute_definition/registry.rb +43 -9
  19. data/lib/attr_json/attribute_definition.rb +51 -14
  20. data/lib/attr_json/config.rb +1 -2
  21. data/lib/attr_json/model/nested_model_validator.rb +27 -0
  22. data/lib/attr_json/model.rb +185 -53
  23. data/lib/attr_json/nested_attributes/builder.rb +2 -0
  24. data/lib/attr_json/nested_attributes/multiparameter_attribute_writer.rb +2 -0
  25. data/lib/attr_json/nested_attributes/writer.rb +6 -6
  26. data/lib/attr_json/nested_attributes.rb +7 -1
  27. data/lib/attr_json/record/query_builder.rb +2 -0
  28. data/lib/attr_json/record/query_scopes.rb +2 -0
  29. data/lib/attr_json/record.rb +113 -88
  30. data/lib/attr_json/serialization_coder_from_type.rb +2 -0
  31. data/lib/attr_json/type/array.rb +12 -3
  32. data/lib/attr_json/type/container_attribute.rb +2 -0
  33. data/lib/attr_json/type/model.rb +15 -4
  34. data/lib/attr_json/type/polymorphic_model.rb +43 -23
  35. data/lib/attr_json/version.rb +1 -1
  36. data/lib/attr_json.rb +21 -6
  37. data/playground_models.rb +2 -2
  38. metadata +15 -32
  39. data/doc_src/dirty_tracking.md +0 -155
  40. data/lib/attr_json/record/dirty.rb +0 -281
data/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # AttrJson
2
- [![CI Status](https://github.com/jrochkind/attr_json/workflows/CI/badge.svg?branch=master)](https://github.com/jrochkind/attr_json/actions?query=workflow%3ACI+branch%3Amaster)
3
- [![CI Status](https://github.com/jrochkind/attr_json/workflows/CI%20on%20Future%20Rails%20Versions/badge.svg?branch=master)](https://github.com/jrochkind/attr_json/actions?query=workflow%3A%22CI+on+Future+Rails+Versions%22+branch%3Amaster)
4
- [![Gem Version](https://badge.fury.io/rb/attr_json.svg)](https://badge.fury.io/rb/attr_json)
5
2
 
3
+ [![CI](https://github.com/jrochkind/attr_json/actions/workflows/ci.yml/badge.svg)](https://github.com/jrochkind/attr_json/actions/workflows/ci.yml)
4
+ [![CI on Future Rails Versions](https://github.com/jrochkind/attr_json/actions/workflows/future_rails_ci.yml/badge.svg)](https://github.com/jrochkind/attr_json/actions/workflows/future_rails_ci.yml)
5
+ [![Gem Version](https://badge.fury.io/rb/attr_json.svg)](https://badge.fury.io/rb/attr_json)
6
6
 
7
- ActiveRecord attributes stored serialized in a json column, super smooth. For Rails 5.0 through 6.1. Ruby 2.4+.
7
+ ActiveRecord attributes stored serialized in a json column, super smooth. For Rails 6.0.x through 7.0.x. Ruby 2.7+.
8
8
 
9
- 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).
9
+ Typed and cast like Active Record. Supporting [nested models](#nested), [dirty tracking](#ar_attributes), 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).
10
10
 
11
11
  *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.*
12
12
 
@@ -18,7 +18,7 @@ has not yet been tested with MySQL.
18
18
  ## Basic Use
19
19
 
20
20
  ```ruby
21
- # migration
21
+ # migration, default column used is `json_attributes, but this can be changed
22
22
  class CreatMyModels < ActiveRecord::Migration[5.0]
23
23
  def change
24
24
  create_table :my_models do |t|
@@ -30,6 +30,14 @@ class CreatMyModels < ActiveRecord::Migration[5.0]
30
30
  end
31
31
  end
32
32
 
33
+ # An embedded model, if desired
34
+ class LangAndValue
35
+ include AttrJson::Model
36
+
37
+ attr_json :lang, :string, default: "en"
38
+ attr_json :value, :string
39
+ end
40
+
33
41
  class MyModel < ActiveRecord::Base
34
42
  include AttrJson::Record
35
43
 
@@ -39,14 +47,40 @@ class MyModel < ActiveRecord::Base
39
47
  attr_json :my_integer, :integer
40
48
  attr_json :my_datetime, :datetime
41
49
 
42
- # You can have an _array_ of those things too.
50
+ # You can have an _array_ of those things too. It will ordinarily default to empty array.
43
51
  attr_json :int_array, :integer, array: true
44
52
 
53
+ # The empty array default can be disabled with the following setting
54
+ attr_json :str_array, :string, array: true, default: AttrJson::AttributeDefinition::NO_DEFAULT_PROVIDED
55
+
45
56
  #and/or defaults
46
- attr_json :int_with_default, :integer, default: 100
57
+ attr_json :str_with_default, :string, default: "default value"
58
+
59
+ attr_json :embedded_lang_and_val, LangAndValue.to_type
47
60
  end
61
+
62
+ model = MyModel.create!(
63
+ my_integer: 101,
64
+ my_datetime: DateTime.new(2001,2,3,4,5,6),
65
+ embedded_lang_and_val: LangAndValue.new(value: "a sentance in default language english")
66
+ )
48
67
  ```
49
68
 
69
+ What will get serialized to your `json_attributes` column will look like:
70
+
71
+ ```json
72
+ {
73
+ "my_integer":101,
74
+ "my_datetime":"2001-02-03T04:05:06Z",
75
+ "str_with_default":"default value",
76
+ "embedded_lang_and_val": {
77
+ "lang":"en",
78
+ "value":"a sentance in default language english"
79
+ }
80
+ }
81
+ ```
82
+
83
+
50
84
  These attributes have type-casting behavior very much like ordinary ActiveRecord values.
51
85
 
52
86
  ```ruby
@@ -57,6 +91,8 @@ model.int_array = "12"
57
91
  model.int_array # => [12]
58
92
  model.my_datetime = "2016-01-01 17:45"
59
93
  model.my_datetime # => a Time object representing that, just like AR would cast
94
+ model.embedded_lang_and_val = { value: "val"}
95
+ model.embedded_lang_and_val #=> #<LangAndVal:0x000000010c9a7ad8 @attributes={"value"=>"val"...>
60
96
  ```
61
97
 
62
98
  You can use ordinary ActiveRecord validation methods with `attr_json` attributes.
@@ -64,14 +100,9 @@ You can use ordinary ActiveRecord validation methods with `attr_json` attributes
64
100
  All the `attr_json` attributes are serialized to json as keys in a hash, in a database jsonb/json column. By default, in a column `json_attributes`.
65
101
  If you look at `model.json_attributes`, you'll see values already cast to their ruby representations.
66
102
 
67
- But one way to see something like what it's really like in the db is to
68
- save the record and then use the standard Rails `*_before_type_cast` method.
103
+ To see JSON representations, we can use Rails [\*\_before_type_cast](https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/BeforeTypeCast.html) methods, [\*\-in_database](https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Dirty.html#method-i-attribute_in_database) and [\*\_for_database] methods (Rails 7.0+ only).
69
104
 
70
- ```ruby
71
- model.save!
72
- model.json_attributes_before_type_cast
73
- # => string containing: {"my_integer":12,"int_array":[12],"my_datetime":"2016-01-01T17:45:00.000Z"}
74
- ```
105
+ These methods can all be called on the container `json_attributes` json hash attribute (generally showing serialized JSON to string), or any individual attribute (generally showing in-memory JSON-able object). [This is a bit confusing and possibly not entirely consistent, needs more investigation.]
75
106
 
76
107
  ## Specifying db column to use
77
108
 
@@ -159,10 +190,8 @@ attribute name, it'll actually query on store_key. And properly handles any
159
190
  `container_attribute` -- it'll look in the proper jsonb column.
160
191
 
161
192
  Anything you can do with `jsonb_contains` should be handled
162
- by a [postgres `USING GIN` index](https://www.postgresql.org/docs/9.5/static/datatype-json.html#JSON-INDEXING)
163
- (I think! can anyone help confirm/deny?). To be sure, I recommend you
164
- investigate: Check out `to_sql` on any query to see what jsonb SQL it generates,
165
- and explore if you have the indexes you need.
193
+ by a [postgres `USING GIN` index](https://www.postgresql.org/docs/9.5/static/datatype-json.html#JSON-INDEXING). Figuring out how to use indexes for jsonb
194
+ queries can be confusing, [here is a good blog post](https://blog.kiprosh.com/postgres-gin-index-in-rails/).
166
195
 
167
196
  <a name="nested"></a>
168
197
  ## Nested models -- Structured/compound data
@@ -229,6 +258,13 @@ m.attr_jsons_before_type_cast
229
258
 
230
259
  You can nest AttrJson::Model objects inside each other, as deeply as you like.
231
260
 
261
+ You *can* edit nested models "in place", they will be properly saved.
262
+
263
+ m.lang_and_value.lang = "de"
264
+ m.save! # no problem!
265
+
266
+ For use with Rails forms, you may want to use `attr_json_accepts_nested_attributes_for` (like Rails `accepts_nested_attributes_for`, see doc page on [Use with Forms and Form Builders](https://github.com/jrochkind/attr_json/blob/master/doc_src/forms.md).
267
+
232
268
  ### Model-type defaults
233
269
 
234
270
  If you want to set a default for an AttrJson::Model type, you should use a proc argument for
@@ -328,6 +364,9 @@ end
328
364
 
329
365
  class MyTable < ApplicationRecord
330
366
  serialize :some_json_column, MyModel.to_serialization_coder
367
+
368
+ # NOTE: In Rails 7.1+, write:
369
+ # serialize :some_json_column, coder: MyModel.to_serialization_coder
331
370
  end
332
371
 
333
372
  MyTable.create(some_json_column: MyModel.new(some_string: "string"))
@@ -386,44 +425,29 @@ Use with Rails form builders is supported pretty painlessly. Including with [sim
386
425
 
387
426
  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.
388
427
 
389
- 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)`
390
-
391
428
  For more info, see doc page on [Use with Forms and Form Builders](doc_src/forms.md).
392
429
 
393
- <a name="dirty"></a>
394
- ## Dirty tracking
430
+ <a name="ar_attributes"></a>
431
+ ## ActiveRecord Attributes and Dirty tracking
395
432
 
396
- Full change-tracking, ActiveRecord::Attributes::Dirty-style, is available in
397
- Rails 5.1+ on `attr_json`s on your ActiveRecord classes that include
398
- `AttrJson::Record`, by including `AttrJson::Record::Dirty`.
399
- Change-tracking methods are available off the `attr_json_changes` method.
433
+ We endeavor to make record-level `attr_json` attributes available as standard ActiveRecord attributes, supporting that full API.
400
434
 
401
- ```ruby
402
- class MyModel < ActiveRecord::Base
403
- include AttrJson::Record
404
- include AttrJson::Record::Dirty
435
+ Standard [Rails dirty tracking](https://api.rubyonrails.org/classes/ActiveModel/Dirty.html) should work properly with AttrJson::Record attributes! We have a test suite demonstrating.
405
436
 
406
- attr_json :str, :string
407
- end
437
+ We actually keep the "canonical" copy of data inside the "container attribute" hash in the ActiveRecord model. This is because this is what will actually get saved when you save. So we have two copies, that we do our best to keep in sync.
408
438
 
409
- model = MyModel.new
410
- model.str = "old"
411
- model.save
412
- model.str = "new"
413
-
414
- # All and only "new" style dirty tracking methods (Raisl 5.1+)
415
- # are available:
416
-
417
- model.attr_json_changes.saved_changes
418
- model.attr_json_changes.changes_to_save
419
- model.attr_json_changes.saved_change_to_str?
420
- model.attr_json_changes.saved_change_to_str
421
- model.attr_json_changes.will_save_change_to_str?
422
- # etc
439
+ They get out of sync if you are doing unusual things like using the ActiveRecord attribute API directly (like calling `write_attribute` with an attr_json attribute). Even if this happens, mostly you won't notice. But one thing it will effect is dirty tracking.
440
+
441
+ If you ever need to sync the ActiveRecord attribute values from the AttrJson "canonical" copies, you can call `active_record_model.attr_json_sync_to_rails_attributes`. If you wanted to be 100% sure of dirty tracking, I suppose you could always call this method first. Sorry, this is the best we could do!
442
+
443
+ Note that ActiveRecord DirtyTracking will give you ruby objects, for instance for nested models, you might get:
444
+
445
+ ```ruby
446
+ record_obj.attribute_change_to_be_saved(:nested_model)
447
+ # => [#<object>, #<object>]
423
448
  ```
424
449
 
425
- More options are available, including merging changes from 'ordinary'
426
- ActiveRecord attributes in. See docs on [Dirty Tracking](./doc_src/dirty_tracking.md)
450
+ If you want to see JSON instead, you could call #as_json on the values. The Rails [\*\_before_type_cast](https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/BeforeTypeCast.html) and [\*\-in_database](https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Dirty.html#method-i-attribute_in_database) methods may also be useful.
427
451
 
428
452
  <a name="why"></a>
429
453
  ## Do you want this?
@@ -437,7 +461,7 @@ Why might you want this?
437
461
 
438
462
  * Single-Table Inheritance, with sub-classes that have non-shared
439
463
  data fields. You rather not make all those columns, some of which will then also appear
440
- to inapplicable sub-classes.
464
+ to inapplicable sub-classes. (**note** you may have trouble with [ActiveRecord #becomes](https://api.rubyonrails.org/v7.0.4/classes/ActiveRecord/Persistence.html#method-i-becomes) in some versions of Rails due to Rails bug. See https://github.com/jrochkind/attr_json/issues/189 and https://github.com/rails/rails/issues/47538))
441
465
 
442
466
  * A "content management system" type project, where you need complex
443
467
  structured data of various types, maybe needs to be vary depending
@@ -478,15 +502,13 @@ to prevent overwriting other updates from processes.
478
502
 
479
503
  ## State of Code, and To Be Done
480
504
 
481
- This code is solid and stable and is being used in production by at least a handful of people, including the primary maintainer, jrochkind.
505
+ This code is solid and stable and is being used in production. If you don't see a lot of activity, it might be because it's stable, rather than abandoned. Check to see if it's passing/supported on recent Rails? We test on "edge" unreleased rails to try to stay ahead of compatibility, and has worked through multiple major Rails verisons with few if any changes needed.
482
506
 
483
- The project is currently getting very little maintainance -- and is still working reliably through Rails releases. It is tested on edge rails and ruby (and has needed very few if any changes with subsequent releases), and I endeavor to keep it working as Rails keeps releasing.
484
-
485
- In order to keep the low-maintenace scenario sustainable, I am *very* cautious accepting new features, especially if they increase code complexity at all. Even if you have a working PR, I may be reluctant to accept it. I'm prioritizing sustainability and stability over new features, and so far this is working out well. However, discussion is always welcome! Especially when paired with code (failing tests for the bugfix or feature you want are super helpful on their own!).
507
+ In order to keep the low-maintenace scenario sustainable, I am *very* cautious accepting new features, especially if they increase code complexity at all. Even if you have a working PR, I may be reluctant to accept it. I'm prioritizing sustainability and stability over new features, and so far this is working out well. However, discussion is always welcome! Especially when paired with code (failing tests for the bugfix or feature you want are super helpful on their own!).
486
508
 
487
509
  We are committed to [semantic versioning](https://semver.org/) and will endeavor to release no backwards breaking changes without a major version. We are also serious about minimizing backwards incompat releases altogether (ie minimiing major version releases).
488
510
 
489
- Feedback of any kind of _very welcome_, please feel free to use the issue tracker. It is hard to get a sense of how many people are actually using this, which is helpful both for my own sense of reward and for anyone to get a sense of the size of the userbase -- feel free to say hi and let us know how you are using it!
511
+ Feedback of any kind of _very welcome_, please feel free to use the issue tracker. It is hard to get a sense of how many people are actually using this, which is helpful both for my own sense of reward and for anyone to get a sense of the size of the userbase -- feel free to say hi and let us know how you are using it!
490
512
 
491
513
  Except for the jsonb_contains stuff using postgres jsonb contains operator, I don't believe any postgres-specific features are used. It ought to work with MySQL, testing and feedback welcome. (Or a PR to test on MySQL?). My own interest is postgres.
492
514
 
@@ -494,13 +516,9 @@ This is still mostly a single-maintainer operation, so has all the sustainabilit
494
516
 
495
517
  ### Possible future features:
496
518
 
497
- * Make AttrJson::Model lean more heavily on ActiveModel::Attributes API that did not fully exist in first version of attr_json [hope for a future attr_json 2.0 release]
498
-
499
- * Make AttrJson::Record insist on creating rails cover attributes (no longer optional) and integrating more fully into rails, including rails dirty tricking, eliminating need for custom dirty tracking implementation. Overall decrease in lines of code. Can use jsonb_accessor as guide for some aspects. [hope for inclusion in future attr_json 2.0 release]
519
+ * Make AttrJson::Model lean more heavily on ActiveModel::Attributes API that did not fully exist in first version of attr_json (perhaps not, see https://github.com/jrochkind/attr_json/issues/18)
500
520
 
501
- * 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. [update: this is hard, probably not coming soon]
502
-
503
- * seamless compatibility with ransack [update: not necessarily prioritized]
521
+ * 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. [update: this is hard, probably not coming soon. See https://github.com/jrochkind/attr_json/issues/143]
504
522
 
505
523
  * Should we give AttrJson::Model a before_serialize hook that you might
506
524
  want to use similar to AR before_save? Should AttrJson::Models
@@ -533,9 +551,9 @@ We use [appraisal](https://github.com/thoughtbot/appraisal) to test with multipl
533
551
 
534
552
  There is a `./bin/console` that will give you a console in the context of attr_json and all it's dependencies, including the combustion rails app, and the models defined there.
535
553
 
536
- ## Acknowledements and Prior Art
554
+ ## Acknowledements, Prior Art, alternatives
537
555
 
538
- * The excellent work [Sean Griffin](https://twitter.com/sgrif) did on ActiveModel::Type
556
+ * The excellent work [sgrif](https://twitter.com/sgrif) did on ActiveModel::Type
539
557
  really lays the groundwork and makes this possible. Plus many other Rails developers.
540
558
  Rails has a reputation for being composed of messy or poorly designed code, but
541
559
  it's some really nice design in Rails that allows us to do some pretty powerful
@@ -555,3 +573,5 @@ There is a `./bin/console` that will give you a console in the context of attr_j
555
573
  haven't looked at it too much.
556
574
 
557
575
  * [store_model](https://github.com/DmitryTsepelev/store_model) was created after `attr_json`, and has some overlapping functionality.
576
+
577
+ * [store_attribute](https://github.com/palkan/store_attribute) is also a more recent addition. while it's not specifically about JSON, it could be used with an underlying JSON coder to give you typed json attributes.
data/attr_json.gemspec CHANGED
@@ -11,8 +11,8 @@ Gem::Specification.new do |spec|
11
11
 
12
12
  spec.summary = %q{ActiveRecord attributes stored serialized in a json column, super smooth.}
13
13
  spec.description = %q{ActiveRecord attributes stored serialized in a json column, super smooth.
14
- For Rails 5.0, 5.1, or 5.2. Typed and cast like Active Record. Supporting nested models,
15
- dirty tracking, some querying (with postgres jsonb contains), and working smoothy with form builders.
14
+ Typed and cast like Active Record. Supporting nested models, dirty tracking, some querying
15
+ (with postgres jsonb contains), and working smoothy with form builders.
16
16
 
17
17
  Use your database as a typed object store via ActiveRecord, in the same models right next to
18
18
  ordinary ActiveRecord column-backed attributes and associations. Your json-serialized attr_json
@@ -40,18 +40,19 @@ attributes use as much of the existing ActiveRecord architecture as we can.}
40
40
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
41
41
  spec.require_paths = ["lib"]
42
42
 
43
- spec.required_ruby_version = '>= 2.4.0'
43
+ spec.required_ruby_version = '>= 2.6.0'
44
44
 
45
- # Only to get CI to work on versions of Rails other than we release with,
46
- # should never release a gem with RAILS_GEM set!
45
+ # This conditional is only to get CI to work on versions of Rails other than
46
+ # we release with. The gem should never be released without the activerecord
47
+ # dependency included just as it is here, should never be released
48
+ # from an env tht has any of these variables set.
47
49
  unless ENV['APPRAISAL_INITIALIZED'] || ENV["TRAVIS"] || ENV['CI']
48
- spec.add_runtime_dependency "activerecord", ">= 5.0.0", "< 7.1"
50
+ spec.add_runtime_dependency "activerecord", ">= 6.0.0", "< 8.2"
49
51
  end
50
52
 
51
53
  spec.add_development_dependency "bundler"
52
54
  spec.add_development_dependency "rake", ">= 10.0"
53
55
  spec.add_development_dependency "rspec", "~> 3.7"
54
- spec.add_development_dependency "database_cleaner", "~> 1.5"
55
56
  spec.add_development_dependency "yard-activesupport-concern"
56
57
  spec.add_development_dependency "appraisal", "~> 2.2"
57
58
 
data/doc_src/forms.md CHANGED
@@ -31,7 +31,7 @@ With ordinary rails associations handled in the ordinary Rails way, you use [acc
31
31
  You can handle a single or array AttrJson::Model attr_json similarly, but you have to:
32
32
 
33
33
  * include AttrJson::NestedAttributes in your model, and then
34
- * use our own similar `attr_json_accepts_nested_attributes_for` instead. It _always_ has `allow_destroy`, and some of the other `accepts_nested_attributes_for` options also don't apply, see method for full options.
34
+ * use our own similar `attr_json_accepts_nested_attributes_for` instead. It _always_ has `allow_destroy`, and some of the other `accepts_nested_attributes_for` options also don't apply, see method for full options -- we expect you will usually want to use this with the `reject_if: :all_blank` option to ignore hashes with no non-nil values.
35
35
 
36
36
  ```ruby
37
37
  class Event
@@ -47,7 +47,7 @@ class MyRecord < ActiveRecord::Base
47
47
  attr_json :one_event, Event.to_type
48
48
  attr_json :many_events, Event.to_type, array: true
49
49
 
50
- attr_json_accepts_nested_attributes_for :one_event, :many_events
50
+ attr_json_accepts_nested_attributes_for :one_event, :many_events, reject_if: :all_blank
51
51
  end
52
52
 
53
53
  # In a form template...
@@ -109,19 +109,8 @@ Remember to add `include AttrJson::NestedAttributes` to all your AttrJson::Model
109
109
 
110
110
  One of the nice parts about [simple_form](https://github.com/plataformatec/simple_form) is how you can just give it `f.input`, and it figures out the right input for you.
111
111
 
112
- AttrJson by default, on an ActiveRecord::Base, doesn't register it's `attr_jsons` in the right way for simple_form to reflect and figure out their types. However, you can ask it to with `rails_attribute: true`.
112
+ AttrJson by default registers `attr_jsons` as Rails attributes, and provides other appropriate implementations in AttrJson::Model classes, such that simple_form should just work. There are limited CI tests in this project to confirm simple_form stays working.
113
113
 
114
- ```ruby
115
- class SomeRecord < ActiveRecord::Base
116
- include AttrJson::Record
117
-
118
- attr_json :my_date, :date, rails_attribute: true
119
- end
120
- ```
121
-
122
- This will use the [ActiveRecord::Base.attribute](http://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html) method to register the attribute and type, and SimpleForm will now be able to automatically look up attribute type just as you expect. (Q: Should we make this default on?)
123
-
124
- You don't need to do this in your nested AttrJson::Model classes, SimpleForm will already be able to reflect on their attribute types just fine as is.
125
114
 
126
115
  ### Arrays of simple attributes
127
116
 
@@ -5,14 +5,15 @@ source "https://rubygems.org"
5
5
  gem "combustion", "~> 1.0"
6
6
  gem "rails", ">= 6.0.0", "< 6.1"
7
7
  gem "pg", "~> 1.0"
8
- gem "rspec-rails", "~> 4.0"
8
+ gem "rspec-rails", "~> 5.0"
9
9
  gem "simple_form", ">= 4.0"
10
10
  gem "cocoon", ">= 1.2"
11
11
  gem "jquery-rails"
12
12
  gem "coffee-rails"
13
+ gem "sprockets-rails"
13
14
  gem "capybara", "~> 3.0"
14
- gem "webdrivers", "~> 4.0"
15
15
  gem "selenium-webdriver"
16
16
  gem "byebug"
17
+ gem "webdrivers", ">= 5.3.1"
17
18
 
18
19
  gemspec path: "../"
@@ -5,13 +5,13 @@ source "https://rubygems.org"
5
5
  gem "combustion", "~> 1.0"
6
6
  gem "rails", "~> 6.1.0"
7
7
  gem "pg", "~> 1.0"
8
- gem "rspec-rails", "~> 4.0"
8
+ gem "rspec-rails", "~> 6.0"
9
9
  gem "simple_form", ">= 4.0"
10
10
  gem "cocoon", ">= 1.2"
11
11
  gem "jquery-rails"
12
12
  gem "coffee-rails"
13
+ gem "sprockets-rails"
13
14
  gem "capybara", "~> 3.0"
14
- gem "webdrivers", "~> 4.0"
15
15
  gem "selenium-webdriver"
16
16
  gem "byebug"
17
17
 
@@ -5,15 +5,14 @@ source "https://rubygems.org"
5
5
  gem "combustion", "~> 1.0"
6
6
  gem "rails", "~> 7.0.0"
7
7
  gem "pg", "~> 1.0"
8
- gem "rspec-rails", "~> 4.0"
8
+ gem "rspec-rails", "~> 6.0"
9
9
  gem "simple_form", ">= 4.0"
10
10
  gem "cocoon", ">= 1.2"
11
11
  gem "jquery-rails"
12
12
  gem "coffee-rails"
13
+ gem "sprockets-rails"
13
14
  gem "capybara", "~> 3.0"
14
- gem "webdrivers", "~> 4.0"
15
15
  gem "selenium-webdriver"
16
16
  gem "byebug"
17
- gem "sprockets-rails"
18
17
 
19
18
  gemspec path: "../"
@@ -2,16 +2,16 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "combustion", "~> 0.9.0"
6
- gem "rails", "~> 5.1.0"
5
+ gem "combustion", "~> 1.0"
6
+ gem "rails", "~> 7.1.0"
7
7
  gem "pg", "~> 1.0"
8
- gem "rspec-rails", "~> 4.0"
8
+ gem "rspec-rails", "~> 6.0"
9
9
  gem "simple_form", ">= 4.0"
10
10
  gem "cocoon", ">= 1.2"
11
11
  gem "jquery-rails"
12
12
  gem "coffee-rails"
13
+ gem "sprockets-rails"
13
14
  gem "capybara", "~> 3.0"
14
- gem "webdrivers", "~> 4.0"
15
15
  gem "selenium-webdriver"
16
16
  gem "byebug"
17
17
 
@@ -2,16 +2,16 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "combustion", "~> 0.9.0"
6
- gem "rails", "~> 5.2.0"
5
+ gem "combustion", "~> 1.0"
6
+ gem "rails", "~> 7.2.0"
7
7
  gem "pg", "~> 1.0"
8
- gem "rspec-rails", "~> 4.0"
8
+ gem "rspec-rails", "~> 6.0"
9
9
  gem "simple_form", ">= 4.0"
10
10
  gem "cocoon", ">= 1.2"
11
11
  gem "jquery-rails"
12
12
  gem "coffee-rails"
13
+ gem "sprockets-rails"
13
14
  gem "capybara", "~> 3.0"
14
- gem "webdrivers", "~> 4.0"
15
15
  gem "selenium-webdriver"
16
16
  gem "byebug"
17
17
 
@@ -2,18 +2,17 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "combustion", "~> 0.9.0"
6
- gem "rails", "~> 5.0.0"
7
- gem "pg", "~> 0.18"
8
- gem "rspec-rails", "~> 4.0"
5
+ gem "combustion", "~> 1.0"
6
+ gem "rails", "~> 8.0.0"
7
+ gem "pg", "~> 1.0"
8
+ gem "rspec-rails", "~> 6.0"
9
9
  gem "simple_form", ">= 4.0"
10
10
  gem "cocoon", ">= 1.2"
11
11
  gem "jquery-rails"
12
12
  gem "coffee-rails"
13
+ gem "sprockets-rails"
13
14
  gem "capybara", "~> 3.0"
14
- gem "webdrivers", "~> 4.0"
15
15
  gem "selenium-webdriver"
16
16
  gem "byebug"
17
- gem "rails-ujs", require: false
18
17
 
19
18
  gemspec path: "../"
@@ -0,0 +1,18 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "combustion", "~> 1.0"
6
+ gem "rails", "~> 8.1.0"
7
+ gem "pg", "~> 1.0"
8
+ gem "rspec-rails", "~> 6.0"
9
+ gem "simple_form", ">= 4.0"
10
+ gem "cocoon", ">= 1.2"
11
+ gem "jquery-rails"
12
+ gem "coffee-rails"
13
+ gem "sprockets-rails"
14
+ gem "capybara", "~> 3.0"
15
+ gem "selenium-webdriver"
16
+ gem "byebug"
17
+
18
+ gemspec path: "../"
@@ -2,18 +2,18 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "combustion", "~> 1.0"
5
+ gem "combustion", "~> 1.0", github: "pat/combustion"
6
6
  gem "rails", git: "https://github.com/rails/rails.git", branch: "main"
7
7
  gem "pg", "~> 1.0"
8
- gem "rspec-rails", "~> 4.0"
8
+ gem "rspec-rails", "~> 6.0"
9
9
  gem "simple_form", ">= 4.0"
10
10
  gem "cocoon", ">= 1.2"
11
11
  gem "jquery-rails"
12
12
  gem "coffee-rails"
13
+ gem "sprockets-rails"
13
14
  gem "capybara", "~> 3.0"
14
- gem "webdrivers", "~> 4.0"
15
15
  gem "selenium-webdriver"
16
16
  gem "byebug"
17
- gem "sprockets-rails"
17
+ gem "rack", "~> 2.0"
18
18
 
19
19
  gemspec path: "../"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'attr_json/attribute_definition'
2
4
 
3
5
  module AttrJson
@@ -25,7 +27,10 @@ module AttrJson
25
27
  def initialize(hash = {})
26
28
  @name_to_definition = hash.dup
27
29
  @store_key_to_definition = {}
28
- definitions.each { |d| store_key_index!(d) }
30
+
31
+ @name_to_definition.values.each { |d| store_key_index!(d) }
32
+
33
+ @container_attributes_registered = Hash.new { Set.new }
29
34
  end
30
35
 
31
36
  def fetch(key, *args, &block)
@@ -46,12 +51,13 @@ module AttrJson
46
51
 
47
52
  # Can return nil if none found.
48
53
  def store_key_lookup(container_attribute, store_key)
49
- @store_key_to_definition[container_attribute.to_s] &&
50
- @store_key_to_definition[container_attribute.to_s][store_key.to_s]
54
+ @store_key_to_definition[AttrJson.efficient_to_s(container_attribute)] &&
55
+ @store_key_to_definition[AttrJson.efficient_to_s(container_attribute)][AttrJson.efficient_to_s(store_key)]
51
56
  end
52
57
 
53
58
  def definitions
54
- @name_to_definition.values
59
+ # Since we are immutable, we can cache this to avoid allocation in a hot spot
60
+ @stored_definitions ||= @name_to_definition.values
55
61
  end
56
62
 
57
63
  # Returns all registered attributes as an array of symbols
@@ -60,7 +66,7 @@ module AttrJson
60
66
  end
61
67
 
62
68
  def container_attributes
63
- @store_key_to_definition.keys.collect(&:to_s)
69
+ @store_key_to_definition.keys.collect { |s| AttrJson.efficient_to_s(s) }
64
70
  end
65
71
 
66
72
  # This is how you register additional definitions, as a non-mutating
@@ -73,6 +79,32 @@ module AttrJson
73
79
  end
74
80
  end
75
81
 
82
+
83
+ # We need to lazily set the container type only the FIRST time
84
+ #
85
+ # While also avoiding this triggering ActiveRecord to actually go to DB,
86
+ # we don't want DB connection forced on boot, that's a problem for many apps,
87
+ # including that may not have a DB connection in initial development.
88
+ # (#type_for_attribute forces DB connection)
89
+ #
90
+ # AND we need to call container attriubte on SUB-CLASS AGAIN, iff sub-class
91
+ # has any of it's own new registrations, to make sure we get the right type in
92
+ # sub-class!
93
+ #
94
+ # So we just keep track of whether we've registered ourselves, so we can
95
+ # first time we need to.
96
+ #
97
+ # While current implementation is simple, this has ended up a bit fragile,
98
+ # a different API that doesn't require us to do this implicitly lazily
99
+ # might be preferred! But this is what we got for now.
100
+ def register_container_attribute(attribute_name:, model:)
101
+ @container_attributes_registered[attribute_name.to_sym] << model
102
+ end
103
+
104
+ def container_attribute_registered?(attribute_name:, model:)
105
+ @container_attributes_registered[attribute_name.to_sym].include?(model)
106
+ end
107
+
76
108
  protected
77
109
 
78
110
  def add!(definition)
@@ -81,17 +113,19 @@ module AttrJson
81
113
  end
82
114
  @name_to_definition[definition.name.to_sym] = definition
83
115
  store_key_index!(definition)
116
+
117
+ @stored_definitions = nil
84
118
  end
85
119
 
86
120
  def store_key_index!(definition)
87
- container_hash = (@store_key_to_definition[definition.container_attribute.to_s] ||= {})
121
+ container_hash = (@store_key_to_definition[AttrJson.efficient_to_s(definition.container_attribute)] ||= {})
88
122
 
89
- if container_hash.has_key?(definition.store_key.to_s)
90
- existing = container_hash[definition.store_key.to_s]
123
+ if container_hash.has_key?(AttrJson.efficient_to_s(definition.store_key))
124
+ existing = container_hash[AttrJson.efficient_to_s(definition.store_key)]
91
125
  raise ArgumentError, "Can't add, store key `#{definition.store_key}` conflicts with existing attribute: #{existing.original_args}"
92
126
  end
93
127
 
94
- container_hash[definition.store_key.to_s] = definition
128
+ container_hash[AttrJson.efficient_to_s(definition.store_key)] = definition
95
129
  end
96
130
  end
97
131
  end