attr_json 0.5.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 80130e194fdf715bf967ea25fc6fff1d7e043f53479c1e539d9b9e2122233404
4
- data.tar.gz: df2d7d442df1dfec49becd935321a0fbdc8aebec5892e34d84d415c4f5a87471
3
+ metadata.gz: b6bbeb0707de928eae27b0bceafa5691486a2784d3065c71f91757e825bbaa94
4
+ data.tar.gz: 64b4d70c31b1b9499ee74f764940b283d45231ec6f4809cec35459e7d6ed9288
5
5
  SHA512:
6
- metadata.gz: 1e2712bca13ff4fa2b874b5f141070cd5f8e5ab306c6ff9f5e41a7129c4a7b740f6ce0a79a7f9cdf759195cb1c7d144bd0d8ea7ef6d46682487f8d75a47a1c25
7
- data.tar.gz: 1c86d1e8cea7a9413960fa357507fd5ba25e55c6f27820fb091bd8f4921c3aa7aa3274c90bd30f93b8cdde521d177d6a3cebe9154757038f2b30ba5cebbf970e
6
+ metadata.gz: 91aab7ce45bf723d4c76d9b2756ad65650bef1b223b2214bc9091118a8e4a04cb347fbac2438e7aaab4ed302f92c26f88b23f602e626b494f1aa5251f81473d7
7
+ data.tar.gz: 7862793a46004520e65cbb8b98a4857ec5441d6f0456dd8e368c4af7be8fafaefd8f0ef780f38f7361ea45b1117dbee4fe384492e44f5044aa295ee56488a99b
@@ -1,21 +1,64 @@
1
1
  #
2
- dist: trusty
3
2
  sudo: false
4
3
  addons:
5
4
  postgresql: '9.4'
6
5
  chrome: stable
7
6
  language: ruby
8
7
  cache: bundler
9
- rvm:
10
- - 2.4
11
- - 2.5.1
12
- gemfile:
13
- - gemfiles/rails_5_0.gemfile
14
- - gemfiles/rails_5_1.gemfile
15
- - gemfiles/rails_5_2.gemfile
16
- - gemfiles/rails_edge_6.gemfile
8
+
9
+ # rvm:
10
+ # - 2.4.5
11
+ # - 2.5.3
12
+ # - 2.6.5
13
+ # - 2.7.0
14
+ # gemfile:
15
+ # - gemfiles/rails_5_0.gemfile
16
+ # - gemfiles/rails_5_1.gemfile
17
+ # - gemfiles/rails_5_2.gemfile
18
+ # - gemfiles/rails_6_0.gemfile
19
+ # - gemfiles/rails_edge.gemfile
20
+
17
21
  before_install:
18
- - gem install bundler -v 1.14.6
22
+ - gem install bundler -v "~> 2.0"
23
+
19
24
  matrix:
25
+ include:
26
+ - rvm: 2.4.5
27
+ gemfile: gemfiles/rails_5_0.gemfile
28
+
29
+ - rvm: 2.4.5
30
+ gemfile: gemfiles/rails_5_1.gemfile
31
+
32
+ - rvm: 2.4.5
33
+ gemfile: gemfiles/rails_5_2.gemfile
34
+
35
+ - rvm: 2.5.3
36
+ gemfile: gemfiles/rails_5_0.gemfile
37
+
38
+ - rvm: 2.5.3
39
+ gemfile: gemfiles/rails_5_1.gemfile
40
+
41
+ - rvm: 2.5.3
42
+ gemfile: gemfiles/rails_5_2.gemfile
43
+
44
+ - rvm: 2.5.3
45
+ gemfile: gemfiles/rails_6_0.gemfile
46
+
47
+ - rvm: 2.6.5
48
+ gemfile: gemfiles/rails_5_2.gemfile
49
+
50
+ - rvm: 2.6.5
51
+ gemfile: gemfiles/rails_6_0.gemfile
52
+
53
+ - rvm: 2.7.0
54
+ gemfile: gemfiles/rails_6_0.gemfile
55
+
56
+ - rvm: 2.7.0
57
+ gemfile: gemfiles/rails_edge.gemfile
58
+
20
59
  allow_failures:
21
- - gemfile: gemfiles/rails_edge_6.gemfile
60
+ - gemfile: gemfiles/rails_edge.gemfile
61
+ fast_finish: true
62
+
63
+
64
+
data/Appraisals CHANGED
@@ -25,10 +25,17 @@ appraise "rails-5-2" do
25
25
  gem "pg", "~> 1.0"
26
26
  end
27
27
 
28
+ appraise "rails-6-0" do
29
+ gem 'combustion', "~> 1.0"
30
+
31
+ gem "rails", ">= 6.0.0.beta1", "< 6.1"
32
+ gem "pg", "~> 1.0"
33
+ end
34
+
28
35
  appraise "rails-edge-6" do
29
36
  # Edge rails needs unreleased combustion
30
37
  # https://github.com/pat/combustion/issues/92
31
- gem 'combustion', git: "https://github.com/pat/combustion.git"
38
+ gem 'combustion', "~> 1.0"
32
39
 
