attr_json 0.2.0 → 1.5.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 +5 -5
- data/.github/workflows/ci.yml +87 -0
- data/.github/workflows/future_rails_ci.yml +66 -0
- data/.gitignore +1 -0
- data/Appraisals +62 -0
- data/CHANGELOG.md +67 -0
- data/Gemfile +38 -35
- data/README.md +161 -36
- data/attr_json.gemspec +27 -4
- data/doc_src/dirty_tracking.md +1 -1
- data/doc_src/forms.md +76 -14
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/rails_5_0.gemfile +20 -0
- data/gemfiles/rails_5_1.gemfile +19 -0
- data/gemfiles/rails_5_2.gemfile +19 -0
- data/gemfiles/rails_6_0.gemfile +19 -0
- data/gemfiles/rails_6_1.gemfile +19 -0
- data/gemfiles/rails_7_0.gemfile +19 -0
- data/gemfiles/rails_edge.gemfile +19 -0
- data/lib/attr_json/attribute_definition/registry.rb +6 -1
- data/lib/attr_json/attribute_definition.rb +15 -2
- data/lib/attr_json/config.rb +55 -0
- data/lib/attr_json/model.rb +125 -42
- data/lib/attr_json/nested_attributes/writer.rb +26 -1
- data/lib/attr_json/nested_attributes.rb +7 -1
- data/lib/attr_json/record/dirty.rb +19 -13
- data/lib/attr_json/record/query_builder.rb +15 -3
- data/lib/attr_json/record/query_scopes.rb +6 -0
- data/lib/attr_json/record.rb +120 -32
- data/lib/attr_json/serialization_coder_from_type.rb +40 -0
- data/lib/attr_json/type/array.rb +6 -0
- data/lib/attr_json/type/container_attribute.rb +14 -3
- data/lib/attr_json/type/model.rb +38 -8
- data/lib/attr_json/type/polymorphic_model.rb +7 -0
- data/lib/attr_json/version.rb +1 -1
- data/lib/attr_json.rb +1 -1
- metadata +61 -21
- data/.travis.yml +0 -17
data/README.md
CHANGED
@@ -1,9 +1,10 @@
|
|
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)
|
3
4
|
[](https://badge.fury.io/rb/attr_json)
|
4
5
|
|
5
6
|
|
6
|
-
ActiveRecord attributes stored serialized in a json column, super smooth. For Rails 5.0
|
7
|
+
ActiveRecord attributes stored serialized in a json column, super smooth. For Rails 5.0 through 7.0. Ruby 2.4+.
|
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
10
|
|
@@ -11,8 +12,6 @@ Typed and cast like Active Record. Supporting [nested models](#nested), [dirty t
|
|
11
12
|
|
12
13
|
[Why might you want or not want this?](#why)
|
13
14
|
|
14
|
-
AttrJson is pre-1.0. The functionality that is documented here _is_ already implemented (these docs are real, not vaporware) and seems pretty solid. It may still have backwards-incompat changes before 1.0 release. Review and feedback is very welcome.
|
15
|
-
|
16
15
|
Developed for postgres, but most features should work with MySQL json columns too, although
|
17
16
|
has not yet been tested with MySQL.
|
18
17
|
|
@@ -85,7 +84,7 @@ class OtherModel < ActiveRecord::Base
|
|
85
84
|
include AttrJson::Record
|
86
85
|
|
87
86
|
# as a default for the model
|
88
|
-
|
87
|
+
attr_json_config(default_container_attribute: :some_other_column_name)
|
89
88
|
|
90
89
|
# now this is going to serialize to column 'some_other_column_name'
|
91
90
|
attr_json :my_int, :integer
|
@@ -116,7 +115,13 @@ model.json_attributes_before_type_cast # => string containing: {"__my_string":"f
|
|
116
115
|
|
117
116
|
You can of course combine `array`, `default`, `store_key`, and `container_attribute`
|
118
117
|
params however you like, with whatever types you like: symbols resolvable
|
119
|
-
with `
|
118
|
+
with `ActiveRecord::Type.lookup`, or any [ActiveModel::Type::Value](https://apidock.com/rails/ActiveRecord/Attributes/ClassMethods/attribute) subclass, built-in or custom.
|
119
|
+
|
120
|
+
You can register your custom `ActiveModel::Type::Value` in a Rails initializer or early on in your app boot sequence:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
ActiveRecord::Type.register(:my_type, MyActiveModelTypeSubclass)
|
124
|
+
```
|
120
125
|
|
121
126
|
<a name="querying"></a>
|
122
127
|
## Querying
|
@@ -134,6 +139,9 @@ MyModel.jsonb_contains(my_string: "foo", my_integer: 100).first
|
|
134
139
|
# Implemented with scopes, this is an ordinary relation, you can
|
135
140
|
# combine it with whatever, just like ordinary `where`.
|
136
141
|
|
142
|
+
MyModel.not_jsonb_contains(my:string: "foo", my_integer: 100).to_sql
|
143
|
+
# SELECT "products".* FROM "products" WHERE NOT (products.json_attributes @> ('{"my_string":"foo","my_integer":100}')::jsonb)
|
144
|
+
|
137
145
|
# typecasts much like ActiveRecord on query too:
|
138
146
|
MyModel.jsonb_contains(my_string: "foo", my_integer: "100")
|
139
147
|
# no problem
|
@@ -221,6 +229,26 @@ m.attr_jsons_before_type_cast
|
|
221
229
|
|
222
230
|
You can nest AttrJson::Model objects inside each other, as deeply as you like.
|
223
231
|
|
232
|
+
### Model-type defaults
|
233
|
+
|
234
|
+
If you want to set a default for an AttrJson::Model type, you should use a proc argument for
|
235
|
+
the default, to avoid accidentally re-using a shared global default value, similar to issues
|
236
|
+
people have with ruby Hash default.
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
attr_json :lang_and_value, LangAndValue.to_type, default: -> { LangAndValue.new(lang: "en", value: "default") }
|
240
|
+
```
|
241
|
+
|
242
|
+
You can also use a Hash value that will be cast to your model, no need for proc argument
|
243
|
+
in this case.
|
244
|
+
|
245
|
+
```ruby
|
246
|
+
attr_json :lang_and_value, LangAndValue.to_type, default: { lang: "en", value: "default" }
|
247
|
+
```
|
248
|
+
|
249
|
+
|
250
|
+
### Polymorphic model types
|
251
|
+
|
224
252
|
There is some support for "polymorphic" attributes that can hetereogenously contain instances of different AttrJson::Model classes, see comment docs at [AttrJson::Type::PolymorphicModel](./lib/attr_json/type/polymorphic_model.rb).
|
225
253
|
|
226
254
|
|
@@ -279,6 +307,77 @@ always mean 'contains' -- the previous query needs a `my_labels.hello`
|
|
279
307
|
which is a hash that includes the key/value, `lang: en`, it can have
|
280
308
|
other key/values in it too. String values will need to match exactly.
|
281
309
|
|
310
|
+
## Single AttrJson::Model serialized to an entire json column
|
311
|
+
|
312
|
+
The main use case of the gem is set up to let you combine multiple primitives and nested models
|
313
|
+
under different keys combined in a single json or jsonb column.
|
314
|
+
|
315
|
+
But you may also want to have one AttrJson::Model class that serializes to map one model class, as
|
316
|
+
a hash, to an entire json column on it's own.
|
317
|
+
|
318
|
+
`AttrJson::Model` can supply a simple coder for the [ActiveRecord serialization](https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html)
|
319
|
+
feature to easily do that.
|
320
|
+
|
321
|
+
```ruby
|
322
|
+
class MyModel
|
323
|
+
include AttrJson::Model
|
324
|
+
|
325
|
+
attr_json :some_string, :string
|
326
|
+
attr_json :some_int, :int
|
327
|
+
end
|
328
|
+
|
329
|
+
class MyTable < ApplicationRecord
|
330
|
+
serialize :some_json_column, MyModel.to_serialization_coder
|
331
|
+
end
|
332
|
+
|
333
|
+
MyTable.create(some_json_column: MyModel.new(some_string: "string"))
|
334
|
+
|
335
|
+
# will cast from hash for you
|
336
|
+
MyTable.create(some_json_column: { some_int: 12 })
|
337
|
+
|
338
|
+
# etc
|
339
|
+
```
|
340
|
+
|
341
|
+
To avoid errors raised at inconvenient times, we recommend you set these settings to make 'bad'
|
342
|
+
data turn into `nil`, consistent with most ActiveRecord types:
|
343
|
+
|
344
|
+
```ruby
|
345
|
+
class MyModel
|
346
|
+
include AttrJson::Model
|
347
|
+
|
348
|
+
attr_json_config(bad_cast: :as_nil, unknown_key: :strip)
|
349
|
+
# ...
|
350
|
+
end
|
351
|
+
```
|
352
|
+
|
353
|
+
And/or define a setter method to cast, and raise early on data problems:
|
354
|
+
|
355
|
+
```ruby
|
356
|
+
class MyTable < ApplicationRecord
|
357
|
+
serialize :some_json_column, MyModel.to_serialization_coder
|
358
|
+
|
359
|
+
def some_json_column=(val)
|
360
|
+
super( )
|
361
|
+
end
|
362
|
+
end
|
363
|
+
```
|
364
|
+
|
365
|
+
Serializing a model to an entire json column is a relatively recent feature, please let us know how it's working for you.
|
366
|
+
|
367
|
+
<a name="arbitrary-json-data"></a>
|
368
|
+
## Storing Arbitrary JSON data
|
369
|
+
|
370
|
+
Arbitrary JSON data (hashes, arrays, primitives of any depth) can be stored within attributes by using the rails built in `ActiveModel::Type::Value` as the attribute type. This is basically a "no-op" value type -- JSON alone will be used to serialize/deserialize whatever values you put there, because of the json type on the container field.
|
371
|
+
|
372
|
+
```ruby
|
373
|
+
class MyModel < ActiveRecord::Base
|
374
|
+
include AttrJson::Record
|
375
|
+
|
376
|
+
attr_json :arbitrary_hash, ActiveModel::Type::Value.new
|
377
|
+
end
|
378
|
+
|
379
|
+
```
|
380
|
+
|
282
381
|
|
283
382
|
<a name="forms"></a>
|
284
383
|
## Forms and Form Builders
|
@@ -287,7 +386,7 @@ Use with Rails form builders is supported pretty painlessly. Including with [sim
|
|
287
386
|
|
288
387
|
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.
|
289
388
|
|
290
|
-
To get simple_form to properly detect your attribute types, define your attributes with `rails_attribute: true`.
|
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)`
|
291
390
|
|
292
391
|
For more info, see doc page on [Use with Forms and Form Builders](doc_src/forms.md).
|
293
392
|
|
@@ -299,27 +398,29 @@ Rails 5.1+ on `attr_json`s on your ActiveRecord classes that include
|
|
299
398
|
`AttrJson::Record`, by including `AttrJson::Record::Dirty`.
|
300
399
|
Change-tracking methods are available off the `attr_json_changes` method.
|
301
400
|
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
attr_json :str, :string
|
307
|
-
end
|
308
|
-
|
309
|
-
model = MyModel.new
|
310
|
-
model.str = "old"
|
311
|
-
model.save
|
312
|
-
model.str = "new"
|
401
|
+
```ruby
|
402
|
+
class MyModel < ActiveRecord::Base
|
403
|
+
include AttrJson::Record
|
404
|
+
include AttrJson::Record::Dirty
|
313
405
|
|
314
|
-
|
315
|
-
|
406
|
+
attr_json :str, :string
|
407
|
+
end
|
316
408
|
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
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
|
423
|
+
```
|
323
424
|
|
324
425
|
More options are available, including merging changes from 'ordinary'
|
325
426
|
ActiveRecord attributes in. See docs on [Dirty Tracking](./doc_src/dirty_tracking.md)
|
@@ -377,38 +478,60 @@ to prevent overwriting other updates from processes.
|
|
377
478
|
|
378
479
|
## State of Code, and To Be Done
|
379
480
|
|
380
|
-
This is
|
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.
|
482
|
+
|
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.
|
381
484
|
|
382
|
-
|
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!).
|
383
486
|
|
384
|
-
|
487
|
+
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).
|
385
488
|
|
386
|
-
Feedback of any kind of _very welcome_, please feel free to use the issue tracker.
|
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!
|
387
490
|
|
388
491
|
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.
|
389
492
|
|
493
|
+
This is still mostly a single-maintainer operation, so has all the sustainability risks of that. Although there are other people using and contributing to it, check out the Github Issues and Pull Request tabs yourself to get a sense.
|
494
|
+
|
390
495
|
### Possible future features:
|
391
496
|
|
392
|
-
*
|
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]
|
393
500
|
|
394
|
-
*
|
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]
|
395
504
|
|
396
505
|
* Should we give AttrJson::Model a before_serialize hook that you might
|
397
506
|
want to use similar to AR before_save? Should AttrJson::Models
|
398
|
-
raise on trying to serialize an invalid model?
|
507
|
+
raise on trying to serialize an invalid model? [update: eh, hasn't really come up]
|
399
508
|
|
400
509
|
* There are limits to what you can do with just jsonb_contains
|
401
510
|
queries. We could support operations like `>`, `<`, `<>`
|
402
511
|
as [jsonb_accessor](https://github.com/devmynd/jsonb_accessor),
|
403
512
|
even accross keypaths. (At present, you could use a
|
404
513
|
before_savee to denormalize/renormalize copy your data into
|
405
|
-
ordinary AR columns/associations for searching. Or perhaps a postgres ts_vector for text searching. Needs to be worked out.)
|
514
|
+
ordinary AR columns/associations for searching. Or perhaps a postgres ts_vector for text searching. Needs to be worked out.) [update: interested, but not necessarily prioritized. This one would be interesting for a third-party PR draft!]
|
406
515
|
|
407
516
|
* We could/should probably support `jsonb_order` clauses, even
|
408
|
-
accross key paths, like jsonb_accessor.
|
517
|
+
accross key paths, like jsonb_accessor. [update: interested but not necessarily prioritized]
|
409
518
|
|
410
519
|
* Could we make these attributes work in ordinary AR where, same
|
411
|
-
as they do in jsonb_contains? Maybe.
|
520
|
+
as they do in jsonb_contains? Maybe. [update: probably not]
|
521
|
+
|
522
|
+
## Development
|
523
|
+
|
524
|
+
While `attr_json` depends only on `active_record`, we run integration tests in the context of a full Rails app, in order to test working with simple_form and cocoon, among other things. (Via [combustion](https://github.com/pat/combustion), with app skeleton at [./spec/internal](./spec/internal)).
|
525
|
+
|
526
|
+
At present this does mean that all our automated tests are run in a full Rails environment, which is not great (any suggestions or PR's to fix this while still running integration tests under CI with full Rails app).
|
527
|
+
|
528
|
+
Tests are in rspec, run tests simply with `./bin/rspec`.
|
529
|
+
|
530
|
+
We use [appraisal](https://github.com/thoughtbot/appraisal) to test with multiple rails versions, including on travis. Locally you can run `bundle exec appraisal rspec` to run tests multiple times for each rails version, or eg `bundle exec appraisal rails-5-1 rspec`. If the `Gemfile` _or_ `Appraisal` file changes, you may need to re-run `bundle exec appraisal install` and commit changes. (Try to put dev dependencies in gemspec instead of Gemfile, but sometimes it gets weird.)
|
531
|
+
|
532
|
+
* If you've been switching between rails versions and you get integration test failures, try `rm -rf spec/internal/tmp/cache`. Rails 6 does some things in there apparently not compatible with Rails 5, at least in our setup, and vice versa.
|
533
|
+
|
534
|
+
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.
|
412
535
|
|
413
536
|
## Acknowledements and Prior Art
|
414
537
|
|
@@ -430,3 +553,5 @@ Except for the jsonb_contains stuff using postgres jsonb contains operator, I do
|
|
430
553
|
* Didn't actually notice existing [json_attributes](https://github.com/joel/json_attributes)
|
431
554
|
until I was well on my way here. I think it's not updated for Rails5 or type-aware,
|
432
555
|
haven't looked at it too much.
|
556
|
+
|
557
|
+
* [store_model](https://github.com/DmitryTsepelev/store_model) was created after `attr_json`, and has some overlapping functionality.
|
data/attr_json.gemspec
CHANGED
@@ -40,11 +40,34 @@ 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.
|
43
|
+
spec.required_ruby_version = '>= 2.4.0'
|
44
44
|
|
45
|
-
|
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!
|
47
|
+
unless ENV['APPRAISAL_INITIALIZED'] || ENV["TRAVIS"] || ENV['CI']
|
48
|
+
spec.add_runtime_dependency "activerecord", ">= 5.0.0", "< 7.1"
|
49
|
+
end
|
50
|
+
|
51
|
+
spec.add_development_dependency "bundler"
|
46
52
|
spec.add_development_dependency "rake", ">= 10.0"
|
47
|
-
spec.add_development_dependency "rspec", "~> 3.
|
48
|
-
spec.add_development_dependency "database_cleaner", "~> 1.5"
|
53
|
+
spec.add_development_dependency "rspec", "~> 3.7"
|
49
54
|
spec.add_development_dependency "yard-activesupport-concern"
|
55
|
+
spec.add_development_dependency "appraisal", "~> 2.2"
|
56
|
+
|
57
|
+
# Working around annoying issue in selenium 3.x with ruby 3.0.
|
58
|
+
# we don't actually use rexml ourselves. selenium 3 is a dependency
|
59
|
+
# of webdrivers, and tries to use rexml without depending on it
|
60
|
+
# as is needed in ruby 3.
|
61
|
+
#
|
62
|
+
# https://github.com/SeleniumHQ/selenium/issues/9001
|
63
|
+
#
|
64
|
+
# if in the future you can remove this dependecy and still have tests pass
|
65
|
+
# under ruby 3.x, you're good.
|
66
|
+
spec.add_development_dependency "rexml"
|
67
|
+
|
68
|
+
# Used only for Capybara.server in our spec_helper.rb.
|
69
|
+
# webrick is no longer included in ruby 3.0, so has to
|
70
|
+
# be expressed as a dependecy, unless we switch
|
71
|
+
# capybara to use alternate webserver.
|
72
|
+
spec.add_development_dependency "webrick", "~> 1.0"
|
50
73
|
end
|
data/doc_src/dirty_tracking.md
CHANGED
@@ -133,7 +133,7 @@ model.attr_json_changes.merged.changes_to_save
|
|
133
133
|
# and attr_jsons, as applicable for changes.
|
134
134
|
```
|
135
135
|
|
136
|
-
This will ordinarily include your json container attributes (eg `
|
136
|
+
This will ordinarily include your json container attributes (eg `json_attributes`)
|
137
137
|
too, as they will show up in ordinary AR dirty tracking since they are just AR
|
138
138
|
columns.
|
139
139
|
|
data/doc_src/forms.md
CHANGED
@@ -17,25 +17,12 @@ You can look at our [stub app used for integration tests](../spec/internal) as a
|
|
17
17
|
Use with form builder just as you would anything else.
|
18
18
|
|
19
19
|
f.text_field :some_string
|
20
|
-
f.
|
20
|
+
f.datetime_field :some_datetime
|
21
21
|
|
22
22
|
It _will_ work with the weird rails multi-param setting used for date fields.
|
23
23
|
|
24
24
|
Don't forget you gotta handle strong params same as you would for any ordinary attribute.
|
25
25
|
|
26
|
-
### Arrays of simple attributes
|
27
|
-
|
28
|
-
attr_json :string_array, :string, array: true
|
29
|
-
|
30
|
-
The ActionView+ActiveRecord architecture isn't really setup for an array of "primitives", but you can make it work:
|
31
|
-
|
32
|
-
<% f.object.string_array.each do |str| %>
|
33
|
-
<%= f.text_field(:string_array, value: str, multiple: true) %>
|
34
|
-
<% end %>
|
35
|
-
|
36
|
-
That will display, submit and update fine, although when you try to handle reporting validation errors, you'll probably only be able to report on the array, not the specific element.
|
37
|
-
|
38
|
-
You may want to [use SimpleForm and create a custom input](https://github.com/plataformatec/simple_form#custom-inputs) to handle arrays of primitives in the way you want. Or you may want to consider an array of AttrJson::Model value types instead -- you can have a model with only one attribute! It can be handled more conventionally, see below.
|
39
26
|
|
40
27
|
### Embedded/Nested AttrJson::Model attributes
|
41
28
|
|
@@ -81,6 +68,37 @@ It should just work as you are expecting! You have to handle strong params as no
|
|
81
68
|
|
82
69
|
Note that the `AttrJsons::NestedAttributes` module also adds convenient rails-style `build_` methods for you. In the case above, you get a `build_one_event` and `build_many_event` (note singularization, cause that's how Rails does) method, which you can use much like Rails' `build_to_one_association` or `to_many_assocication.build` methods. You can turn off creation of the build methods by passing `define_build_method: false` to `attr_json_accepts_nested_attributes_for`.
|
83
70
|
|
71
|
+
### Shortcuts for attr_json_accepts_nested_attributes_for
|
72
|
+
|
73
|
+
You can automatically add the 'accepts nested attributes' call when you define your attribute, including with additional arguments like `reject_if`.
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
class SomeRecord < ApplicationRecord
|
77
|
+
include AttrJson::Record
|
78
|
+
include AttrJson::NestedAttributes
|
79
|
+
|
80
|
+
attr_json :one_event, Event.to_type, accepts_nested_attributes: true
|
81
|
+
attr_json :many_events, Event.to_type, array: true, accepts_nested_attributes: { reject_if: :all_blank }
|
82
|
+
end
|
83
|
+
```
|
84
|
+
|
85
|
+
You can also set defaults for the whole class:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
class SomeRecord < ApplicationRecord
|
89
|
+
include AttrJson::Record
|
90
|
+
include AttrJson::NestedAttributes
|
91
|
+
|
92
|
+
attr_json_config(default_accepts_nested_attributes_for: { reject_if: :all_blank })
|
93
|
+
|
94
|
+
attr_json :one_event, Event.to_type, accepts_nested_attributes: false # override default
|
95
|
+
attr_json :many_events, Event.to_type, array: true
|
96
|
+
end
|
97
|
+
```
|
98
|
+
|
99
|
+
If you're doing a lot of rails-style nested attributes form handling, the above probably makes sense. More convenient than having to do it each time, or repeat every param in another
|
100
|
+
long call.
|
101
|
+
|
84
102
|
### Nested multi-level/compound embedded models
|
85
103
|
|
86
104
|
A model inside a model inside a model? Some single and some array? No problem, should just work.
|
@@ -105,6 +123,50 @@ This will use the [ActiveRecord::Base.attribute](http://api.rubyonrails.org/clas
|
|
105
123
|
|
106
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.
|
107
125
|
|
126
|
+
### Arrays of simple attributes
|
127
|
+
|
128
|
+
attr_json :string_array, :string, array: true
|
129
|
+
|
130
|
+
The ActionView+ActiveRecord architecture isn't really setup for an array of "primitives", but we can kind of make it work:
|
131
|
+
|
132
|
+
<% f.object.string_array.each do |str| %>
|
133
|
+
<%= f.text_field(:string_array, value: str, multiple: true) %>
|
134
|
+
<% end %>
|
135
|
+
|
136
|
+
Or with simple_form, perhaps:
|
137
|
+
|
138
|
+
<%= f.input :string_array do %>
|
139
|
+
<% f.object.string_array.each do |str| %>
|
140
|
+
<%= f.text_field(:string_array, value: str, class: "form-control", multiple: true) %>
|
141
|
+
<% end %>
|
142
|
+
<% end %>
|
143
|
+
|
144
|
+
That will display, submit and update fine, although when you try to handle reporting validation errors, you'll probably only be able to report on the array, not the specific element.
|
145
|
+
|
146
|
+
The bigger problem is that if you use some javascript to allow people to add/remove elements,
|
147
|
+
and they remove _all_ elements, Rails params get no input for this attribute, and instead of setting it to empty array, doesn't touch it as in-memory. This is similar to the problem
|
148
|
+
with Rails collection checkboxes, and we can use a similar solution, of a hidden input value
|
149
|
+
making sure there's always at least one value.
|
150
|
+
|
151
|
+
<%= f.hidden_field "string_array[]", "" %>
|
152
|
+
<%= f.input :string_array do %>
|
153
|
+
<% f.object.string_array.each do |str| %>
|
154
|
+
<%= f.text_field(:string_array, value: str, class: "form-control", multiple: true) %>
|
155
|
+
<% end %>
|
156
|
+
<% end %>
|
157
|
+
|
158
|
+
But now we'll get an extra empty string added on to our array on every submit! We need to filter
|
159
|
+
out empty strings. `attr_json_accepts_nested_attributes_for` has an `_attributes=` method implementation when it recognizes an array of primitives, that simply filteres out empty strings. So! With `attr_json_accepts_nested_attributes_for :string_array`:
|
160
|
+
|
161
|
+
<%= f.hidden_field "string_array_attributes[]", "" %>
|
162
|
+
<%= f.input :string_array do %>
|
163
|
+
<% f.object.string_array.each do |str| %>
|
164
|
+
<%= f.text_field(:string_array_attributes, value: str, class: "form-control", multiple: true) %>
|
165
|
+
<% end %>
|
166
|
+
<% end %>
|
167
|
+
|
168
|
+
This is definitely getting a bit obtuse. You may want to consider an array of AttrJson::Model value types instead -- you can have a model with only one attribute! It can be handled somewhat more conventionally, as above.
|
169
|
+
|
108
170
|
## Cocoon
|
109
171
|
|
110
172
|
[Cocoon](https://github.com/nathanvda/cocoon) is one easy way to implement js-powered add- and remove-field functionality for to-many associations nested on a form with Rails.
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "combustion", "~> 0.9.0"
|
6
|
+
gem "rails", "~> 5.0.0"
|
7
|
+
gem "pg", "~> 0.18"
|
8
|
+
gem "rspec-rails", "~> 4.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 "webdrivers", "~> 4.0"
|
16
|
+
gem "selenium-webdriver"
|
17
|
+
gem "byebug"
|
18
|
+
gem "rails-ujs", require: false
|
19
|
+
|
20
|
+
gemspec path: "../"
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "combustion", "~> 0.9.0"
|
6
|
+
gem "rails", "~> 5.1.0"
|
7
|
+
gem "pg", "~> 1.0"
|
8
|
+
gem "rspec-rails", "~> 4.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 "webdrivers", "~> 4.0"
|
16
|
+
gem "selenium-webdriver"
|
17
|
+
gem "byebug"
|
18
|
+
|
19
|
+
gemspec path: "../"
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "combustion", "~> 0.9.0"
|
6
|
+
gem "rails", "~> 5.2.0"
|
7
|
+
gem "pg", "~> 1.0"
|
8
|
+
gem "rspec-rails", "~> 4.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 "webdrivers", "~> 4.0"
|
16
|
+
gem "selenium-webdriver"
|
17
|
+
gem "byebug"
|
18
|
+
|
19
|
+
gemspec path: "../"
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "combustion", "~> 1.0"
|
6
|
+
gem "rails", ">= 6.0.0", "< 6.1"
|
7
|
+
gem "pg", "~> 1.0"
|
8
|
+
gem "rspec-rails", "~> 4.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 "webdrivers", "~> 4.0"
|
16
|
+
gem "selenium-webdriver"
|
17
|
+
gem "byebug"
|
18
|
+
|
19
|
+
gemspec path: "../"
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "combustion", "~> 1.0"
|
6
|
+
gem "rails", "~> 6.1.0"
|
7
|
+
gem "pg", "~> 1.0"
|
8
|
+
gem "rspec-rails", "~> 4.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 "webdrivers", "~> 4.0"
|
16
|
+
gem "selenium-webdriver"
|
17
|
+
gem "byebug"
|
18
|
+
|
19
|
+
gemspec path: "../"
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "combustion", "~> 1.0"
|
6
|
+
gem "rails", "~> 7.0.0"
|
7
|
+
gem "pg", "~> 1.0"
|
8
|
+
gem "rspec-rails", "~> 4.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 "webdrivers", "~> 4.0"
|
16
|
+
gem "selenium-webdriver"
|
17
|
+
gem "byebug"
|
18
|
+
|
19
|
+
gemspec path: "../"
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "combustion", "~> 1.0", github: "pat/combustion"
|
6
|
+
gem "rails", git: "https://github.com/rails/rails.git", branch: "main"
|
7
|
+
gem "pg", "~> 1.0"
|
8
|
+
gem "rspec-rails", "~> 4.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 "webdrivers", "~> 4.0"
|
16
|
+
gem "selenium-webdriver"
|
17
|
+
gem "byebug"
|
18
|
+
|
19
|
+
gemspec path: "../"
|
@@ -23,7 +23,7 @@ module AttrJson
|
|
23
23
|
# All references in code to "definition" are to a AttrJson::AttributeDefinition instance.
|
24
24
|
class Registry
|
25
25
|
def initialize(hash = {})
|
26
|
-
@name_to_definition = hash
|
26
|
+
@name_to_definition = hash.dup
|
27
27
|
@store_key_to_definition = {}
|
28
28
|
definitions.each { |d| store_key_index!(d) }
|
29
29
|
end
|
@@ -54,6 +54,11 @@ module AttrJson
|
|
54
54
|
@name_to_definition.values
|
55
55
|
end
|
56
56
|
|
57
|
+
# Returns all registered attributes as an array of symbols
|
58
|
+
def attribute_names
|
59
|
+
@name_to_definition.keys
|
60
|
+
end
|
61
|
+
|
57
62
|
def container_attributes
|
58
63
|
@store_key_to_definition.keys.collect(&:to_s)
|
59
64
|
end
|