jsonb_accessor 0.4.0.beta → 1.0.0.beta

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
  SHA1:
3
- metadata.gz: 6eb9689dbf752a8e5759e0224838eab520fe7ced
4
- data.tar.gz: 07132e8465d69d01909de02f544c5db202ff6e81
3
+ metadata.gz: c764a056e39fc6979212aac29a31384cb694a38b
4
+ data.tar.gz: 495df5a38ac759f3b07d718f818bddfbcb9449ec
5
5
  SHA512:
6
- metadata.gz: 8e5131ee5b8d0c15ad6bb563261867678485a576622a2e6ddff8e710acbcee8ea15ca9a2a3eedf73495ad17600b9fe0e02347ef55c1efb671141cb57ec4fedb2
7
- data.tar.gz: c7d9d78356a2d4f081c078bc843282da3918d6502153c6030cf1e85bc373f45e63cc6c6eb089520bcd331de9aba71af8038afe393d3a82b8b85767faba0cf5f0
6
+ metadata.gz: 8716e5c48b3122677518e20230d3d70c59b42eb9434ed4ebe8992f4ef9b797a4b9e2b2566960ed34a3a6cbf77baa785f0fe0853f9bf40198d9214360ced4bf8f
7
+ data.tar.gz: 90389c14a8b8aa3b27da14301897e5ee836c713456ed562421ef989d6b3c88407d211ff22cc79fd76be318735660bb9129e4ce6ccd9bd7b17bbe7444f346b2b8
data/.rubocop.yml CHANGED
@@ -23,6 +23,8 @@ Metrics/MethodLength:
23
23
  Enabled: false
24
24
  Metrics/ModuleLength:
25
25
  Enabled: false
26
+ Metrics/BlockLength:
27
+ Enabled: false
26
28
  Metrics/PerceivedComplexity:
27
29
  Enabled: false
28
30
  Style/AlignParameters:
data/.travis.yml CHANGED
@@ -1,7 +1,7 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.2.4
4
- - 2.3.0
3
+ - 2.2.5
4
+ - 2.3.1
5
5
  addons:
6
6
  postgresql: '9.4'
7
7
  before_install:
@@ -13,5 +13,4 @@ before_script:
13
13
  - bundle exec rake db:migrate
14
14
  cache: bundler
15
15
  gemfile:
16
- - gemfiles/activerecord_4.2.4.gemfile
17
16
  - gemfiles/activerecord_5.0.0.gemfile
data/Appraisals CHANGED
@@ -1,8 +1,3 @@
1
- appraise "activerecord-4.2.4" do
2
- gemspec
3
- gem "activerecord", "4.2.4"
4
- end
5
-
6
1
  appraise "activerecord-5.0.0" do
7
2
  gemspec
8
3
  gem "activerecord", "5.0.0"
