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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +22 -22
- data/.github/workflows/future_rails_ci.yml +3 -3
- data/Appraisals +40 -29
- data/CHANGELOG.md +144 -1
- data/Gemfile +8 -2
- data/README.md +83 -63
- data/attr_json.gemspec +8 -7
- data/doc_src/forms.md +3 -14
- data/gemfiles/rails_6_0.gemfile +3 -2
- data/gemfiles/rails_6_1.gemfile +2 -2
- data/gemfiles/rails_7_0.gemfile +2 -3
- data/gemfiles/{rails_5_1.gemfile → rails_7_1.gemfile} +4 -4
- data/gemfiles/{rails_5_2.gemfile → rails_7_2.gemfile} +4 -4
- data/gemfiles/{rails_5_0.gemfile → rails_8_0.gemfile} +5 -6
- data/gemfiles/rails_8_1.gemfile +18 -0
- data/gemfiles/rails_edge.gemfile +4 -4
- data/lib/attr_json/attribute_definition/registry.rb +43 -9
- data/lib/attr_json/attribute_definition.rb +51 -14
- data/lib/attr_json/config.rb +1 -2
- data/lib/attr_json/model/nested_model_validator.rb +27 -0
- data/lib/attr_json/model.rb +185 -53
- data/lib/attr_json/nested_attributes/builder.rb +2 -0
- data/lib/attr_json/nested_attributes/multiparameter_attribute_writer.rb +2 -0
- data/lib/attr_json/nested_attributes/writer.rb +6 -6
- data/lib/attr_json/nested_attributes.rb +7 -1
- data/lib/attr_json/record/query_builder.rb +2 -0
- data/lib/attr_json/record/query_scopes.rb +2 -0
- data/lib/attr_json/record.rb +113 -88
- data/lib/attr_json/serialization_coder_from_type.rb +2 -0
- data/lib/attr_json/type/array.rb +12 -3
- data/lib/attr_json/type/container_attribute.rb +2 -0
- data/lib/attr_json/type/model.rb +15 -4
- data/lib/attr_json/type/polymorphic_model.rb +43 -23
- data/lib/attr_json/version.rb +1 -1
- data/lib/attr_json.rb +21 -6
- data/playground_models.rb +2 -2
- metadata +15 -32
- data/doc_src/dirty_tracking.md +0 -155
- data/lib/attr_json/record/dirty.rb +0 -281
data/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# AttrJson
|
|
2
|
-
[](https://github.com/jrochkind/attr_json/actions?query=workflow%3ACI+branch%3Amaster)
|
|
3
|
-
[](https://github.com/jrochkind/attr_json/actions?query=workflow%3A%22CI+on+Future+Rails+Versions%22+branch%3Amaster)
|
|
4
|
-
[](https://badge.fury.io/rb/attr_json)
|
|
5
2
|
|
|
3
|
+
[](https://github.com/jrochkind/attr_json/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/jrochkind/attr_json/actions/workflows/future_rails_ci.yml)
|
|
5
|
+
[](https://badge.fury.io/rb/attr_json)
|
|
6
6
|
|
|
7
|
-
ActiveRecord attributes stored serialized in a json column, super smooth. For Rails
|
|
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](#
|
|
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 :
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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="
|
|
394
|
-
## Dirty tracking
|
|
430
|
+
<a name="ar_attributes"></a>
|
|
431
|
+
## ActiveRecord Attributes and Dirty tracking
|
|
395
432
|
|
|
396
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
554
|
+
## Acknowledements, Prior Art, alternatives
|
|
537
555
|
|
|
538
|
-
* The excellent work [
|
|
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
|
-
|
|
15
|
-
|
|
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.
|
|
43
|
+
spec.required_ruby_version = '>= 2.6.0'
|
|
44
44
|
|
|
45
|
-
#
|
|
46
|
-
# should never
|
|
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", ">=
|
|
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
|
|
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
|
|
data/gemfiles/rails_6_0.gemfile
CHANGED
|
@@ -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", "~>
|
|
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: "../"
|
data/gemfiles/rails_6_1.gemfile
CHANGED
|
@@ -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", "~>
|
|
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
|
|
data/gemfiles/rails_7_0.gemfile
CHANGED
|
@@ -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", "~>
|
|
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", "~>
|
|
6
|
-
gem "rails", "~>
|
|
5
|
+
gem "combustion", "~> 1.0"
|
|
6
|
+
gem "rails", "~> 7.1.0"
|
|
7
7
|
gem "pg", "~> 1.0"
|
|
8
|
-
gem "rspec-rails", "~>
|
|
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", "~>
|
|
6
|
-
gem "rails", "~>
|
|
5
|
+
gem "combustion", "~> 1.0"
|
|
6
|
+
gem "rails", "~> 7.2.0"
|
|
7
7
|
gem "pg", "~> 1.0"
|
|
8
|
-
gem "rspec-rails", "~>
|
|
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", "~>
|
|
6
|
-
gem "rails", "~>
|
|
7
|
-
gem "pg", "~> 0
|
|
8
|
-
gem "rspec-rails", "~>
|
|
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: "../"
|
data/gemfiles/rails_edge.gemfile
CHANGED
|
@@ -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", "~>
|
|
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 "
|
|
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
|
-
|
|
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
|
|
50
|
-
@store_key_to_definition[container_attribute
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
90
|
-
existing = container_hash[definition.store_key
|
|
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
|
|
128
|
+
container_hash[AttrJson.efficient_to_s(definition.store_key)] = definition
|
|
95
129
|
end
|
|
96
130
|
end
|
|
97
131
|
end
|