33
40
  gem "rails", git: "https://github.com/rails/rails.git", branch: "master"
34
41
  gem "pg", "~> 1.0"
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+ Notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased](https://github.com/jrochkind/attr_json/compare/v1.2.0...HEAD)
8
+
9
+
10
+
11
+ ## [1.2.0](https://github.com/jrochkind/attr_json/compare/v1.1.0...v1.2.0)
12
+
13
+ ### Added
14
+
15
+ * attr_json_config(bad_cast: :as_nil) to avoid raising on data that can't be cast to a
16
+ AttrJson::Model, instead just casting to nil. https://github.com/jrochkind/attr_json/pull/95
17
+
18
+ * Documented and tested support for using ActiveRecord serialize to map one AttrJson::Model
19
+ to an entire column on it's own. https://github.com/jrochkind/attr_json/pull/89 and
20
+ https://github.com/jrochkind/attr_json/pull/93
21
+
22
+ * Better synchronization with ActiveRecord attributes when using rails_attribute:true, and a configurable true default_rails_attribute. Thanks @volkanunsal . https://github.com/jrochkind/attr_json/pull/94
23
+
24
+ ### Changed
25
+
26
+ * AttrJson::Model#== now requires same class for equality. And doesn't raise on certain arguments. https://github.com/jrochkind/attr_json/pull/90 Thanks @caiofilipemr for related bug report.
27
+
28
+ ## [1.1.0](https://github.com/jrochkind/attr_json/compare/v1.0.0...v1.1.0)
29
+
30
+ ### Added
31
+
32
+ * not_jsonb_contains query method, like `jsonb_contains` but negated. https://github.com/jrochkind/attr_json/pull/85
data/Gemfile CHANGED
@@ -10,7 +10,7 @@ gemspec
10
10
  # We also have these development dependencies here in the Gemfile instead of the
11
11
  # gemspec so appraisal can override them from our Appraisal file.
12
12
 
13
- gem 'combustion', '~> 0.9.0'
13
+ gem 'combustion', '~> 1.1'
14
14
 
15
15
  # all of rails is NOT a dependency, just activerecord.
16
16
  # But we use it for integration testing with combustion. Hmm, a bit annoying
@@ -24,13 +24,13 @@ gem 'rails'
24
24
  gem 'railties'
25
25
 
26
26
  gem "pg"
27
- gem "rspec-rails", "~> 3.7"
27
+ gem "rspec-rails", "~> 4.0"
28
28
  gem "simple_form", ">= 4.0"
29
29
  gem 'cocoon', ">= 1.2"
30
30
  gem 'jquery-rails'
31
31
 
32
32
  gem 'capybara', "~> 3.0"
33
- gem "chromedriver-helper"
33
+ gem 'webdrivers', '~> 4.0'
34
34
  gem "selenium-webdriver"
35
35
 
36
36
  gem "byebug"
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![Gem Version](https://badge.fury.io/rb/attr_json.svg)](https://badge.fury.io/rb/attr_json)
4
4
 
5
5
 
6
- ActiveRecord attributes stored serialized in a json column, super smooth. For Rails 5.0, 5.1, or 5.2. Ruby 2.4+.
6
+ ActiveRecord attributes stored serialized in a json column, super smooth. For Rails 5.0, 5.1, 5.2, or 6.0. Ruby 2.4+.
7
7
 
8
8
  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
9
 
@@ -11,8 +11,6 @@ Typed and cast like Active Record. Supporting [nested models](#nested), [dirty t
11
11
 
12
12
  [Why might you want or not want this?](#why)
13
13
 
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
14
  Developed for postgres, but most features should work with MySQL json columns too, although
17
15
  has not yet been tested with MySQL.
18
16
 
@@ -140,6 +138,9 @@ MyModel.jsonb_contains(my_string: "foo", my_integer: 100).first
140
138
  # Implemented with scopes, this is an ordinary relation, you can
141
139
  # combine it with whatever, just like ordinary `where`.
142
140
 
141
+ MyModel.not_jsonb_contains(my:string: "foo", my_integer: 100).to_sql
142
+ # SELECT "products".* FROM "products" WHERE NOT (products.json_attributes @> ('{"my_string":"foo","my_integer":100}')::jsonb)
143
+
143
144
  # typecasts much like ActiveRecord on query too:
144
145
  MyModel.jsonb_contains(my_string: "foo", my_integer: "100")
145
146
  # no problem
@@ -227,6 +228,26 @@ m.attr_jsons_before_type_cast
227
228
 
228
229
  You can nest AttrJson::Model objects inside each other, as deeply as you like.
229
230
 
231
+ ### Model-type defaults
232
+
233
+ If you want to set a default for an AttrJson::Model type, you should use a proc argument for
234
+ the default, to avoid accidentally re-using a shared global default value, similar to issues
235
+ people have with ruby Hash default.
236
+
237
+ ```ruby
238
+ attr_json :lang_and_value, LangAndValue.to_type, default: -> { LangAndValue.new(lang: "en", value: "default") }
239
+ ```
240
+
241
+ You can also use a Hash value that will be cast to your model, no need for proc argument
242
+ in this case.
243
+
244
+ ```ruby
245
+ attr_json :lang_and_value, LangAndValue.to_type, default: { lang: "en", value: "default" }
246
+ ```
247
+
248
+
249
+ ### Polymorphic model types
250
+
230
251
  There is some support for "polymorphic" attributes that can hetereogenously contain instances of different AttrJson::Model classes, see comment docs at [AttrJson::Type::PolymorphicModel](./lib/attr_json/type/polymorphic_model.rb).
231
252
 
232
253
 
@@ -285,6 +306,77 @@ always mean 'contains' -- the previous query needs a `my_labels.hello`
285
306
  which is a hash that includes the key/value, `lang: en`, it can have
286
307
  other key/values in it too. String values will need to match exactly.
287
308
 
309
+ ## Single AttrJson::Model serialized to an entire json column
310
+
311
+ The main use case of the gem is set up to let you combine multiple primitives and nested models
312
+ under different keys combined in a single json or jsonb column.
313
+
314
+ But you may also want to have one AttrJson::Model class that serializes to map one model class, as
315
+ a hash, to an entire json column on it's own.
316
+
317
+ `AttrJson::Model` can supply a simple coder for the [ActiveRecord serialization](https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html)
318
+ feature to easily do that.
319
+
320
+ ```ruby
321
+ class MyModel
322
+ include AttrJson::Model
323
+
324
+ attr_json :some_string, :string
325
+ attr_json :some_int, :int
326
+ end
327
+
328
+ class MyTable < ApplicationRecord
329
+ serialize :some_json_column, MyModel.to_serialization_coder
330
+ end
331
+
332
+ MyTable.create(some_json_column: MyModel.new(some_string: "string"))
333
+
334
+ # will cast from hash for you
335
+ MyTable.create(some_json_column: { some_int: 12 })
336
+
337
+ # etc
338
+ ```
339
+
340
+ To avoid errors raised at inconvenient times, we recommend you set these settings to make 'bad'
341
+ data turn into `nil`, consistent with most ActiveRecord types:
342
+
343
+ ```ruby
344
+ class MyModel
345
+ include AttrJson::Model
346
+
347
+ attr_json_config(bad_cast: :as_nil, unknown_key: :strip)
348
+ # ...
349
+ end
350
+ ```
351
+
352
+ And/or define a setter method to cast, and raise early on data problems:
353
+
354
+ ```ruby
355
+ class MyTable < ApplicationRecord
356
+ serialize :some_json_column, MyModel.to_serialization_coder
357
+
358
+ def some_json_column=(val)
359
+ super( )
360
+ end
361
+ end
362
+ ```
363
+
364
+ Serializing a model to an entire json column is a relatively recent feature, please let us know how it's working for you.
365
+
366
+ <a name="arbitrary-json-data"></a>
367
+ ## Storing Arbitrary JSON data
368
+
369
+ 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.
370
+
371
+ ```ruby
372
+ class MyModel < ActiveRecord::Base
373
+ include AttrJson::Record
374
+
375
+ attr_json :arbitrary_hash, ActiveModel::Type::Value.new
376
+ end
377
+
378
+ ```
379
+
288
380
 
289
381
  <a name="forms"></a>
290
382
  ## Forms and Form Builders
@@ -293,7 +385,7 @@ Use with Rails form builders is supported pretty painlessly. Including with [sim
293
385
 
294
386
  If you have nested AttrJson::Models you'd like to use in your forms much like Rails associated records: Where you would use Rails `accepts_nested_attributes_for`, instead `include AttrJson::NestedAttributes` and use `attr_json_accepts_nested_attributes_for`. Multiple levels of nesting are supported.
295
387
 
296
- To get simple_form to properly detect your attribute types, define your attributes with `rails_attribute: true`.
388
+ To get simple_form to properly detect your attribute types, define your attributes with `rails_attribute: true`. You can default rails_attribute to true with `attr_json_config(default_rails_attribute: true)`
297
389
 
298
390
  For more info, see doc page on [Use with Forms and Form Builders](doc_src/forms.md).
299
391
 
@@ -383,16 +475,16 @@ to prevent overwriting other updates from processes.
383
475
 
384
476
  ## State of Code, and To Be Done
385
477
 
386
- This is a pre-1.0 work in progress. But the functionality that is here seems pretty solid.
478
+ The functionality that is here seems pretty solid, and is being used by jrochkind in a production app.
387
479
 
388
- Backwards incompatible changes are possible before 1.0. Once I tag something 1.0, I'm pretty serious about minimizing backwards incompats.
389
-
390
- I do not yet use this myself in production, and may not for a while. I generally am reluctant to release something as 1.0 with implied suitable for production when I'm not yet using it in production myself, but may with enough feedback. A couple others are already using in production.
480
+ 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).
391
481
 
392
482
  Feedback of any kind of _very welcome_, please feel free to use the issue tracker.
393
483
 
394
484
  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.
395
485
 
486
+ 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.
487
+
396
488
  ### Possible future features:
397
489
 
398
490
  * 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.
@@ -450,3 +542,5 @@ There is a `./bin/console` that will give you a console in the context of attr_j
450
542
  * Didn't actually notice existing [json_attributes](https://github.com/joel/json_attributes)
451
543
  until I was well on my way here. I think it's not updated for Rails5 or type-aware,
452
544
  haven't looked at it too much.
545
+
546
+ * [store_model](https://github.com/DmitryTsepelev/store_model) was created after `attr_json`, and has some overlapping functionality.
@@ -45,10 +45,10 @@ attributes use as much of the existing ActiveRecord architecture as we can.}
45
45
  # Only to get CI to work on versions of Rails other than we release with,
46
46
  # should never release a gem with RAILS_GEM set!
47
47
  unless ENV['APPRAISAL_INITIALIZED'] || ENV["TRAVIS"]
48
- spec.add_runtime_dependency "activerecord", ">= 5.0.0", "< 5.3"
48
+ spec.add_runtime_dependency "activerecord", ">= 5.0.0", "< 6.1"
49
49
  end
50
50
 
51
- spec.add_development_dependency "bundler", "~> 1.14"
51
+ spec.add_development_dependency "bundler"
52
52
  spec.add_development_dependency "rake", ">= 10.0"
53
53
  spec.add_development_dependency "rspec", "~> 3.7"
54
54
  spec.add_development_dependency "database_cleaner", "~> 1.5"
@@ -6,12 +6,12 @@ gem "combustion", "~> 0.9.0"
6
6
  gem "rails", "~> 5.0.0"
7
7
  gem "railties"
8
8
  gem "pg", "~> 0.18"
9
- gem "rspec-rails", "~> 3.7"
9
+ gem "rspec-rails", "~> 4.0"
10
10
  gem "simple_form", ">= 4.0"
11
11
  gem "cocoon", ">= 1.2"
12
12
  gem "jquery-rails"
13
13
  gem "capybara", "~> 3.0"
14
- gem "chromedriver-helper"
14
+ gem "webdrivers", "~> 4.0"
15
15
  gem "selenium-webdriver"
16
16
  gem "byebug"
17
17
  gem "rails-ujs", require: false
@@ -6,12 +6,12 @@ gem "combustion", "~> 0.9.0"
6
6
  gem "rails", "~> 5.1.0"
7
7
  gem "railties"
8
8
  gem "pg", "~> 1.0"
9
- gem "rspec-rails", "~> 3.7"
9
+ gem "rspec-rails", "~> 4.0"
10
10
  gem "simple_form", ">= 4.0"
11
11
  gem "cocoon", ">= 1.2"
12
12
  gem "jquery-rails"
13
13
  gem "capybara", "~> 3.0"
14
- gem "chromedriver-helper"
14
+ gem "webdrivers", "~> 4.0"
15
15
  gem "selenium-webdriver"
16
16
  gem "byebug"
17
17
 
@@ -6,12 +6,12 @@ gem "combustion", "~> 0.9.0"
6
6
  gem "rails", "~> 5.2.0"
7
7
  gem "railties"
8
8
  gem "pg", "~> 1.0"
9
- gem "rspec-rails", "~> 3.7"
9
+ gem "rspec-rails", "~> 4.0"
10
10
  gem "simple_form", ">= 4.0"
11
11
  gem "cocoon", ">= 1.2"
12
12
  gem "jquery-rails"
13
13
  gem "capybara", "~> 3.0"
14
- gem "chromedriver-helper"
14
+ gem "webdrivers", "~> 4.0"
15
15
  gem "selenium-webdriver"
16
16
  gem "byebug"
17
17
 
@@ -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", ">= 6.0.0.beta1", "< 6.1"
7
+ gem "railties"
8
+ gem "pg", "~> 1.0"
9
+ gem "rspec-rails", "~> 4.0"
10
+ gem "simple_form", ">= 4.0"
11
+ gem "cocoon", ">= 1.2"
12
+ gem "jquery-rails"
13
+ gem "capybara", "~> 3.0"
14
+ gem "webdrivers", "~> 4.0"
15
+ gem "selenium-webdriver"
16
+ gem "byebug"
17
+
18
+ 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", git: "https://github.com/rails/rails.git", branch: "master"
7
+ gem "railties"
8
+ gem "pg", "~> 1.0"
9
+ gem "rspec-rails", "~> 3.7"
10
+ gem "simple_form", ">= 4.0"
11
+ gem "cocoon", ">= 1.2"
12
+ gem "jquery-rails"
13
+ gem "capybara", "~> 3.0"
14
+ gem "webdrivers", "~> 4.0"
15
+ gem "selenium-webdriver"
16
+ gem "byebug"
17
+ gem "coffee-rails"
18
+
19
+ gemspec path: "../"
@@ -2,16 +2,16 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "combustion", git: "https://github.com/pat/combustion.git"
5
+ gem "combustion", "~> 1.0"
6
6
  gem "rails", git: "https://github.com/rails/rails.git", branch: "master"
7
7
  gem "railties"
8
8
  gem "pg", "~> 1.0"
9
- gem "rspec-rails", "~> 3.7"
9
+ gem "rspec-rails", "~> 4.0"
10
10
  gem "simple_form", ">= 4.0"
11
11
  gem "cocoon", ">= 1.2"
12
12
  gem "jquery-rails"
13
13
  gem "capybara", "~> 3.0"
14
- gem "chromedriver-helper"
14
+ gem "webdrivers", "~> 4.0"
15
15
  gem "selenium-webdriver"
16
16
  gem "byebug"
17
17
  gem "coffee-rails"
@@ -1,7 +1,6 @@
1
1
  require "attr_json/version"
2
2
 
3
3
  require "active_record"
4
- require "active_record/connection_adapters/postgresql_adapter"
5
4
 
6
5
  require 'attr_json/config'
7
6
  require 'attr_json/record'
@@ -78,6 +78,12 @@
78
78
  @default != NO_DEFAULT_PROVIDED
79
79
  end
80
80
 
81
+ # Can be value or proc!
82
+ def default_argument
83
+ return nil unless has_default?
84
+ @default
85
+ end
86
+
81
87
  def provide_default!
82
88
  unless has_default?
83
89
  raise ArgumentError.new("This #{self.class.name} does not have a default defined!")
@@ -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
@@ -3,10 +3,20 @@ module AttrJson
3
3
  # and rails class_attribute. Instead, you set to new Config object
4
4
  # changed with {#merge}.
5
5
  class Config
6
- RECORD_ALLOWED_KEYS = %i{default_container_attribute default_accepts_nested_attributes}
7
- MODEL_ALLOWED_KEYS = %i{unknown_key}
6
+ RECORD_ALLOWED_KEYS = %i{
7
+ default_container_attribute
8
+ default_rails_attribute
9
+ default_accepts_nested_attributes
10
+ }
11
+
12
+ MODEL_ALLOWED_KEYS = %i{
13
+ unknown_key
14
+ bad_cast
15
+ }
16
+
8
17
  DEFAULTS = {
9
18
  default_container_attribute: "json_attributes",
19
+ default_rails_attribute: false,
10
20
  unknown_key: :raise
11
21
  }
12
22
 
@@ -7,6 +7,8 @@ require 'attr_json/attribute_definition/registry'
7
7
  require 'attr_json/type/model'
8
8
  require 'attr_json/model/cocoon_compat'
9
9
 
10
+ require 'attr_json/serialization_coder_from_type'
11
+
10
12
  module AttrJson
11
13
 
12
14
  # Meant for use in a plain class, turns it into an ActiveModel::Model
@@ -30,9 +32,39 @@ module AttrJson
30
32
  #
31
33
  # class Something
32
34
  # include AttrJson::Model
33
- # attr_json_config(unknown_key: :ignore)
35
+ # attr_json_config(unknown_key: :allow)
36
+ # #...
37
+ # end
38
+ #
39
+ # Similarly, trying to set a Model-valued attribute with an object that
40
+ # can't be cast to a Hash or Model at all will normally raise a
41
+ # AttrJson::Type::Model::BadCast error, but you can set config `bad_cast: :as_nil`
42
+ # to make it cast to nil, more like typical ActiveRecord cast.
43
+ #
44
+ # class Something
45
+ # include AttrJson::Model
46
+ # attr_json_config(bad_cast: :as_nil)
34
47
  # #...
35
48
  # end
49
+ #
50
+ # ## ActiveRecord `serialize`
51
+ #
52
+ # If you want to map a single AttrJson::Model to a json/jsonb column, you
53
+ # can use ActiveRecord `serialize` feature.
54
+ #
55
+ # https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html
56
+ #
57
+ # We provide a simple shim to give you the right API for a "coder" for AR serialize:
58
+ #
59
+ # class ValueModel
60
+ # include AttrJson::Model
61
+ # attr_json :some_string, :string
62
+ # end
63
+ #
64
+ # class SomeModel < ApplicationRecord
65
+ # serialize :some_json_column, ValueModel.to_serialize_coder
66
+ # end
67
+ #
36
68
  module Model
37
69
  extend ActiveSupport::Concern
38
70
 
@@ -88,6 +120,10 @@ module AttrJson
88
120
  @type ||= AttrJson::Type::Model.new(self)
89
121
  end
90
122
 
123
+ def to_serialization_coder
124
+ @serialization_coder ||= AttrJson::SerializationCoderFromType.new(to_type)
125
+ end
126
+
91
127
  # Type can be an instance of an ActiveModel::Type::Value subclass, or a symbol that will
92
128
  # be looked up in `ActiveModel::Type.lookup`
93
129
  #
@@ -240,12 +276,9 @@ module AttrJson
240
276
  end
241
277
 
242
278
  # Two AttrJson::Model objects are equal if they are the same class
243
- # or one is a subclass of the other, AND their #attributes are equal.
244
- # TODO: Should we allow subclasses to be equal, or should they have to be the
245
- # exact same class?
279
+ # AND their #attributes are equal.
246
280
  def ==(other_object)
247
- (other_object.is_a?(self.class) || self.is_a?(other_object.class)) &&
248
- other_object.attributes == self.attributes
281
+ other_object.class == self.class && other_object.attributes == self.attributes
249
282
  end
250
283
 
251
284
  # ActiveRecord objects [have a](https://github.com/rails/rails/blob/v5.1.5/activerecord/lib/active_record/nested_attributes.rb#L367-L374)
@@ -26,6 +26,37 @@ module AttrJson
26
26
  self.attr_json_registry = AttrJson::AttributeDefinition::Registry.new
27
27
  end
28
28
 
29
+ protected
30
+
31
+ # adapted from ActiveRecord query_attribute method
32
+ # https://github.com/rails/rails/blob/v5.2.3/activerecord/lib/active_record/attribute_methods/query.rb#L12
33
+ #
34
+ # Sadly we could not re-use Rails code here, becuase the built-in method assumes attribute
35
+ # can be obtained with `self[attr_name]`, which you can not with attr_json (is that bad?), as
36
+ # well as `self.class.columns_hash[attr_name]` which you definitely can not (which is probably not bad),
37
+ # and has no way to use the value-translation semantics independently of that. May be a problem if
38
+ # ActiveRecord changes it's query method semantics in the future, will have to be sync'd here.
39
+ #
40
+ # Used to implement query methods on attr_json attributes, like `attr_json :foo, :string`, method `#foo?`
41
+ def self.attr_json_query_method(record, attribute)
42
+ value = record.send(attribute)
43
+
44
+ case value
45
+ when true
46
+ true
47
+ when false, nil, ActiveModel::Type::Boolean::FALSE_VALUES
48
+ false
49
+ else
50
+ if value.respond_to?(:to_i) && ( Numeric === value || value.to_s !~ /[^0-9]/ )
51
+ !value.to_i.zero?
52
+ elsif value.respond_to?(:zero?)
53
+ !value.zero?
54
+ else
55
+ !value.blank?
56
+ end
57
+ end
58
+ end
59
+
29
60
  class_methods do
30
61
  # Access or set class-wide json_attribute_config. Inherited by sub-classes,
31
62
  # but setting on sub-classes is unique to subclass. Similar to how
@@ -88,10 +119,11 @@ module AttrJson
88
119
  # @option options [Boolean] :rails_attribute (false) Create an actual ActiveRecord
89
120
  # `attribute` for name param. A Rails attribute isn't needed for our functionality,
90
121
  # but registering thusly will let the type be picked up by simple_form and
91
- # other tools that may look for it via Rails attribute APIs.
122
+ # other tools that may look for it via Rails attribute APIs. Default can be changed
123
+ # with `attr_json_config(default_rails_attribute: true)`
92
124
  def attr_json(name, type, **options)
93
125
  options = {
94
- rails_attribute: false,
126
+ rails_attribute: self.attr_json_config.default_rails_attribute,
95
127
  validate: true,
96
128
  container_attribute: self.attr_json_config.default_container_attribute,
97
129
  accepts_nested_attributes: self.attr_json_config.default_accepts_nested_attributes
@@ -129,34 +161,46 @@ module AttrJson
129
161
  # We don't actually use this for anything, we provide our own covers. But registering
130
162
  # it with usual system will let simple_form and maybe others find it.
131
163
  if options[:rails_attribute]
132
- self.attribute name.to_sym, self.attr_json_registry.fetch(name).type
164
+ attr_json_definition = attr_json_registry[name]
165
+
166
+ attribute_args = attr_json_definition.has_default? ? { default: attr_json_definition.default_argument } : {}
167
+ self.attribute name.to_sym, attr_json_definition.type, **attribute_args
168
+
169
+ # Ensure that rails attributes tracker knows about value we just fetched
170
+ # for this particular attribute. Yes, we are registering an after_find for each
171
+ # attr_json registered with rails_attribute:true, using the `name` from above under closure. .
172
+ after_find do
173
+ value = public_send(name)
174
+ if value && has_attribute?(name.to_sym)
175
+ write_attribute(name.to_sym, value)
176
+ self.send(:clear_attribute_changes, [name.to_sym])
177
+ end
178
+ end
133
179
  end
134
180
 
135
181
  _attr_jsons_module.module_eval do
182
+ # For getter and setter, we used to use read_store_attribute/write_store_attribute
183
+ # copied from Rails store_accessor implementation.
184
+ # https://github.com/rails/rails/blob/74c3e43fba458b9b863d27f0c45fd2d8dc603cbc/activerecord/lib/active_record/store.rb#L90-L96
185
+ #
186
+ # But in fact just getting/setting in the hash provided to us by ActiveRecord json type
187
+ # container works BETTER for dirty tracking. We had a test that only passed doing it
188
+ # this simple way.
189
+
136
190
  define_method("#{name}=") do |value|
191
+ super(value) if defined?(super)
137
192
  attribute_def = self.class.attr_json_registry.fetch(name.to_sym)
138
- # write_store_attribute copied from Rails store_accessor implementation.
139
- # https://github.com/rails/rails/blob/74c3e43fba458b9b863d27f0c45fd2d8dc603cbc/activerecord/lib/active_record/store.rb#L90-L96
140
-
141
- # special handling for nil, sorry, because if name key was previously
142
- # not present, write_store_attribute by default will decide there was
143
- # no change and refuse to make the change. TODO messy.
144
- if value.nil? && !public_send(attribute_def.container_attribute).has_key?(attribute_def.store_key)
145
- public_send :"#{attribute_def.container_attribute}_will_change!"
146
- public_send(attribute_def.container_attribute)[attribute_def.store_key] = nil
147
- else
148
- # use of `write_store_attribute` is copied from Rails store_accessor implementation.
149
- # https://github.com/rails/rails/blob/74c3e43fba458b9b863d27f0c45fd2d8dc603cbc/activerecord/lib/active_record/store.rb#L90-L96
150
- write_store_attribute(attribute_def.container_attribute, attribute_def.store_key, attribute_def.cast(value))
151
- end
193
+ public_send(attribute_def.container_attribute)[attribute_def.store_key] = attribute_def.cast(value)
152
194
  end
153
195
 
154
196
  define_method("#{name}") do
155
197
  attribute_def = self.class.attr_json_registry.fetch(name.to_sym)
198
+ public_send(attribute_def.container_attribute)[attribute_def.store_key]
199
+ end
156
200
 
157
- # use of `read_store_attribute` is copied from Rails store_accessor implementation.
158
- # https://github.com/rails/rails/blob/74c3e43fba458b9b863d27f0c45fd2d8dc603cbc/activerecord/lib/active_record/store.rb#L90-L96
159
- read_store_attribute(attribute_def.container_attribute, attribute_def.store_key)
201
+ define_method("#{name}?") do
202
+ # implementation of `query_store_attribute` is based on Rails `query_attribute` implementation
203
+ AttrJson::Record.attr_json_query_method(self, name)
160
204
  end
161
205
  end
162
206
 
@@ -10,6 +10,20 @@ module AttrJson
10
10
  end
11
11
 
12
12
  def contains_relation
13
+ contains_relation_impl do |relation, query, params|
14
+ relation.where(query, params)
15
+ end
16
+ end
17
+
18
+ def contains_not_relation
19
+ contains_relation_impl do |relation, query, params|
20
+ relation.where.not(query, params)
21
+ end
22
+ end
23
+
24
+ protected
25
+
26
+ def contains_relation_impl
13
27
  result_relation = relation
14
28
 
15
29
  group_attributes_by_container.each do |container_attribute, attributes|
@@ -18,14 +32,12 @@ module AttrJson
18
32
  attributes.each do |key, value|
19
33
  add_to_param_hash!(param_hash, key, value)
20
34
  end
21
- result_relation = result_relation.where("#{relation.table_name}.#{container_attribute} @> (?)::jsonb", param_hash.to_json)
35
+ result_relation = yield(result_relation, "#{relation.table_name}.#{container_attribute} @> (?)::jsonb", param_hash.to_json)
22
36
  end
23
37
 
24
38
  result_relation
25
39
  end
26
40
 
27
- protected
28
-
29
41
  def merge_param_hash!(original, new)
30
42
  original.deep_merge!(new) do |key, old_val, new_val|
31
43
  if old_val.is_a?(Array) && old_val.first.is_a?(Hash) && new_val.is_a?(Array) && new_val.first.is_a?(Hash)
@@ -17,6 +17,8 @@ module AttrJson
17
17
  #
18
18
  # some_model.jsonb_contains(a_string: "foo").first
19
19
  #
20
+ # some_model.not_jsonb_contains(a_string: "bar").first
21
+ #
20
22
  # See more in {file:README} docs.
21
23
  module QueryScopes
22
24
  extend ActiveSupport::Concern
@@ -29,6 +31,10 @@ module AttrJson
29
31
  scope(:jsonb_contains, lambda do |attributes|
30
32
  QueryBuilder.new(self, attributes).contains_relation
31
33
  end)
34
+
35
+ scope(:not_jsonb_contains, lambda do |attributes|
36
+ QueryBuilder.new(self, attributes).contains_not_relation
37
+ end)
32
38
  end
33
39
  end
34
40
  end
@@ -0,0 +1,40 @@
1
+ module AttrJson
2
+
3
+ # A little wrapper to provide an object that provides #dump and #load method for use
4
+ # as a coder second-argument for [ActiveRecord Serialization](https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html),
5
+ # that simply delegates to #serialize and #deserialize from a ActiveModel::Type object.
6
+ #
7
+ # Created to be used with an AttrJson::Model type (AttrJson::Type::Model), but hypothetically
8
+ # could be a shim from anything with serialize/deserialize to dump/load instead.
9
+ #
10
+ # class ValueModel
11
+ # include AttrJson::Model
12
+ # attr_json :some_string, :string
13
+ # end
14
+ #
15
+ # class SomeModel < ApplicationRecord
16
+ # serialize :some_json_column, ValueModel.to_serialize_coder
17
+ # end
18
+ #
19
+ # Note when used with an AttrJson::Model, it will dump/load from a HASH, not a
20
+ # string. It assumes it's writing to a Json(b) column that wants/provides hashes,
21
+ # not strings.
22
+ class SerializationCoderFromType
23
+ attr_reader :type
24
+ def initialize(type)
25
+ @type = type
26
+ end
27
+
28
+ # Dump and load methods to support ActiveRecord Serialization
29
+ # too.
30
+ def dump(value)
31
+ type.serialize(value)
32
+ end
33
+
34
+ # Dump and load methods to support ActiveRecord Serialization
35
+ # too. https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html
36
+ def load(value)
37
+ type.deserialize(value)
38
+ end
39
+ end
40
+ end
@@ -7,6 +7,7 @@ module AttrJson
7
7
  # You create one with AttrJson::Model::Type.new(attr_json_model_class),
8
8
  # but normally that's only done in AttrJson::Model.to_type, there isn't
9
9
  # an anticipated need to create from any other place.
10
+ #
10
11
  class Model < ::ActiveModel::Type::Value
11
12
  class BadCast < ArgumentError ; end
12
13
 
@@ -35,11 +36,13 @@ module AttrJson
35
36
  elsif v.respond_to?(:to_h)
36
37
  # TODO Maybe we ought not to do this on #to_h?
37
38
  model.new_from_serializable(v.to_h)
39
+ elsif model.attr_json_config.bad_cast == :as_nil
40
+ # This was originally default behavior, to be like existing ActiveRecord
41
+ # which kind of silently does this for non-castable basic values. That
42
+ # ended up being confusing in the basic case, so now we raise by default,
43
+ # but this is still configurable.
44
+ nil
38
45
  else
39
- # Bad input. Originally we were trying to return nil, to be like
40
- # existing ActiveRecord which kind of silently does a basic value
41
- # with null input. But that ended up making things confusing, let's
42
- # just raise.
43
46
  raise BadCast.new("Can not cast from #{v.inspect} to #{self.type}")
44
47
  end
45
48
  end
@@ -50,7 +53,7 @@ module AttrJson
50
53
  elsif v.kind_of?(model)
51
54
  v.serializable_hash
52
55
  else
53
- cast(v).serializable_hash
56
+ (cast_v = cast(v)) && cast_v.serializable_hash
54
57
  end
55
58
  end
56
59
 
@@ -51,6 +51,8 @@ module AttrJson
51
51
  # MyRecord.jsonb_contains(author: { name: "foo", type: "Corporation"})
52
52
  # MyRecord.jsonb_contains(author: Corporation.new(name: "foo"))
53
53
  #
54
+ # Additionally, there is not_jsonb_contains, which creates the same query terms like jsonb_contains, but negated.
55
+ #
54
56
  class PolymorphicModel < ActiveModel::Type::Value
55
57
  class TypeError < ::TypeError ; end
56
58
 
@@ -1,3 +1,3 @@
1
1
  module AttrJson
2
- VERSION = "0.5.0"
2
+ VERSION = "1.2.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: attr_json
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Rochkind
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-01-02 00:00:00.000000000 Z
11
+ date: 2020-06-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -19,7 +19,7 @@ dependencies:
19
19
  version: 5.0.0
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '5.3'
22
+ version: '6.1'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -29,21 +29,21 @@ dependencies:
29
29
  version: 5.0.0
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '5.3'
32
+ version: '6.1'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: bundler
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - "~>"
37
+ - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: '1.14'
39
+ version: '0'
40
40
  type: :development
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - "~>"
44
+ - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: '1.14'
46
+ version: '0'
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: rake
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -133,6 +133,7 @@ files:
133
133
  - ".travis.yml"
134
134
  - ".yardopts"
135
135
  - Appraisals
136
+ - CHANGELOG.md
136
137
  - Gemfile
137
138
  - LICENSE.txt
138
139
  - README.md
@@ -149,6 +150,8 @@ files:
149
150
  - gemfiles/rails_5_0.gemfile
150
151
  - gemfiles/rails_5_1.gemfile
151
152
  - gemfiles/rails_5_2.gemfile
153
+ - gemfiles/rails_6_0.gemfile
154
+ - gemfiles/rails_edge.gemfile
152
155
  - gemfiles/rails_edge_6.gemfile
153
156
  - lib/attr_json.rb
154
157
  - lib/attr_json/attribute_definition.rb
@@ -164,6 +167,7 @@ files:
164
167
  - lib/attr_json/record/dirty.rb
165
168
  - lib/attr_json/record/query_builder.rb
166
169
  - lib/attr_json/record/query_scopes.rb
170
+ - lib/attr_json/serialization_coder_from_type.rb
167
171
  - lib/attr_json/type/array.rb
168
172
  - lib/attr_json/type/container_attribute.rb
169
173
  - lib/attr_json/type/model.rb
@@ -176,7 +180,7 @@ licenses:
176
180
  metadata:
177
181
  homepage_uri: https://github.com/jrochkind/attr_json
178
182
  source_code_uri: https://github.com/jrochkind/attr_json
179
- post_install_message:
183
+ post_install_message:
180
184
  rdoc_options: []
181
185
  require_paths:
182
186
  - lib
@@ -191,9 +195,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
191
195
  - !ruby/object:Gem::Version
192
196
  version: '0'
193
197
  requirements: []
194
- rubyforge_project:
195
- rubygems_version: 2.7.6
196
- signing_key:
198
+ rubygems_version: 3.0.3
199
+ signing_key:
197
200
  specification_version: 4
198
201
  summary: ActiveRecord attributes stored serialized in a json column, super smooth.
199
202
  test_files: []