data/README.md CHANGED
@@ -1,19 +1,15 @@
1
1
  # JSONb Accessor
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/jsonb_accessor.svg)](http://badge.fury.io/rb/jsonb_accessor) [![Build Status](https://travis-ci.org/devmynd/jsonb_accessor.svg)](https://travis-ci.org/devmynd/jsonb_accessor)
3
+ [![Gem Version](https://badge.fury.io/rb/jsonb_accessor.svg)](http://badge.fury.io/rb/jsonb_accessor) [![Build Status](https://travis-ci.org/devmynd/jsonb_accessor.svg)](https://travis-ci.org/devmynd/jsonb_accessor)
4
4
 
5
5
  Adds typed `jsonb` backed fields as first class citizens to your `ActiveRecord` models. This gem is similar in spirit to [HstoreAccessor](https://github.com/devmynd/hstore_accessor), but the `jsonb` column in PostgreSQL has a few distinct advantages, mostly around nested documents and support for collections.
6
6
 
7
7
  ## Table of Contents
8
8
 
9
9
  * [Installation](#installation)
10
- * [Rails 5](#rails-5)
11
10
  * [Usage](#usage)
12
- * [ActiveRecord Methods Generated for Fields](#activerecord-methods-generated-for-fields)
13
11
  * [Validations](#validations)
14
12
  * [Single-Table Inheritance](#single-table-inheritance)
15
- * [Scopes](#scopes)
16
- * [Migrations](#migrations)
17
13
  * [Dependencies](#dependencies)
18
14
  * [Development](#development)
19
15
  * [Contributing](#contributing)
@@ -30,10 +26,6 @@ And then execute:
30
26
 
31
27
  $ bundle install
32
28
 
33
- ## Rails 5
34
-
35
- Version 0.4.X will run on Rails 5, but behavior around type coercion for array and other collection field types behaves differently. When you upgrade to 0.4.X make sure you do not depend on subtle type coercion rules.
36
-
37
29
  ## Usage
38
30
 
39
31
  First we must create a model which has a `jsonb` column available to store data into it:
@@ -54,82 +46,14 @@ We can then declare the `jsonb` fields we wish to expose via the accessor:
54
46
  class Product < ActiveRecord::Base
55
47
  jsonb_accessor(
56
48
  :options,
57
- :count, # => value type
58
49
  title: :string,
59
50
  id_value: :value,
60
51
  external_id: :integer,
61
- reviewed_at: :date_time
52
+ reviewed_at: :datetime
62
53
  )
63
54
  end
64
55
  ```
65
56
 
66
- JSONb Accessor accepts both untyped and typed key definitions. Untyped keys are treated as-is and no additional casting is performed. This allows the freedom of dynamic values alongside the power types, which is especially convenient when saving nested form attributes. Typed keys will be cast to their respective values using the same mechanism ActiveRecord uses to coerce standard attribute columns. It's as close to a real column as you can get and the goal is to keep it that way.
67
-
68
- All untyped keys must be defined prior to typed columns. You can declare a typed column with type `value` for explicit dynamic behavior. For reference, the `jsonb_accessor` macro is defined thusly.
69
-
70
- ```ruby
71
- def jsonb_accessor(jsonb_attribute, *value_fields, **typed_fields)
72
- ...
73
- end
74
- ```
75
-
76
- There's quite a bit more to do do and document but we're excited to get this out there while we work on it some more.
77
-
78
- ## ActiveRecord Methods Generated for Fields
79
-
80
- ```ruby
81
- class Product < ActiveRecord::Base
82
- jsonb_accessor :data, field: :string
83
- end
84
- ```
85
-
86
- * `field`
87
- * `field=`
88
- * `field?`
89
- * `field_changed?`
90
- * `field_was`
91
- * `field_change`
92
- * `reset_field!`
93
- * `restore_field!`
94
- * `field_will_change!`
95
-
96
- ### Supported Types
97
-
98
- Because the underlying storage mechanism is JSON, we attempt to abide by the limitations of what can be represented natively. We use [ActiveRecord::Type](https://github.com/rails/rails/blob/master/activerecord/lib/active_record/type.rb) for seralization, but any type defined in the [Postgres connection adapter](https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb) will also be accepted. Beware of the impact of using complex Postgres column types such as inet, enum, hstore, etc... We plan to restrict which types are allowed in a future patch.
99
-
100
- The following types are explicitly supported.
101
-
102
- * big_integer
103
- * binary
104
- * boolean
105
- * date
106
- * date_time
107
- * decimal
108
- * float
109
- * integer
110
- * string
111
- * text
112
- * time
113
- * value
114
-
115
-
116
- Typed arrays are also supported by specifying `:type_array` (i.e. `:float_array`). `:array` is interpreted as an array of `value` types.
117
-
118
- Support for nested types is also available but experimental at this point. If you must, you may try something like this for nested objects.
119
-
120
- ```ruby
121
- class Product < ActiveRecord::Base
122
- jsonb_accessor(
123
- :options,
124
- nested_object: { key: :integer }
125
- )
126
- end
127
-
128
- p = Product.new
129
- p.nested_object.key = "10"
130
- puts p.nested_object.key #=> 10
131
- ```
132
-
133
57
  ## Validations
134
58
 
135
59
  Because this gem promotes attributes nested into the JSON column to first level attributes, most validations should just work. We still have to add some testing and support around this feature but feel free to try and leave us feedback if they're not working as expected.
@@ -208,7 +132,7 @@ end
208
132
 
209
133
  #### `<jsonb_field>_contains`
210
134
 
211
- **Description:** returns all records that contain matching attributes in the specified `jsonb` field.
135
+ **Description:** returns all records that contain matching attributes in the specified `jsonb` field.
212
136
 
213
137
  ```ruby
214
138
  product_1 = Product.create!(name: "foo", approved: true, reviewed_at: 3.days.ago)
@@ -218,143 +142,13 @@ product_3 = Product.create!(name: "foo", approved: false)
218
142
  Product.data_contains(name: "foo", approved: true) # => [product_1]
219
143
  ```
220
144
 
221
- **Note:** when including an array attribute, the stored array and the array used for the query do not need to match exactly. For example, when queried with `[1, 2]`, records that have arrays of `[2, 1, 3]` will be returned.
222
-
223
- #### `with_<jsonb_defined_field>`
224
-
225
- **Description:** returns all records with the given value in the field. This is defined for all `jsonb_accessor` defined fields. It's a convenience method that allows you to do `Product.with_name("foo")` instead of `Product.data_contains(name: "foo")`.
226
-
227
- ```ruby
228
- product_1 = Product.create!(name: "foo")
229
- product_2 = Product.create!(name: "bar")
230
-
231
- Product.with_name("foo") # => [product_1]
232
- ```
233
-
234
- **Note:** when including an array attribute, the stored array and the array used for the query do not need to match exactly. For example, when queried with `[1, 2]`, records that have arrays of `[2, 1, 3]` will be returned.
235
-
236
- ### Integer, Big Integer, Decimal, and Float Scopes
237
-
238
- #### `<jsonb_defined_field>_gt`
239
-
240
- **Description:** returns all records with a value that is greater than the argument.
241
-
242
- ```ruby
243
- product_1 = Product.create!(price: 10)
244
- product_2 = Product.create!(price: 11)
245
-
246
- Product.price_gt(10) # => [product_2]
247
- ```
248
-
249
- #### `<jsonb_defined_field>_gte`
250
-
251
- **Description:** returns all records with a value that is greater than or equal to the argument.
252
-
253
- ```ruby
254
- product_1 = Product.create!(price: 10)
255
- product_2 = Product.create!(price: 11)
256
- product_3 = Product.create!(price: 9)
257
-
258
- Product.price_gte(10) # => [product_1, product_2]
259
- ```
260
-
261
- #### `<jsonb_defined_field>_lt`
262
-
263
- **Description:** returns all records with a value that is less than the argument.
264
-
265
- ```ruby
266
- product_1 = Product.create!(price: 10)
267
- product_2 = Product.create!(price: 11)
268
-
269
- Product.price_lt(11) # => [product_1]
270
- ```
271
-
272
- #### `<jsonb_defined_field>_lte`
273
-
274
- **Description:** returns all records with a value that is less than or equal to the argument.
275
-
276
- ```ruby
277
- product_1 = Product.create!(price: 10)
278
- product_2 = Product.create!(price: 11)
279
- product_3 = Product.create!(price: 12)
280
-
281
- Product.price_lte(11) # => [product_1, product_2]
282
- ```
283
-
284
-
285
- ### Boolean Scopes
286
-
287
- #### `is_<jsonb_defined_field>`
288
-
289
- **Description:** returns all records where the value is `true`.
290
-
291
- ```ruby
292
- product_1 = Product.create!(approved: true)
293
- product_2 = Product.create!(approved: false)
294
-
295
- Product.is_approved # => [product_1]
296
- ```
297
-
298
- #### `not_<jsonb_defined_field>`
299
-
300
- **Description:** returns all records where the value is `false`.
301
-
302
- ```ruby
303
- product_1 = Product.create!(approved: true)
304
- product_2 = Product.create!(approved: false)
305
-
306
- Product.not_approved # => [product_2]
307
- ```
308
-
309
- ### Date, DateTime Scopes
310
-
311
- #### `<jsonb_defined_field>_before`
312
-
313
- **Description:** returns all records where the value is before the argument. Also supports JSON string arguments.
314
-
315
- ```ruby
316
- product_1 = Product.create!(reviewed_at: 3.days.ago)
317
- product_2 = Product.create!(reviewed_at: 5.days.ago)
318
-
319
- Product.reviewed_at_before(4.days.ago) # => [product_2]
320
- Product.reviewed_at_before(4.days.ago.to_json) # => [product_2]
321
- ```
322
-
323
- #### `<jsonb_defined_field>_after`
324
-
325
- **Description:** returns all records where the value is after the argument. Also supports JSON string arguments.
326
-
327
- ```ruby
328
- product_1 = Product.create!(reviewed_at: 3.days.from_now)
329
- product_2 = Product.create!(reviewed_at: 5.days.from_now)
330
-
331
- Product.reviewed_at_after(4.days.from_now) # => [product_2]
332
- Product.reviewed_at_after(4.days.from_now.to_json) # => [product_2]
333
- ```
334
-
335
- ### Array Scopes
336
-
337
- #### `<jsonb_defined_fields>_contains`
338
-
339
- **Description:** returns all records where the value is contained in the array field.
340
-
341
- ```ruby
342
- product_1 = Product.create!(previous_prices: [3])
343
- product_2 = Product.create!(previous_prices: [4, 5, 6])
344
-
345
- Product.previous_prices_contains(5) # => [product_2]
346
- ```
347
-
348
- ## Migrations
349
-
350
- Coming soon...
351
-
352
- `jsonb` supports `GIN`, `GIST`, `btree` and `hash` indexes over `json` column. We have plans to add migrations helpers for generating these indexes for you.
145
+ **Note:** when including an array attribute, the stored array and the array used for the query do not need to match exactly. For example, when queried with `[1, 2]`, records that have arrays of `[2, 1, 3]` will be returned.
353
146
 
354
147
  ## Dependencies
355
148
 
356
- - ActiveRecord 4.2
357
- - Postgres 9.4 (in order to use the [jsonb column type](http://www.postgresql.org/docs/9.4/static/datatype-json.html)).
149
+ - ActiveRecord 5.0
150
+ - Ruby >= 2.2.2
151
+ - Postgres >= 9.4 (in order to use the [jsonb column type](http://www.postgresql.org/docs/9.4/static/datatype-json.html)).
358
152
 
359
153
  ## Development
360
154
 
data/Rakefile CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "rubygems"
2
3
  require "bundler/setup"
3
4
  require "bundler/gem_tasks"
data/bin/console CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
  # rubocop:disable Lint/UselessAssignment
3
4
 
4
5
  require "bundler/setup"
@@ -6,11 +7,8 @@ require "jsonb_accessor"
6
7
  require "rspec"
7
8
  require File.expand_path("../../spec/spec_helper.rb", __FILE__)
8
9
 
9
- ActiveRecord::Base.establish_connection(
10
- adapter: "postgresql",
11
- database: "jsonb_accessor",
12
- username: "postgres"
13
- )
10
+ dbconfig = YAML.load(File.open("db/config.yml"))
11
+ ActiveRecord::Base.establish_connection(dbconfig["development"])
14
12
 
15
13
  x = Product.new
16
14
 
data/db/schema.rb CHANGED
@@ -1,4 +1,3 @@
1
- # encoding: UTF-8
2
1
  # This file is auto-generated from the current state of the database. Instead
3
2
  # of editing this file, please use the migrations feature of Active Record to
4
3
  # incrementally modify your database, and then regenerate this schema definition.
@@ -1,8 +1,8 @@
1
1
  PATH
2
2
  remote: ../
3
3
  specs:
4
- jsonb_accessor (0.4.0.beta)
5
- activerecord (>= 4.2.1)
4
+ jsonb_accessor (1.0.0.beta)
5
+ activerecord (>= 5.0.0)
6
6
  pg (>= 0.18.1)
7
7
 
8
8
  GEM
@@ -38,8 +38,6 @@ GEM
38
38
  thor (>= 0.14.0)
39
39
  arel (7.1.4)
40
40
  ast (2.3.0)
41
- astrolabe (1.3.1)
42
- parser (~> 2.2)
43
41
  awesome_print (1.7.0)
44
42
  builder (3.2.2)
45
43
  coderay (1.1.1)
@@ -84,25 +82,25 @@ GEM
84
82
  thor (>= 0.18.1, < 2.0)
85
83
  rainbow (2.1.0)
86
84
  rake (10.5.0)
87
- rspec (3.2.0)
88
- rspec-core (~> 3.2.0)
89
- rspec-expectations (~> 3.2.0)
90
- rspec-mocks (~> 3.2.0)
91
- rspec-core (3.2.3)
92
- rspec-support (~> 3.2.0)
93
- rspec-expectations (3.2.1)
85
+ rspec (3.5.0)
86
+ rspec-core (~> 3.5.0)
87
+ rspec-expectations (~> 3.5.0)
88
+ rspec-mocks (~> 3.5.0)
89
+ rspec-core (3.5.4)
90
+ rspec-support (~> 3.5.0)
91
+ rspec-expectations (3.5.0)
94
92
  diff-lcs (>= 1.2.0, < 2.0)
95
- rspec-support (~> 3.2.0)
96
- rspec-mocks (3.2.1)
93
+ rspec-support (~> 3.5.0)
94
+ rspec-mocks (3.5.0)
97
95
  diff-lcs (>= 1.2.0, < 2.0)
98
- rspec-support (~> 3.2.0)
99
- rspec-support (3.2.2)
100
- rubocop (0.31.0)
101
- astrolabe (~> 1.3)
102
- parser (>= 2.2.2.1, < 3.0)
96
+ rspec-support (~> 3.5.0)
97
+ rspec-support (3.5.0)
98
+ rubocop (0.44.1)
99
+ parser (>= 2.3.1.1, < 3.0)
103
100
  powerpack (~> 0.1)
104
101
  rainbow (>= 1.99.1, < 3.0)
105
- ruby-progressbar (~> 1.4)
102
+ ruby-progressbar (~> 1.7)
103
+ unicode-display_width (~> 1.0, >= 1.0.1)
106
104
  ruby-progressbar (1.8.1)
107
105
  shoulda-matchers (3.1.1)
108
106
  activesupport (>= 4.0.0)
@@ -115,13 +113,13 @@ GEM
115
113
  thread_safe (0.3.5)
116
114
  tzinfo (1.2.2)
117
115
  thread_safe (~> 0.1)
116
+ unicode-display_width (1.1.1)
118
117
  yard (0.9.5)
119
118
 
120
119
  PLATFORMS
121
120
  ruby
122
121
 
123
122
  DEPENDENCIES
124
- actionpack
125
123
  activerecord (= 5.0.0)
126
124
  appraisal
127
125
  awesome_print
@@ -133,10 +131,10 @@ DEPENDENCIES
133
131
  pry-doc
134
132
  pry-nav
135
133
  rake (~> 10.0)
136
- rspec (~> 3.2.0)
137
- rubocop (= 0.31.0)
134
+ rspec (~> 3.2)
135
+ rubocop (~> 0.31)
138
136
  shoulda-matchers
139
137
  standalone_migrations
140
138
 
141
139
  BUNDLED WITH
142
- 1.11.2
140
+ 1.13.5
@@ -20,10 +20,9 @@ Gem::Specification.new do |spec|
20
20
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
21
  spec.require_paths = ["lib"]
22
22
 
23
- spec.add_dependency "activerecord", ">= 4.2.1"
23
+ spec.add_dependency "activerecord", ">= 5.0.0"
24
24
  spec.add_dependency "pg", ">= 0.18.1"
25
25
 
26
- spec.add_development_dependency "actionpack"
27
26
  spec.add_development_dependency "appraisal"
28
27
  spec.add_development_dependency "bundler", "~> 1.9"
29
28
  spec.add_development_dependency "database_cleaner"
@@ -32,8 +31,8 @@ Gem::Specification.new do |spec|
32
31
  spec.add_development_dependency "pry-doc"
33
32
  spec.add_development_dependency "pry-nav"
34
33
  spec.add_development_dependency "rake", "~> 10.0"
35
- spec.add_development_dependency "rspec", "~> 3.2.0"
36
- spec.add_development_dependency "rubocop", "0.31.0"
34
+ spec.add_development_dependency "rspec", "~> 3.2"
35
+ spec.add_development_dependency "rubocop", "~> 0.31"
37
36
  spec.add_development_dependency "shoulda-matchers"
38
37
  spec.add_development_dependency "standalone_migrations"
39
38
  end
@@ -1,174 +1,62 @@
1
+ # frozen_string_literal: true
1
2
  module JsonbAccessor
2
3
  module Macro
3
4
  module ClassMethods
4
- def jsonb_accessor(jsonb_attribute, *value_fields, **typed_fields)
5
- fields_map = JsonbAccessor::FieldsMap.new(value_fields, typed_fields)
6
- class_namespace = ClassBuilder.generate_class_namespace(name)
7
- attribute_namespace = ClassBuilder.generate_attribute_namespace(jsonb_attribute, class_namespace)
8
- nested_classes = ClassBuilder.generate_nested_classes(attribute_namespace, fields_map.nested_fields)
9
- jsonb_attribute_initialization_method_name = "initialize_jsonb_attrs_for_#{jsonb_attribute}"
10
- jsonb_attribute_scope_name = "#{jsonb_attribute}_contains"
11
-
12
- singleton_class.send(:define_method, "#{jsonb_attribute}_classes") do
13
- nested_classes
14
- end
15
-
16
- delegate "#{jsonb_attribute}_classes", to: :class
17
-
18
- _initialize_jsonb_attrs(jsonb_attribute, fields_map, jsonb_attribute_initialization_method_name)
19
- _create_jsonb_attribute_scope_name(jsonb_attribute, jsonb_attribute_scope_name)
20
- _create_jsonb_scopes(jsonb_attribute, fields_map, jsonb_attribute_scope_name)
21
- _create_jsonb_accessor_methods(jsonb_attribute, jsonb_attribute_initialization_method_name, fields_map)
22
-
23
- _register_jsonb_classes_for_cleanup
24
- end
25
-
26
- private
27
-
28
- def _register_jsonb_classes_for_cleanup
29
- return unless ENV["RACK_ENV"] == "development"
30
-
31
- class_name = CLASS_PREFIX + name
32
- clean_up_proc = proc do
33
- if JsonbAccessor.constants.any? { |c| c.to_s == class_name }
34
- JsonbAccessor.send(:remove_const, class_name)
35
- end
36
- end
37
-
38
- if ActiveRecord::VERSION::MAJOR == 5 && defined?(ActiveSupport::Reloader)
39
- ActiveSupport::Reloader.to_complete(&clean_up_proc)
40
- elsif defined?(ActionDispatch)
41
- ActionDispatch::Reloader.to_cleanup(&clean_up_proc)
42
- end
43
- end
44
-
45
- def _initialize_jsonb_attrs(jsonb_attribute, fields_map, jsonb_attribute_initialization_method_name)
46
- define_method(jsonb_attribute_initialization_method_name) do
47
- if has_attribute?(jsonb_attribute)
48
- jsonb_attribute_hash = send(jsonb_attribute) || {}
49
-
50
- fields_map.typed_fields.keys.each do |field|
51
- write_attribute(field, jsonb_attribute_hash[field.to_s])
5
+ def jsonb_accessor(jsonb_attribute, field_types)
6
+ field_names = field_types.keys
7
+ names_and_store_keys = field_types.each_with_object({}) do |(name, type), mapping|
8
+ _type, options = Array(type)
9
+ mapping[name.to_s] = (options.try(:delete, :store_key) || name).to_s
10
+ end
11
+ # Defines virtual attributes for each jsonb field.
12
+ field_types.each do |name, type|
13
+ attribute name, *type
14
+ end
15
+
16
+ # Setters are in a module to allow users to override them and still be able to use `super`.
17
+ setters = Module.new do
18
+ # Overrides the setter created by `attribute` above to make sure the jsonb attribute is kept in sync.
19
+ names_and_store_keys.each do |name, store_key|
20
+ define_method("#{name}=") do |value|
21
+ super(value)
22
+ new_values = (public_send(jsonb_attribute) || {}).merge(store_key => public_send(name))
23
+ write_attribute(jsonb_attribute, new_values)
52
24
  end
53
-
54
- fields_map.nested_fields.keys.each do |field|
55
- send("#{field}=", jsonb_attribute_hash[field.to_s])
56
- end
57
-
58
- send(:clear_attribute_changes, fields_map.names)
59
- end
60
- end
61
-
62
- after_initialize(jsonb_attribute_initialization_method_name.to_sym)
63
- end
64
-
65
- def _create_jsonb_attribute_scope_name(jsonb_attribute, jsonb_attribute_scope_name)
66
- scope jsonb_attribute_scope_name, (lambda do |attributes|
67
- query_options = new(attributes).send(jsonb_attribute)
68
- fields = attributes.keys.map(&:to_s)
69
- query_options.delete_if { |key, value| fields.exclude?(key) }
70
- query_json = TypeHelper.type_cast_as_jsonb(query_options)
71
- where("#{table_name}.#{jsonb_attribute} @> ?", query_json)
72
- end)
73
- end
74
-
75
- def _create_jsonb_scopes(jsonb_attribute, fields_map, jsonb_attribute_scope_name)
76
- __create_jsonb_standard_scopes(fields_map, jsonb_attribute_scope_name)
77
- __create_jsonb_typed_scopes(jsonb_attribute, fields_map)
78
- end
79
-
80
- def __create_jsonb_standard_scopes(fields_map, jsonb_attribute_scope_name)
81
- fields_map.names.each do |field|
82
- scope "with_#{field}", -> (value) { send(jsonb_attribute_scope_name, field => value) }
83
- end
84
- end
85
-
86
- def __create_jsonb_typed_scopes(jsonb_attribute, fields_map)
87
- fields_map.typed_fields.each do |field, type|
88
- case type
89
- when :boolean
90
- ___create_jsonb_boolean_scopes(field)
91
- when :integer, :float, :decimal, :big_integer
92
- ___create_jsonb_numeric_scopes(field, jsonb_attribute, type)
93
- when :date_time, :date
94
- ___create_jsonb_date_time_scopes(field, jsonb_attribute, type)
95
- when /array/
96
- ___create_jsonb_array_scopes(field)
97
25
  end
98
- end
99
- end
100
-
101
- def ___create_jsonb_boolean_scopes(field)
102
- scope "is_#{field}", -> { send("with_#{field}", true) }
103
- scope "not_#{field}", -> { send("with_#{field}", false) }
104
- end
105
-
106
- def ___create_jsonb_numeric_scopes(field, jsonb_attribute, type)
107
- safe_type = type.to_s.gsub("big_", "")
108
- scope "__numeric_#{field}_comparator", -> (value, operator) { where("((#{table_name}.#{jsonb_attribute}) ->> ?)::#{safe_type} #{operator} ?", field, value) }
109
- scope "#{field}_lt", -> (value) { send("__numeric_#{field}_comparator", value, "<") }
110
- scope "#{field}_lte", -> (value) { send("__numeric_#{field}_comparator", value, "<=") }
111
- scope "#{field}_gte", -> (value) { send("__numeric_#{field}_comparator", value, ">=") }
112
- scope "#{field}_gt", -> (value) { send("__numeric_#{field}_comparator", value, ">") }
113
- end
114
-
115
- def ___create_jsonb_date_time_scopes(field, jsonb_attribute, type)
116
- scope "__date_time_#{field}_comparator", -> (value, operator) { where("((#{table_name}.#{jsonb_attribute}) ->> ?)::timestamp #{operator} ?::timestamp", field, value.to_json) }
117
- scope "#{field}_before", -> (value) { send("__date_time_#{field}_comparator", value, "<") }
118
- scope "#{field}_after", -> (value) { send("__date_time_#{field}_comparator", value, ">") }
119
- end
120
26
 
121
- def ___create_jsonb_array_scopes(field)
122
- scope "#{field}_contains", -> (value) { send("with_#{field}", [value]) }
123
- end
124
-
125
- def _create_jsonb_accessor_methods(jsonb_attribute, jsonb_attribute_initialization_method_name, fields_map)
126
- jsonb_accessor_methods = Module.new do
27
+ # Overrides the jsonb attribute setter to make sure the jsonb fields are kept in sync.
127
28
  define_method("#{jsonb_attribute}=") do |value|
128
- write_attribute(jsonb_attribute, value)
129
- send(jsonb_attribute_initialization_method_name)
130
- end
29
+ indifferent_value = value.try(:with_indifferent_access) || {}
30
+ value_with_store_keys = names_and_store_keys.each_with_object({}) do |(name, store_key), new_value|
31
+ new_value[store_key] = indifferent_value[name]
32
+ end
131
33
 
132
- define_method(:reload) do |*args, &block|
133
- super(*args, &block)
134
- send(jsonb_attribute_initialization_method_name)
135
- self
34
+ super(value_with_store_keys)
35
+
36
+ new_attributes = field_names.each_with_object({}) { |name, defaults| defaults[name] = nil }.merge(value || {})
37
+ new_attributes.each { |name, new_value| write_attribute(name, new_value) }
136
38
  end
137
39
  end
40
+ include setters
138
41
 
139
- __create_jsonb_typed_field_setters(jsonb_attribute, jsonb_accessor_methods, fields_map)
140
- __create_jsonb_nested_field_accessors(jsonb_attribute, jsonb_accessor_methods, fields_map)
141
- include jsonb_accessor_methods
142
- end
143
-
144
- def __create_jsonb_typed_field_setters(jsonb_attribute, jsonb_accessor_methods, fields_map)
145
- fields_map.typed_fields.each do |field, type|
146
- attribute(field.to_s, TypeHelper.fetch(type))
147
-
148
- jsonb_accessor_methods.instance_eval do
149
- define_method("#{field}=") do |value, *args, &block|
150
- super(value, *args, &block)
151
- new_jsonb_value = (send(jsonb_attribute) || {}).merge(field => attributes[field.to_s])
152
- write_attribute(jsonb_attribute, new_jsonb_value)
153
- end
42
+ # Makes sure new objects have the appropriate values in their jsonb fields.
43
+ after_initialize do
44
+ jsonb_values = public_send(jsonb_attribute) || {}
45
+ jsonb_values.each do |store_key, value|
46
+ name = names_and_store_keys.key(store_key)
47
+ write_attribute(name, value)
154
48
  end
49
+ clear_changes_information if persisted?
155
50
  end
156
- end
157
51
 
158
- def __create_jsonb_nested_field_accessors(jsonb_attribute, jsonb_accessor_methods, fields_map)
159
- fields_map.nested_fields.each do |field, nested_attributes|
160
- attribute(field.to_s, TypeHelper.fetch(:value))
161
- jsonb_accessor_methods.instance_eval do
162
- define_method("#{field}=") do |value|
163
- instance_class = send("#{jsonb_attribute}_classes")[field]
164
- instance = cast_nested_field_value(value, instance_class, __method__)
165
-
166
- new_jsonb_value = (send(jsonb_attribute) || {}).merge(field.to_s => instance.attributes)
167
- write_attribute(jsonb_attribute, new_jsonb_value)
168
- super(instance)
169
- end
52
+ # <jsonb_attribute>_where scope
53
+ scope("#{jsonb_attribute}_where", lambda do |attributes|
54
+ store_key_attributes = attributes.each_with_object({}) do |(name, value), new_attributes|
55
+ store_key = names_and_store_keys[name.to_s]
56
+ new_attributes[store_key] = value
170
57
  end
171
- end
58
+ jsonb_where(jsonb_attribute, store_key_attributes)
59
+ end)
172
60
  end
173
61
  end
174
62
  end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+ module JsonbAccessor
3
+ GREATER_THAN = ">"
4
+ GREATER_THAN_OR_EQUAL_TO = ">="
5
+ LESS_THAN = "<"
6
+ LESS_THAN_OR_EQUAL_TO = "<="
7
+
8
+ NUMBER_OPERATORS_MAP = {
9
+ GREATER_THAN => GREATER_THAN,
10
+ "greater_than" => GREATER_THAN,
11
+ "gt" => GREATER_THAN,
12
+ GREATER_THAN_OR_EQUAL_TO => GREATER_THAN_OR_EQUAL_TO,
13
+ "greater_than_or_equal_to" => GREATER_THAN_OR_EQUAL_TO,
14
+ "gte" => GREATER_THAN_OR_EQUAL_TO,
15
+ LESS_THAN => LESS_THAN,
16
+ "less_than" => LESS_THAN,
17
+ "lt" => LESS_THAN,
18
+ LESS_THAN_OR_EQUAL_TO => LESS_THAN_OR_EQUAL_TO,
19
+ "less_than_or_equal_to" => LESS_THAN_OR_EQUAL_TO,
20
+ "lte" => LESS_THAN_OR_EQUAL_TO
21
+ }.freeze
22
+
23
+ NUMBER_OPERATORS = NUMBER_OPERATORS_MAP.keys.freeze
24
+
25
+ TIME_OPERATORS_MAP = {
26
+ "after" => GREATER_THAN,
27
+ "before" => LESS_THAN
28
+ }.freeze
29
+
30
+ TIME_OPERATORS = TIME_OPERATORS_MAP.keys.freeze
31
+
32
+ IS_NUMBER_QUERY_ARGUMENTS = lambda do |arg|
33
+ arg.is_a?(Hash) &&
34
+ arg.keys.map(&:to_s).all? { |key| JsonbAccessor::NUMBER_OPERATORS.include?(key) }
35
+ end
36
+
37
+ IS_TIME_QUERY_ARGUMENTS = lambda do |arg|
38
+ arg.is_a?(Hash) &&
39
+ arg.keys.map(&:to_s).all? { |key| JsonbAccessor::TIME_OPERATORS.include?(key) }
40
+ end
41
+
42
+ module QueryBuilder
43
+ extend ActiveSupport::Concern
44
+
45
+ included do
46
+ scope(:jsonb_contains,
47
+ -> (column_name, attributes) { where("#{table_name}.#{column_name} @> (?)::jsonb", attributes.to_json) })
48
+
49
+ scope(:jsonb_number_query, lambda do |column_name, field_name, given_operator, value|
50
+ operator = JsonbAccessor::NUMBER_OPERATORS_MAP.fetch(given_operator.to_s)
51
+ where("(#{table_name}.#{column_name} ->> ?)::float #{operator} ?", field_name, value)
52
+ end)
53
+
54
+ scope(:jsonb_time_query, lambda do |column_name, field_name, given_operator, value|
55
+ operator = JsonbAccessor::TIME_OPERATORS_MAP.fetch(given_operator.to_s)
56
+ where("(#{table_name}.#{column_name} ->> ?)::timestamp #{operator} ?", field_name, value)
57
+ end)
58
+
59
+ scope(:jsonb_where, lambda do |column_name, attributes|
60
+ query = all
61
+ contains_attributes = {}
62
+
63
+ attributes.each do |name, value|
64
+ case value
65
+ when IS_NUMBER_QUERY_ARGUMENTS
66
+ value.each { |operator, query_value| query = query.jsonb_number_query(column_name, name, operator, query_value) }
67
+ when IS_TIME_QUERY_ARGUMENTS
68
+ value.each { |operator, query_value| query = query.jsonb_time_query(column_name, name, operator, query_value) }
69
+ else
70
+ contains_attributes[name] = value
71
+ end
72
+ end
73
+
74
+ query.jsonb_contains(column_name, contains_attributes)
75
+ end)
76
+ end
77
+ end
78
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module JsonbAccessor
2
- VERSION = "0.4.0.beta"
3
+ VERSION = "1.0.0.beta"
3
4
  end
@@ -1,14 +1,11 @@
1
+ # frozen_string_literal: true
1
2
  require "active_record"
2
3
 
3
4
  require "active_record/connection_adapters/postgresql_adapter"
4
5
 
5
6
  require "jsonb_accessor/version"
6
- require "jsonb_accessor/fields_map"
7
- require "jsonb_accessor/helpers"
8
- require "jsonb_accessor/type_helper"
9
- require "jsonb_accessor/nested_base"
10
- require "jsonb_accessor/class_builder"
11
7
  require "jsonb_accessor/macro"
8
+ require "jsonb_accessor/query_builder"
12
9
 
13
10
  module JsonbAccessor
14
11
  extend ActiveSupport::Concern
@@ -17,5 +14,5 @@ end
17
14
 
18
15
  ActiveSupport.on_load(:active_record) do
19
16
  ActiveRecord::Base.send(:include, JsonbAccessor)
20
- ActiveRecord::Base.send(:include, JsonbAccessor::Helpers)
17
+ ActiveRecord::Base.send(:include, JsonbAccessor::QueryBuilder)
21
18
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jsonb_accessor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0.beta
4
+ version: 1.0.0.beta
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Crismali
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2016-10-17 00:00:00.000000000 Z
13
+ date: 2016-10-18 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -18,14 +18,14 @@ dependencies:
18
18
  requirements:
19
19
  - - ">="
20
20
  - !ruby/object:Gem::Version
21
- version: 4.2.1
21
+ version: 5.0.0
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
25
25
  requirements:
26
26
  - - ">="
27
27
  - !ruby/object:Gem::Version
28
- version: 4.2.1
28
+ version: 5.0.0
29
29
  - !ruby/object:Gem::Dependency
30
30
  name: pg
31
31
  requirement: !ruby/object:Gem::Requirement
@@ -40,20 +40,6 @@ dependencies:
40
40
  - - ">="
41
41
  - !ruby/object:Gem::Version
42
42
  version: 0.18.1
43
- - !ruby/object:Gem::Dependency
44
- name: actionpack
45
- requirement: !ruby/object:Gem::Requirement
46
- requirements:
47
- - - ">="
48
- - !ruby/object:Gem::Version
49
- version: '0'
50
- type: :development
51
- prerelease: false
52
- version_requirements: !ruby/object:Gem::Requirement
53
- requirements:
54
- - - ">="
55
- - !ruby/object:Gem::Version
56
- version: '0'
57
43
  - !ruby/object:Gem::Dependency
58
44
  name: appraisal
59
45
  requirement: !ruby/object:Gem::Requirement
@@ -172,28 +158,28 @@ dependencies:
172
158
  requirements:
173
159
  - - "~>"
174
160
  - !ruby/object:Gem::Version
175
- version: 3.2.0
161
+ version: '3.2'
176
162
  type: :development
177
163
  prerelease: false
178
164
  version_requirements: !ruby/object:Gem::Requirement
179
165
  requirements:
180
166
  - - "~>"
181
167
  - !ruby/object:Gem::Version
182
- version: 3.2.0
168
+ version: '3.2'
183
169
  - !ruby/object:Gem::Dependency
184
170
  name: rubocop
185
171
  requirement: !ruby/object:Gem::Requirement
186
172
  requirements:
187
- - - '='
173
+ - - "~>"
188
174
  - !ruby/object:Gem::Version
189
- version: 0.31.0
175
+ version: '0.31'
190
176
  type: :development
191
177
  prerelease: false
192
178
  version_requirements: !ruby/object:Gem::Requirement
193
179
  requirements:
194
- - - '='
180
+ - - "~>"
195
181
  - !ruby/object:Gem::Version
196
- version: 0.31.0
182
+ version: '0.31'
197
183
  - !ruby/object:Gem::Dependency
198
184
  name: shoulda-matchers
199
185
  requirement: !ruby/object:Gem::Requirement
@@ -247,18 +233,12 @@ files:
247
233
  - db/config.yml
248
234
  - db/migrate/20150407031737_set_up_testing_db.rb
249
235
  - db/schema.rb
250
- - gemfiles/activerecord_4.2.4.gemfile
251
- - gemfiles/activerecord_4.2.4.gemfile.lock
252
236
  - gemfiles/activerecord_5.0.0.gemfile
253
237
  - gemfiles/activerecord_5.0.0.gemfile.lock
254
238
  - jsonb_accessor.gemspec
255
239
  - lib/jsonb_accessor.rb
256
- - lib/jsonb_accessor/class_builder.rb
257
- - lib/jsonb_accessor/fields_map.rb
258
- - lib/jsonb_accessor/helpers.rb
259
240
  - lib/jsonb_accessor/macro.rb
260
- - lib/jsonb_accessor/nested_base.rb
261
- - lib/jsonb_accessor/type_helper.rb
241
+ - lib/jsonb_accessor/query_builder.rb
262
242
  - lib/jsonb_accessor/version.rb
263
243
  homepage: https://github.com/devmynd/jsonb_accessor
264
244
  licenses:
@@ -1,8 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "pg"
6
- gem "activerecord", "4.2.4"
7
-
8
- gemspec :path => "../"
@@ -1,146 +0,0 @@
1
- PATH
2
- remote: ../
3
- specs:
4
- jsonb_accessor (0.4.0.beta)
5
- activerecord (>= 4.2.1)
6
- pg (>= 0.18.1)
7
-
8
- GEM
9
- remote: https://rubygems.org/
10
- specs:
11
- actionpack (4.2.4)
12
- actionview (= 4.2.4)
13
- activesupport (= 4.2.4)
14
- rack (~> 1.6)
15
- rack-test (~> 0.6.2)
16
- rails-dom-testing (~> 1.0, >= 1.0.5)
17
- rails-html-sanitizer (~> 1.0, >= 1.0.2)
18
- actionview (4.2.4)
19
- activesupport (= 4.2.4)
20
- builder (~> 3.1)
21
- erubis (~> 2.7.0)
22
- rails-dom-testing (~> 1.0, >= 1.0.5)
23
- rails-html-sanitizer (~> 1.0, >= 1.0.2)
24
- activemodel (4.2.4)
25
- activesupport (= 4.2.4)
26
- builder (~> 3.1)
27
- activerecord (4.2.4)
28
- activemodel (= 4.2.4)
29
- activesupport (= 4.2.4)
30
- arel (~> 6.0)
31
- activesupport (4.2.4)
32
- i18n (~> 0.7)
33
- json (~> 1.7, >= 1.7.7)
34
- minitest (~> 5.1)
35
- thread_safe (~> 0.3, >= 0.3.4)
36
- tzinfo (~> 1.1)
37
- appraisal (2.1.0)
38
- bundler
39
- rake
40
- thor (>= 0.14.0)
41
- arel (6.0.3)
42
- ast (2.1.0)
43
- astrolabe (1.3.1)
44
- parser (~> 2.2)
45
- awesome_print (1.6.1)
46
- builder (3.2.2)
47
- coderay (1.1.0)
48
- database_cleaner (1.5.1)
49
- diff-lcs (1.2.5)
50
- erubis (2.7.0)
51
- i18n (0.7.0)
52
- json (1.8.3)
53
- loofah (2.0.3)
54
- nokogiri (>= 1.5.9)
55
- method_source (0.8.2)
56
- mini_portile (0.6.2)
57
- minitest (5.8.1)
58
- nokogiri (1.6.6.2)
59
- mini_portile (~> 0.6.0)
60
- parser (2.2.3.0)
61
- ast (>= 1.1, < 3.0)
62
- pg (0.18.4)
63
- powerpack (0.1.1)
64
- pry (0.10.3)
65
- coderay (~> 1.1.0)
66
- method_source (~> 0.8.1)
67
- slop (~> 3.4)
68
- pry-doc (0.8.0)
69
- pry (~> 0.9)
70
- yard (~> 0.8)
71
- pry-nav (0.2.4)
72
- pry (>= 0.9.10, < 0.11.0)
73
- rack (1.6.4)
74
- rack-test (0.6.3)
75
- rack (>= 1.0)
76
- rails-deprecated_sanitizer (1.0.3)
77
- activesupport (>= 4.2.0.alpha)
78
- rails-dom-testing (1.0.7)
79
- activesupport (>= 4.2.0.beta, < 5.0)
80
- nokogiri (~> 1.6.0)
81
- rails-deprecated_sanitizer (>= 1.0.1)
82
- rails-html-sanitizer (1.0.2)
83
- loofah (~> 2.0)
84
- railties (4.2.4)
85
- actionpack (= 4.2.4)
86
- activesupport (= 4.2.4)
87
- rake (>= 0.8.7)
88
- thor (>= 0.18.1, < 2.0)
89
- rainbow (2.0.0)
90
- rake (10.4.2)
91
- rspec (3.2.0)
92
- rspec-core (~> 3.2.0)
93
- rspec-expectations (~> 3.2.0)
94
- rspec-mocks (~> 3.2.0)
95
- rspec-core (3.2.3)
96
- rspec-support (~> 3.2.0)
97
- rspec-expectations (3.2.1)
98
- diff-lcs (>= 1.2.0, < 2.0)
99
- rspec-support (~> 3.2.0)
100
- rspec-mocks (3.2.1)
101
- diff-lcs (>= 1.2.0, < 2.0)
102
- rspec-support (~> 3.2.0)
103
- rspec-support (3.2.2)
104
- rubocop (0.31.0)
105
- astrolabe (~> 1.3)
106
- parser (>= 2.2.2.1, < 3.0)
107
- powerpack (~> 0.1)
108
- rainbow (>= 1.99.1, < 3.0)
109
- ruby-progressbar (~> 1.4)
110
- ruby-progressbar (1.7.5)
111
- shoulda-matchers (3.0.0)
112
- activesupport (>= 4.0.0)
113
- slop (3.6.0)
114
- standalone_migrations (4.0.2)
115
- activerecord (~> 4.2.0)
116
- railties (~> 4.2.0)
117
- rake (~> 10.0)
118
- thor (0.19.1)
119
- thread_safe (0.3.5)
120
- tzinfo (1.2.2)
121
- thread_safe (~> 0.1)
122
- yard (0.8.7.6)
123
-
124
- PLATFORMS
125
- ruby
126
-
127
- DEPENDENCIES
128
- actionpack
129
- activerecord (= 4.2.4)
130
- appraisal
131
- awesome_print
132
- bundler (~> 1.9)
133
- database_cleaner
134
- jsonb_accessor!
135
- pg
136
- pry
137
- pry-doc
138
- pry-nav
139
- rake (~> 10.0)
140
- rspec (~> 3.2.0)
141
- rubocop (= 0.31.0)
142
- shoulda-matchers
143
- standalone_migrations
144
-
145
- BUNDLED WITH
146
- 1.11.2
@@ -1,101 +0,0 @@
1
- module JsonbAccessor
2
- UnknownValue = Class.new(StandardError)
3
- CLASS_PREFIX = "JA"
4
-
5
- module ClassBuilder
6
- class << self
7
- def generate_class(namespace, new_class_name, attribute_definitions)
8
- fields_map = JsonbAccessor::FieldsMap.new([], attribute_definitions)
9
- klass = generate_new_class(new_class_name, fields_map, namespace)
10
- nested_classes = generate_nested_classes(klass, fields_map.nested_fields)
11
-
12
- define_class_methods(klass, nested_classes, new_class_name)
13
- define_attributes_and_data_types(klass, fields_map)
14
- define_typed_accessors(klass, fields_map)
15
- define_nested_accessors(klass, fields_map)
16
-
17
- klass
18
- end
19
-
20
- def generate_nested_classes(klass, nested_attributes)
21
- nested_attributes.each_with_object({}) do |(attribute_name, nested_attrs), nested_classes|
22
- nested_classes[attribute_name] = generate_class(klass, attribute_name, nested_attrs)
23
- end
24
- end
25
-
26
- def generate_class_namespace(name)
27
- class_name = CLASS_PREFIX + name.gsub(CONSTANT_SEPARATOR, "")
28
- if JsonbAccessor.constants.any? { |c| c.to_s == class_name }
29
- class_namespace = JsonbAccessor.const_get(class_name)
30
- else
31
- class_namespace = Module.new
32
- JsonbAccessor.const_set(class_name, class_namespace)
33
- end
34
- class_namespace
35
- end
36
-
37
- def generate_attribute_namespace(attribute_name, class_namespace)
38
- attribute_namespace = Module.new
39
- name = generate_constant_name(attribute_name)
40
- class_namespace.const_set(name, attribute_namespace)
41
- end
42
-
43
- private
44
-
45
- def define_nested_accessors(klass, fields_map)
46
- klass.class_eval do
47
- fields_map.nested_fields.keys.each do |attribute_name|
48
- attr_reader attribute_name
49
-
50
- define_method("#{attribute_name}=") do |value|
51
- instance_class = nested_classes[attribute_name]
52
- instance = cast_nested_field_value(value, instance_class, __method__)
53
-
54
- instance_variable_set("@#{attribute_name}", instance)
55
- attributes[attribute_name] = instance.attributes
56
- update_parent
57
- end
58
- end
59
- end
60
- end
61
-
62
- def define_typed_accessors(klass, fields_map)
63
- klass.class_eval do
64
- fields_map.typed_fields.keys.each do |attribute_name|
65
- define_method(attribute_name) { attributes[attribute_name] }
66
-
67
- cast_method_name = ActiveRecord::VERSION::MAJOR == 5 ? :cast : :type_cast_from_user
68
- define_method("#{attribute_name}=") do |value|
69
- cast_value = attributes_and_data_types[attribute_name].public_send(cast_method_name, value)
70
- attributes[attribute_name] = cast_value
71
- update_parent
72
- end
73
- end
74
- end
75
- end
76
-
77
- def define_attributes_and_data_types(klass, fields_map)
78
- klass.send(:define_method, :attributes_and_data_types) do
79
- @attributes_and_data_types ||= fields_map.typed_fields.each_with_object({}) do |(name, type), attrs_and_data_types|
80
- attrs_and_data_types[name] = TypeHelper.fetch(type)
81
- end
82
- end
83
- end
84
-
85
- def define_class_methods(klass, nested_classes, attribute_name)
86
- klass.singleton_class.send(:define_method, :nested_classes) { nested_classes }
87
- klass.singleton_class.send(:define_method, :attribute_on_parent_name) { attribute_name }
88
- end
89
-
90
- def generate_new_class(new_class_name, fields_map, namespace)
91
- klass = Class.new(NestedBase)
92
- new_class_name_camelized = generate_constant_name(new_class_name)
93
- namespace.const_set(new_class_name_camelized, klass)
94
- end
95
-
96
- def generate_constant_name(attribute_name)
97
- "#{CLASS_PREFIX}#{attribute_name.to_s.camelize}"
98
- end
99
- end
100
- end
101
- end
@@ -1,32 +0,0 @@
1
- module JsonbAccessor
2
- class FieldsMap
3
- attr_accessor :nested_fields, :typed_fields
4
-
5
- def initialize(value_fields, typed_and_nested_fields)
6
- grouped_fields = extract_typed_and_nested_fields(typed_and_nested_fields)
7
- nested_fields, typed_fields = grouped_fields.values_at(:nested, :typed)
8
-
9
- self.typed_fields = implicitly_typed_fields(value_fields).merge(typed_fields)
10
- self.nested_fields = nested_fields
11
- end
12
-
13
- def names
14
- typed_fields.keys + nested_fields.keys
15
- end
16
-
17
- private
18
-
19
- def implicitly_typed_fields(value_fields)
20
- value_fields.each_with_object({}) do |field_name, implicitly_typed_fields|
21
- implicitly_typed_fields[field_name] = :value
22
- end
23
- end
24
-
25
- def extract_typed_and_nested_fields(typed_and_nested_fields)
26
- typed_and_nested_fields.each_with_object(nested: {}, typed: {}) do |(attribute_name, type_or_nested), grouped_attributes|
27
- group = type_or_nested.is_a?(Hash) ? grouped_attributes[:nested] : grouped_attributes[:typed]
28
- group[attribute_name] = type_or_nested
29
- end
30
- end
31
- end
32
- end
@@ -1,19 +0,0 @@
1
- module JsonbAccessor
2
- module Helpers
3
- def cast_nested_field_value(value, klass, method_name)
4
- case value
5
- when klass
6
- instance = klass.new(value.attributes)
7
- when Hash
8
- instance = klass.new(value)
9
- when nil
10
- instance = klass.new
11
- else
12
- raise UnknownValue, "unable to set value '#{value}' is not a hash, `nil`, or an instance of #{klass} in #{method_name}"
13
- end
14
-
15
- instance.parent = self
16
- instance
17
- end
18
- end
19
- end
@@ -1,35 +0,0 @@
1
- module JsonbAccessor
2
- class NestedBase
3
- include Helpers
4
-
5
- attr_accessor :attributes, :parent
6
- alias_method :to_h, :attributes
7
-
8
- delegate :[], to: :attributes
9
- delegate :nested_classes, :attribute_on_parent_name, to: :class
10
-
11
- def initialize(attributes = {})
12
- self.attributes = {}.with_indifferent_access
13
-
14
- nested_classes.keys.each do |key|
15
- send("#{key}=", nil)
16
- end
17
-
18
- attributes.each do |name, value|
19
- send("#{name}=", value)
20
- end
21
- end
22
-
23
- def update_parent
24
- parent.send("#{attribute_on_parent_name}=", self) if parent
25
- end
26
-
27
- def []=(key, value)
28
- send("#{key}=", value)
29
- end
30
-
31
- def ==(suspect)
32
- self.class == suspect.class && attributes == suspect.attributes
33
- end
34
- end
35
- end
@@ -1,68 +0,0 @@
1
- module JsonbAccessor
2
- CONSTANT_SEPARATOR = "::"
3
- module TypeHelper
4
- ARRAY_MATCHER = /_array\z/
5
- UnknownType = Class.new(StandardError)
6
-
7
- class << self
8
- def fetch(type)
9
- case type
10
- when :array
11
- new_array(value)
12
- when ARRAY_MATCHER
13
- fetch_active_record_array_type(type)
14
- when :value
15
- value
16
- else
17
- fetch_active_record_type(type)
18
- end
19
- end
20
-
21
- def type_cast_as_jsonb(suspect)
22
- user_cast_method_name, db_case_method_name = ActiveRecord::VERSION::MAJOR == 5 ? [:cast, :serialize] : [:type_cast_from_user, :type_cast_for_database]
23
- type_cast_hash = jsonb.public_send(user_cast_method_name, suspect)
24
- jsonb.public_send(db_case_method_name, type_cast_hash)
25
- end
26
-
27
- private
28
-
29
- def jsonb
30
- @jsonb ||= fetch(:jsonb)
31
- end
32
-
33
- def fetch_active_record_array_type(type)
34
- subtype = type.to_s.sub(ARRAY_MATCHER, "")
35
- new_array(fetch_active_record_type(subtype))
36
- end
37
-
38
- def fetch_active_record_type(type)
39
- class_name = type.to_s.camelize
40
- klass = value_descendants.find do |ar_type|
41
- ar_type.to_s.split(CONSTANT_SEPARATOR).last == class_name
42
- end
43
-
44
- if klass
45
- klass.new
46
- else
47
- raise JsonbAccessor::TypeHelper::UnknownType
48
- end
49
- end
50
-
51
- def value_descendants
52
- grouped_types = ActiveRecord::Type::Value.descendants.group_by do |ar_type|
53
- !!ar_type.to_s.match(ActiveRecord::ConnectionAdapters::PostgreSQL::OID.to_s)
54
- end
55
-
56
- grouped_types[true] + grouped_types[false]
57
- end
58
-
59
- def value
60
- ActiveRecord::Type::Value.new
61
- end
62
-
63
- def new_array(subtype)
64
- ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(subtype)
65
- end
66
- end
67
- end
68
- end