jsonb_accessor 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9aa4a83990520b8f0190a339f1973ead1de118c2
4
- data.tar.gz: e12959b405e89178e7bbacbad07948c822db635e
3
+ metadata.gz: b3280838fd8b45ffea5fd3892240799bbb6785f7
4
+ data.tar.gz: 561e28860a5d43e25afc95ef79473480d2cc368e
5
5
  SHA512:
6
- metadata.gz: c3a2da61536078ceef74ce35eb35f9645443389304710cb801fbbb49ff6aa534b738e89c4d29454361ff8916dbe0ac71b600ee697ee9b43c5f1c2fb84bb24d81
7
- data.tar.gz: b8e45b17608b058ecc56c721b32785097422503f24aeddd0dc44243a0d041f6fa55ad812c34f862eea1b38a7f4ca373a37768e82d6615ea15eb9430f0798ca3a
6
+ metadata.gz: 080851d0cc5ea02c5d443a65473d97fd588de21c24011bbb65ab1ec7a96bdbc74b9b44298234d322dc942f8adf7ee775fb0d2fdb336cdb59455ec3f6d9a72e52
7
+ data.tar.gz: 1430c66ba53b9a01ce052043fbb49e75a1f90233f27de85a77e619eaa04e397853aeb7c82a5f2f1e8a0cf5f2ebe1f859e6680b9fc7161b160389c3b0366986b5
data/.rubocop.yml CHANGED
@@ -20,6 +20,8 @@ Metrics/LineLength:
20
20
  Enabled: false
21
21
  Metrics/MethodLength:
22
22
  Enabled: false
23
+ Metrics/ModuleLength:
24
+ Enabled: false
23
25
  Metrics/PerceivedComplexity:
24
26
  Enabled: false
25
27
  Style/AlignParameters:
data/Appraisals CHANGED
@@ -1,3 +1,7 @@
1
1
  appraise "activerecord-4.2.1" do
2
2
  gem "activerecord", "4.2.1"
3
3
  end
4
+
5
+ appraise "activerecord-4.2.2" do
6
+ gem "activerecord", "4.2.2"
7
+ end
data/README.md CHANGED
@@ -1,10 +1,21 @@
1
1
  # JSONb Accessor
2
2
 
3
- [![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
- **This gem is under heavy development. Please use cautiously and help us with feedback by opening issues for defects and feature requests. The current API is subject to change.**
7
+ ## Table of Contents
8
+
9
+ * [Installation](#installation)
10
+ * [Usage](#usage)
11
+ * [ActiveRecord Methods Generated for Fields](#activerecord-methods-generated-for-fields)
12
+ * [Validations](#validations)
13
+ * [Single-Table Inheritance](#single-table-inheritance)
14
+ * [Scopes](#scopes)
15
+ * [Migrations](#migrations)
16
+ * [Dependencies](#dependencies)
17
+ * [Development](#development)
18
+ * [Contributing](#contributing)
8
19
 
9
20
  ## Installation
10
21
 
@@ -42,7 +53,7 @@ class Product < ActiveRecord::Base
42
53
  title: :string,
43
54
  id_value: :value,
44
55
  external_id: :integer,
45
- reviewed_at: :datetime
56
+ reviewed_at: :date_time
46
57
  )
47
58
  end
48
59
  ```
@@ -79,28 +90,40 @@ end
79
90
 
80
91
  ### Supported Types
81
92
 
82
- The following types are supported, including typed collections:
83
-
84
- ```
85
- :array,
86
- :boolean,
87
- :boolean_array,
88
- :date,
89
- :date_array,
90
- :datetime,
91
- :datetime_array,
92
- :decimal,
93
- :decimal_array,
94
- :float,
95
- :float_array,
96
- :integer,
97
- :integer_array,
98
- :string,
99
- :string_array,
100
- :time,
101
- :time_array,
102
- :value
103
- ```
93
+ The following types are supported:
94
+
95
+ * big_integer
96
+ * binary
97
+ * bit
98
+ * bit_varying
99
+ * boolean
100
+ * bytea
101
+ * cidr
102
+ * date
103
+ * date_time
104
+ * decimal
105
+ * decimal_without_scale
106
+ * enum
107
+ * float
108
+ * hstore
109
+ * inet
110
+ * integer
111
+ * json
112
+ * jsonb
113
+ * money
114
+ * point
115
+ * range
116
+ * specialized_string
117
+ * string
118
+ * text
119
+ * time
120
+ * unsigned_integer
121
+ * uuid
122
+ * value
123
+ * vector
124
+ * xml
125
+
126
+ Typed arrays are also supported by specifying `:type_array` (i.e. `:float_array`). `:array` is interpreted as an array of `value` types.
104
127
 
105
128
  Support for nested types is also available but experimental at this point. If you must, you may try something like this for nested objects.
106
129
 
@@ -123,11 +146,214 @@ Because this gem promotes attributes nested into the JSON column to first level
123
146
 
124
147
  ## Single-Table Inheritance
125
148
 
126
- You can use it for STI in the same spirit as [hstore_accessor, which is documented here.](https://github.com/devmynd/hstore_accessor#single-table-inheritance).
149
+ One of the big issues with `ActiveRecord` single-table inheritance (STI)
150
+ is sparse columns. Essentially, as sub-types of the original table
151
+ diverge further from their parent more columns are left empty in a given
152
+ table. Postgres' `jsonb` type provides part of the solution in that
153
+ the values in an `jsonb` column does not impose a structure - different
154
+ rows can have different values.
155
+
156
+ We set up our table with an `jsonb` field:
157
+
158
+ ```ruby
159
+ # db/migration/<timestamp>_create_players_table.rb
160
+ class CreateVehiclesTable < ActiveRecord::Migration
161
+ def change
162
+ create_table :vehicles do |t|
163
+ t.string :make
164
+ t.string :model
165
+ t.integer :model_year
166
+ t.string :type
167
+ t.jsonb :data
168
+ end
169
+ end
170
+ end
171
+ ```
172
+
173
+ And for our models:
174
+
175
+ ```ruby
176
+ # app/models/vehicle.rb
177
+ class Vehicle < ActiveRecord::Base
178
+ end
179
+
180
+ # app/models/vehicles/automobile.rb
181
+ class Automobile < Vehicle
182
+ jsonb_accessor :data,
183
+ axle_count: :integer,
184
+ weight: :float
185
+ end
186
+
187
+ # app/models/vehicles/airplane.rb
188
+ class Airplane < Vehicle
189
+ jsonb_accessor :data,
190
+ engine_type: :string,
191
+ safety_rating: :integer
192
+ end
193
+ ```
194
+
195
+ From here any attributes specific to any sub-class can be stored in the
196
+ `jsonb` column avoiding sparse data. Indices can also be created on
197
+ individual fields in an `jsonb` column.
198
+
199
+ This approach was originally concieved by Joe Hirn in [this blog
200
+ post](http://www.devmynd.com/blog/2013-3-single-table-inheritance-hstore-lovely-combination).
127
201
 
128
202
  ## Scopes
129
203
 
130
- Coming soon...
204
+ JsonbAccessor currently supports several scopes. Let's say we have a class that looks like this:
205
+
206
+ ```ruby
207
+ class Product < ActiveRecord::Base
208
+ jsonb_accessor :data,
209
+ approved: :boolean,
210
+ name: :string,
211
+ price: :integer,
212
+ previous_prices: :integer_array,
213
+ reviewed_at: :date_time
214
+ end
215
+ ```
216
+
217
+ ### General Scopes
218
+
219
+ #### `<jsonb_field>_contains`
220
+
221
+ **Description:** returns all records that contain matching attributes in the specified `jsonb` field.
222
+
223
+ ```ruby
224
+ product_1 = Product.create!(name: "foo", approved: true, reviewed_at: 3.days.ago)
225
+ product_2 = Product.create!(name: "bar", approved: true)
226
+ product_3 = Product.create!(name: "foo", approved: false)
227
+
228
+ Product.data_contains(name: "foo", approved: true) # => [product_1]
229
+ ```
230
+
231
+ **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.
232
+
233
+ #### `with_<jsonb_defined_field>`
234
+
235
+ **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")`.
236
+
237
+ ```ruby
238
+ product_1 = Product.create!(name: "foo")
239
+ product_2 = Product.create!(name: "bar")
240
+
241
+ Product.with_name("foo") # => [product_1]
242
+ ```
243
+
244
+ **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.
245
+
246
+ ### Integer, Big Integer, Decimal, and Float Scopes
247
+
248
+ #### `<jsonb_defined_field>_gt`
249
+
250
+ **Description:** returns all records with a value that is greater than the argument.
251
+
252
+ ```ruby
253
+ product_1 = Product.create!(price: 10)
254
+ product_2 = Product.create!(price: 11)
255
+
256
+ Product.price_gt(10) # => [product_2]
257
+ ```
258
+
259
+ #### `<jsonb_defined_field>_gte`
260
+
261
+ **Description:** returns all records with a value that is greater than or equal to the argument.
262
+
263
+ ```ruby
264
+ product_1 = Product.create!(price: 10)
265
+ product_2 = Product.create!(price: 11)
266
+ product_3 = Product.create!(price: 9)
267
+
268
+ Product.price_gte(10) # => [product_1, product_2]
269
+ ```
270
+
271
+ #### `<jsonb_defined_field>_lt`
272
+
273
+ **Description:** returns all records with a value that is less than the argument.
274
+
275
+ ```ruby
276
+ product_1 = Product.create!(price: 10)
277
+ product_2 = Product.create!(price: 11)
278
+
279
+ Product.price_lt(11) # => [product_1]
280
+ ```
281
+
282
+ #### `<jsonb_defined_field>_lte`
283
+
284
+ **Description:** returns all records with a value that is less than or equal to the argument.
285
+
286
+ ```ruby
287
+ product_1 = Product.create!(price: 10)
288
+ product_2 = Product.create!(price: 11)
289
+ product_3 = Product.create!(price: 12)
290
+
291
+ Product.price_lte(11) # => [product_1, product_2]
292
+ ```
293
+
294
+
295
+ ### Boolean Scopes
296
+
297
+ #### `is_<jsonb_defined_field>`
298
+
299
+ **Description:** returns all records where the value is `true`.
300
+
301
+ ```ruby
302
+ product_1 = Product.create!(approved: true)
303
+ product_2 = Product.create!(approved: false)
304
+
305
+ Product.is_approved # => [product_1]
306
+ ```
307
+
308
+ #### `not_<jsonb_defined_field>`
309
+
310
+ **Description:** returns all records where the value is `false`.
311
+
312
+ ```ruby
313
+ product_1 = Product.create!(approved: true)
314
+ product_2 = Product.create!(approved: false)
315
+
316
+ Product.not_approved # => [product_2]
317
+ ```
318
+
319
+ ### Date, DateTime Scopes
320
+
321
+ #### `<jsonb_defined_field>_before`
322
+
323
+ **Description:** returns all records where the value is before the argument. Also supports JSON string arguments.
324
+
325
+ ```ruby
326
+ product_1 = Product.create!(reviewed_at: 3.days.ago)
327
+ product_2 = Product.create!(reviewed_at: 5.days.ago)
328
+
329
+ Product.reviewed_at_before(4.days.ago) # => [product_2]
330
+ Product.reviewed_at_before(4.days.ago.to_json) # => [product_2]
331
+ ```
332
+
333
+ #### `<jsonb_defined_field>_after`
334
+
335
+ **Description:** returns all records where the value is after the argument. Also supports JSON string arguments.
336
+
337
+ ```ruby
338
+ product_1 = Product.create!(reviewed_at: 3.days.from_now)
339
+ product_2 = Product.create!(reviewed_at: 5.days.from_now)
340
+
341
+ Product.reviewed_at_after(4.days.from_now) # => [product_2]
342
+ Product.reviewed_at_after(4.days.from_now.to_json) # => [product_2]
343
+ ```
344
+
345
+ ### Array Scopes
346
+
347
+ #### `<jsonb_defined_fields>_contains`
348
+
349
+ **Description:** returns all records where the value is contained in the array field.
350
+
351
+ ```ruby
352
+ product_1 = Product.create!(previous_prices: [3])
353
+ product_2 = Product.create!(previous_prices: [4, 5, 6])
354
+
355
+ Product.previous_prices_contains(5) # => [product_2]
356
+ ```
131
357
 
132
358
  ## Migrations
133
359
 
data/bin/console CHANGED
@@ -12,10 +12,6 @@ ActiveRecord::Base.establish_connection(
12
12
  username: "postgres"
13
13
  )
14
14
 
15
- class Product < ActiveRecord::Base
16
- jsonb_accessor :options, title: :string, foo: { bar: { baz: :string } }
17
- end
18
-
19
15
  x = Product.new
20
16
 
21
17
  Pry.start
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../
3
3
  specs:
4
- jsonb_accessor (0.1.0)
4
+ jsonb_accessor (0.3.0)
5
5
  activerecord (>= 4.2.1)
6
6
  pg
7
7
 
@@ -57,10 +57,10 @@ GEM
57
57
  minitest (5.5.1)
58
58
  nokogiri (1.6.6.2)
59
59
  mini_portile (~> 0.6.0)
60
- parser (2.2.0.3)
60
+ parser (2.2.2.5)
61
61
  ast (>= 1.1, < 3.0)
62
62
  pg (0.18.1)
63
- powerpack (0.1.0)
63
+ powerpack (0.1.1)
64
64
  pry (0.10.1)
65
65
  coderay (~> 1.1.0)
66
66
  method_source (~> 0.8.1)
@@ -101,9 +101,9 @@ GEM
101
101
  diff-lcs (>= 1.2.0, < 2.0)
102
102
  rspec-support (~> 3.2.0)
103
103
  rspec-support (3.2.2)
104
- rubocop (0.30.0)
104
+ rubocop (0.31.0)
105
105
  astrolabe (~> 1.3)
106
- parser (>= 2.2.0.1, < 3.0)
106
+ parser (>= 2.2.2.1, < 3.0)
107
107
  powerpack (~> 0.1)
108
108
  rainbow (>= 1.99.1, < 3.0)
109
109
  ruby-progressbar (~> 1.4)
@@ -125,6 +125,7 @@ PLATFORMS
125
125
  ruby
126
126
 
127
127
  DEPENDENCIES
128
+ actionpack (~> 4.2.1)
128
129
  activerecord (= 4.2.1)
129
130
  appraisal
130
131
  awesome_print
@@ -136,6 +137,6 @@ DEPENDENCIES
136
137
  pry-nav
137
138
  rake (~> 10.0)
138
139
  rspec (~> 3.2.0)
139
- rubocop
140
+ rubocop (= 0.31.0)
140
141
  shoulda-matchers
141
142
  standalone_migrations
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "4.2.2"
6
+
7
+ gemspec :path => "../"
@@ -0,0 +1,143 @@
1
+ PATH
2
+ remote: ../
3
+ specs:
4
+ jsonb_accessor (0.3.0)
5
+ activerecord (>= 4.2.1)
6
+ pg
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ actionpack (4.2.2)
12
+ actionview (= 4.2.2)
13
+ activesupport (= 4.2.2)
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.1)
18
+ actionview (4.2.2)
19
+ activesupport (= 4.2.2)
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.1)
24
+ activemodel (4.2.2)
25
+ activesupport (= 4.2.2)
26
+ builder (~> 3.1)
27
+ activerecord (4.2.2)
28
+ activemodel (= 4.2.2)
29
+ activesupport (= 4.2.2)
30
+ arel (~> 6.0)
31
+ activesupport (4.2.2)
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.0.1)
38
+ activesupport (>= 3.2.21)
39
+ bundler
40
+ rake
41
+ thor (>= 0.14.0)
42
+ arel (6.0.0)
43
+ ast (2.0.0)
44
+ astrolabe (1.3.0)
45
+ parser (>= 2.2.0.pre.3, < 3.0)
46
+ awesome_print (1.6.1)
47
+ builder (3.2.2)
48
+ coderay (1.1.0)
49
+ database_cleaner (1.4.1)
50
+ diff-lcs (1.2.5)
51
+ erubis (2.7.0)
52
+ i18n (0.7.0)
53
+ json (1.8.3)
54
+ loofah (2.0.2)
55
+ nokogiri (>= 1.5.9)
56
+ method_source (0.8.2)
57
+ mini_portile (0.6.2)
58
+ minitest (5.7.0)
59
+ nokogiri (1.6.6.2)
60
+ mini_portile (~> 0.6.0)
61
+ parser (2.3.0.pre.2)
62
+ ast (>= 1.1, < 3.0)
63
+ pg (0.18.2)
64
+ powerpack (0.1.1)
65
+ pry (0.10.1)
66
+ coderay (~> 1.1.0)
67
+ method_source (~> 0.8.1)
68
+ slop (~> 3.4)
69
+ pry-doc (0.8.0)
70
+ pry (~> 0.9)
71
+ yard (~> 0.8)
72
+ pry-nav (0.2.4)
73
+ pry (>= 0.9.10, < 0.11.0)
74
+ rack (1.6.2)
75
+ rack-test (0.6.3)
76
+ rack (>= 1.0)
77
+ rails-deprecated_sanitizer (1.0.3)
78
+ activesupport (>= 4.2.0.alpha)
79
+ rails-dom-testing (1.0.6)
80
+ activesupport (>= 4.2.0.beta, < 5.0)
81
+ nokogiri (~> 1.6.0)
82
+ rails-deprecated_sanitizer (>= 1.0.1)
83
+ rails-html-sanitizer (1.0.2)
84
+ loofah (~> 2.0)
85
+ railties (4.2.2)
86
+ actionpack (= 4.2.2)
87
+ activesupport (= 4.2.2)
88
+ rake (>= 0.8.7)
89
+ thor (>= 0.18.1, < 2.0)
90
+ rainbow (2.0.0)
91
+ rake (10.4.2)
92
+ rspec (3.2.0)
93
+ rspec-core (~> 3.2.0)
94
+ rspec-expectations (~> 3.2.0)
95
+ rspec-mocks (~> 3.2.0)
96
+ rspec-core (3.2.3)
97
+ rspec-support (~> 3.2.0)
98
+ rspec-expectations (3.2.1)
99
+ diff-lcs (>= 1.2.0, < 2.0)
100
+ rspec-support (~> 3.2.0)
101
+ rspec-mocks (3.2.1)
102
+ diff-lcs (>= 1.2.0, < 2.0)
103
+ rspec-support (~> 3.2.0)
104
+ rspec-support (3.2.2)
105
+ rubocop (0.31.0)
106
+ astrolabe (~> 1.3)
107
+ parser (>= 2.2.2.1, < 3.0)
108
+ powerpack (~> 0.1)
109
+ rainbow (>= 1.99.1, < 3.0)
110
+ ruby-progressbar (~> 1.4)
111
+ ruby-progressbar (1.7.5)
112
+ shoulda-matchers (2.8.0)
113
+ activesupport (>= 3.0.0)
114
+ slop (3.6.0)
115
+ standalone_migrations (4.0.2)
116
+ activerecord (~> 4.2.0)
117
+ railties (~> 4.2.0)
118
+ rake (~> 10.0)
119
+ thor (0.19.1)
120
+ thread_safe (0.3.5)
121
+ tzinfo (1.2.2)
122
+ thread_safe (~> 0.1)
123
+ yard (0.8.7.6)
124
+
125
+ PLATFORMS
126
+ ruby
127
+
128
+ DEPENDENCIES
129
+ actionpack (~> 4.2.1)
130
+ activerecord (= 4.2.2)
131
+ appraisal
132
+ awesome_print
133
+ bundler (~> 1.9)
134
+ database_cleaner
135
+ jsonb_accessor!
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
@@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.add_dependency "activerecord", ">= 4.2.1"
23
23
  spec.add_dependency "pg"
24
24
 
25
+ spec.add_development_dependency "actionpack", "~> 4.2.1"
25
26
  spec.add_development_dependency "appraisal"
26
27
  spec.add_development_dependency "bundler", "~> 1.9"
27
28
  spec.add_development_dependency "database_cleaner"
@@ -31,7 +32,7 @@ Gem::Specification.new do |spec|
31
32
  spec.add_development_dependency "pry-nav"
32
33
  spec.add_development_dependency "rake", "~> 10.0"
33
34
  spec.add_development_dependency "rspec", "~> 3.2.0"
34
- spec.add_development_dependency "rubocop"
35
+ spec.add_development_dependency "rubocop", "0.31.0"
35
36
  spec.add_development_dependency "shoulda-matchers"
36
37
  spec.add_development_dependency "standalone_migrations"
37
38
  end
@@ -3,6 +3,8 @@ require "active_record"
3
3
  require "active_record/connection_adapters/postgresql_adapter"
4
4
 
5
5
  require "jsonb_accessor/version"
6
+ require "jsonb_accessor/fields_map"
7
+ require "jsonb_accessor/helpers"
6
8
  require "jsonb_accessor/type_helper"
7
9
  require "jsonb_accessor/nested_base"
8
10
  require "jsonb_accessor/class_builder"
@@ -15,4 +17,5 @@ end
15
17
 
16
18
  ActiveSupport.on_load(:active_record) do
17
19
  ActiveRecord::Base.send(:include, JsonbAccessor)
20
+ ActiveRecord::Base.send(:include, JsonbAccessor::Helpers)
18
21
  end
@@ -1,76 +1,100 @@
1
1
  module JsonbAccessor
2
2
  UnknownValue = Class.new(StandardError)
3
+ CLASS_PREFIX = "JA"
3
4
 
4
5
  module ClassBuilder
5
6
  class << self
6
7
  def generate_class(namespace, new_class_name, attribute_definitions)
7
- grouped_attributes = group_attributes(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)
8
11
 
9
- klass = Class.new(NestedBase)
10
- namespace.const_set(new_class_name.to_s.camelize, klass)
11
- nested_classes = generate_nested_classes(klass, grouped_attributes[:nested])
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)
12
16
 
13
- klass.class_eval do
14
- singleton_class.send(:define_method, :nested_classes) { nested_classes }
15
- singleton_class.send(:define_method, :attribute_on_parent_name) { new_class_name }
17
+ klass
18
+ end
16
19
 
17
- define_method(:attributes_and_data_types) do
18
- @attributes_and_data_types ||= grouped_attributes[:typed].each_with_object({}) do |(name, type), attrs_and_data_types|
19
- attrs_and_data_types[name] = TypeHelper.fetch(type)
20
- end
21
- end
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
22
25
 
23
- grouped_attributes[:typed].keys.each do |attribute_name|
24
- define_method(attribute_name) { attributes[attribute_name] }
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
25
36
 
26
- define_method("#{attribute_name}=") do |value|
27
- cast_value = attributes_and_data_types[attribute_name].type_cast_from_user(value)
28
- attributes[attribute_name] = cast_value
29
- update_parent
30
- end
31
- end
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
32
42
 
33
- grouped_attributes[:nested].keys.each do |attribute_name|
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|
34
48
  attr_reader attribute_name
35
49
 
36
50
  define_method("#{attribute_name}=") do |value|
37
51
  instance_class = nested_classes[attribute_name]
52
+ instance = cast_nested_field_value(value, instance_class, __method__)
38
53
 
39
- case value
40
- when instance_class
41
- instance = instance_class.new(value.attributes)
42
- when Hash
43
- instance = instance_class.new(value)
44
- when nil
45
- instance = instance_class.new
46
- else
47
- raise UnknownValue, "unable to set value '#{value}' is not a hash, `nil`, or an instance of #{instance_class} in #{__method__}"
48
- end
49
-
50
- instance.parent = self
51
54
  instance_variable_set("@#{attribute_name}", instance)
52
55
  attributes[attribute_name] = instance.attributes
53
56
  update_parent
54
57
  end
55
58
  end
56
59
  end
57
- klass
58
60
  end
59
61
 
60
- def generate_nested_classes(klass, nested_attributes)
61
- nested_attributes.each_with_object({}) do |(attribute_name, nested_attrs), nested_classes|
62
- nested_classes[attribute_name] = generate_class(klass, attribute_name, nested_attrs)
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
+ define_method("#{attribute_name}=") do |value|
68
+ cast_value = attributes_and_data_types[attribute_name].type_cast_from_user(value)
69
+ attributes[attribute_name] = cast_value
70
+ update_parent
71
+ end
72
+ end
63
73
  end
64
74
  end
65
75
 
66
- private
67
-
68
- def group_attributes(attributes)
69
- attributes.each_with_object(nested: {}, typed: {}) do |(name, type_or_nested), grouped_attributes|
70
- group = type_or_nested.is_a?(Hash) ? grouped_attributes[:nested] : grouped_attributes[:typed]
71
- group[name] = type_or_nested
76
+ def define_attributes_and_data_types(klass, fields_map)
77
+ klass.send(:define_method, :attributes_and_data_types) do
78
+ @attributes_and_data_types ||= fields_map.typed_fields.each_with_object({}) do |(name, type), attrs_and_data_types|
79
+ attrs_and_data_types[name] = TypeHelper.fetch(type)
80
+ end
72
81
  end
73
82
  end
83
+
84
+ def define_class_methods(klass, nested_classes, attribute_name)
85
+ klass.singleton_class.send(:define_method, :nested_classes) { nested_classes }
86
+ klass.singleton_class.send(:define_method, :attribute_on_parent_name) { attribute_name }
87
+ end
88
+
89
+ def generate_new_class(new_class_name, fields_map, namespace)
90
+ klass = Class.new(NestedBase)
91
+ new_class_name_camelized = generate_constant_name(new_class_name)
92
+ namespace.const_set(new_class_name_camelized, klass)
93
+ end
94
+
95
+ def generate_constant_name(attribute_name)
96
+ "#{CLASS_PREFIX}#{attribute_name.to_s.camelize}"
97
+ end
74
98
  end
75
99
  end
76
100
  end
@@ -0,0 +1,32 @@
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
@@ -0,0 +1,19 @@
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,61 +1,112 @@
1
1
  module JsonbAccessor
2
2
  module Macro
3
- class << self
4
- def group_attributes(value_fields, typed_fields)
5
- value_fields_hash = process_value_fields(value_fields)
3
+ 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"
6
11
 
7
- typed_fields.each_with_object(nested: {}, typed: value_fields_hash) do |(attribute_name, type_or_nested), grouped_attributes|
8
- group = type_or_nested.is_a?(Hash) ? grouped_attributes[:nested] : grouped_attributes[:typed]
9
- group[attribute_name] = type_or_nested
12
+ singleton_class.send(:define_method, "#{jsonb_attribute}_classes") do
13
+ nested_classes
10
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
11
24
  end
12
25
 
13
- def process_value_fields(value_fields)
14
- value_fields.each_with_object({}) do |value_field, hash_for_value_fields|
15
- hash_for_value_fields[value_field] = :value
26
+ private
27
+
28
+ def _register_jsonb_classes_for_cleanup
29
+ if defined?(ActionDispatch) && ENV["RACK_ENV"] == "development"
30
+ class_name = CLASS_PREFIX + name
31
+ ActionDispatch::Reloader.to_cleanup do
32
+ if JsonbAccessor.constants.any? { |c| c.to_s == class_name }
33
+ JsonbAccessor.send(:remove_const, class_name)
34
+ end
35
+ end
16
36
  end
17
37
  end
18
38
 
19
- def build_class_namespace(class_name)
20
- class_name = class_name.gsub(CONSTANT_SEPARATOR, "")
21
- if JsonbAccessor.constants.any? { |c| c.to_s == class_name }
22
- class_namespace = JsonbAccessor.const_get(class_name)
23
- else
24
- class_namespace = Module.new
25
- JsonbAccessor.const_set(class_name, class_namespace)
39
+ def _initialize_jsonb_attrs(jsonb_attribute, fields_map, jsonb_attribute_initialization_method_name)
40
+ define_method(jsonb_attribute_initialization_method_name) do
41
+ jsonb_attribute_hash = send(jsonb_attribute) || {}
42
+ fields_map.names.each do |field|
43
+ send("#{field}=", jsonb_attribute_hash[field.to_s])
44
+ end
26
45
  end
27
- class_namespace
46
+ after_initialize(jsonb_attribute_initialization_method_name)
28
47
  end
29
- end
30
48
 
31
- module ClassMethods
32
- def jsonb_accessor(jsonb_attribute, *value_fields, **typed_fields)
33
- all_fields = Macro.group_attributes(value_fields, typed_fields)
34
- nested_fields, typed_fields = all_fields.values_at(:nested, :typed)
49
+ def _create_jsonb_attribute_scope_name(jsonb_attribute, jsonb_attribute_scope_name)
50
+ scope jsonb_attribute_scope_name, (lambda do |attributes|
51
+ query_options = new(attributes).send(jsonb_attribute)
52
+ fields = attributes.keys.map(&:to_s)
53
+ query_options.delete_if { |key, value| fields.exclude?(key) }
54
+ query_json = TypeHelper.type_cast_as_jsonb(query_options)
55
+ where("#{table_name}.#{jsonb_attribute} @> ?", query_json)
56
+ end)
57
+ end
35
58
 
36
- class_namespace = Macro.build_class_namespace(name)
37
- attribute_namespace = Module.new
38
- class_namespace.const_set(jsonb_attribute.to_s.camelize, attribute_namespace)
59
+ def _create_jsonb_scopes(jsonb_attribute, fields_map, jsonb_attribute_scope_name)
60
+ __create_jsonb_standard_scopes(fields_map, jsonb_attribute_scope_name)
61
+ __create_jsonb_typed_scopes(jsonb_attribute, fields_map)
62
+ end
39
63
 
40
- nested_classes = ClassBuilder.generate_nested_classes(attribute_namespace, nested_fields)
64
+ def __create_jsonb_standard_scopes(fields_map, jsonb_attribute_scope_name)
65
+ fields_map.names.each do |field|
66
+ scope "with_#{field}", -> (value) { send(jsonb_attribute_scope_name, field => value) }
67
+ end
68
+ end
41
69
 
42
- singleton_class.send(:define_method, "#{jsonb_attribute}_classes") do
43
- nested_classes
70
+ def __create_jsonb_typed_scopes(jsonb_attribute, fields_map)
71
+ fields_map.typed_fields.each do |field, type|
72
+ case type
73
+ when :boolean
74
+ ___create_jsonb_boolean_scopes(field)
75
+ when :integer, :float, :decimal, :big_integer
76
+ ___create_jsonb_numeric_scopes(field, jsonb_attribute, type)
77
+ when :date_time, :date
78
+ ___create_jsonb_date_time_scopes(field, jsonb_attribute, type)
79
+ when /array/
80
+ ___create_jsonb_array_scopes(field)
81
+ end
44
82
  end
83
+ end
45
84
 
46
- delegate "#{jsonb_attribute}_classes", to: :class
85
+ def ___create_jsonb_boolean_scopes(field)
86
+ scope "is_#{field}", -> { send("with_#{field}", true) }
87
+ scope "not_#{field}", -> { send("with_#{field}", false) }
88
+ end
47
89
 
48
- jsonb_attribute_initialization_method_name = "initialize_jsonb_attrs_for_#{jsonb_attribute}"
90
+ def ___create_jsonb_numeric_scopes(field, jsonb_attribute, type)
91
+ safe_type = type.to_s.gsub("big_", "")
92
+ scope "__numeric_#{field}_comparator", -> (value, operator) { where("((#{table_name}.#{jsonb_attribute}) ->> ?)::#{safe_type} #{operator} ?", field, value) }
93
+ scope "#{field}_lt", -> (value) { send("__numeric_#{field}_comparator", value, "<") }
94
+ scope "#{field}_lte", -> (value) { send("__numeric_#{field}_comparator", value, "<=") }
95
+ scope "#{field}_gte", -> (value) { send("__numeric_#{field}_comparator", value, ">=") }
96
+ scope "#{field}_gt", -> (value) { send("__numeric_#{field}_comparator", value, ">") }
97
+ end
49
98
 
50
- define_method(jsonb_attribute_initialization_method_name) do
51
- jsonb_attribute_hash = send(jsonb_attribute) || {}
52
- (typed_fields.keys + nested_fields.keys).each do |field|
53
- send("#{field}=", jsonb_attribute_hash[field.to_s])
54
- end
55
- end
99
+ def ___create_jsonb_date_time_scopes(field, jsonb_attribute, type)
100
+ scope "__date_time_#{field}_comparator", -> (value, operator) { where("((#{table_name}.#{jsonb_attribute}) ->> ?)::timestamp #{operator} ?::timestamp", field, value.to_json) }
101
+ scope "#{field}_before", -> (value) { send("__date_time_#{field}_comparator", value, "<") }
102
+ scope "#{field}_after", -> (value) { send("__date_time_#{field}_comparator", value, ">") }
103
+ end
56
104
 
57
- after_initialize(jsonb_attribute_initialization_method_name)
105
+ def ___create_jsonb_array_scopes(field)
106
+ scope "#{field}_contains", -> (value) { send("with_#{field}", [value]) }
107
+ end
58
108
 
109
+ def _create_jsonb_accessor_methods(jsonb_attribute, jsonb_attribute_initialization_method_name, fields_map)
59
110
  jsonb_accessor_methods = Module.new do
60
111
  define_method("#{jsonb_attribute}=") do |value|
61
112
  write_attribute(jsonb_attribute, value)
@@ -69,7 +120,13 @@ module JsonbAccessor
69
120
  end
70
121
  end
71
122
 
72
- typed_fields.each do |field, type|
123
+ __create_jsonb_typed_field_setters(jsonb_attribute, jsonb_accessor_methods, fields_map)
124
+ __create_jsonb_nested_field_accessors(jsonb_attribute, jsonb_accessor_methods, fields_map)
125
+ include jsonb_accessor_methods
126
+ end
127
+
128
+ def __create_jsonb_typed_field_setters(jsonb_attribute, jsonb_accessor_methods, fields_map)
129
+ fields_map.typed_fields.each do |field, type|
73
130
  attribute(field.to_s, TypeHelper.fetch(type))
74
131
 
75
132
  jsonb_accessor_methods.instance_eval do
@@ -80,34 +137,22 @@ module JsonbAccessor
80
137
  end
81
138
  end
82
139
  end
140
+ end
83
141
 
84
- nested_fields.each do |field, nested_attributes|
142
+ def __create_jsonb_nested_field_accessors(jsonb_attribute, jsonb_accessor_methods, fields_map)
143
+ fields_map.nested_fields.each do |field, nested_attributes|
85
144
  attribute(field.to_s, TypeHelper.fetch(:value))
86
-
87
145
  jsonb_accessor_methods.instance_eval do
88
146
  define_method("#{field}=") do |value|
89
- instance_class = nested_classes[field]
90
-
91
- case value
92
- when instance_class
93
- instance = instance_class.new(value.attributes)
94
- when Hash
95
- instance = instance_class.new(value)
96
- when nil
97
- instance = instance_class.new
98
- else
99
- raise UnknownValue, "unable to set value '#{value}' is not a hash, `nil`, or an instance of #{instance_class} in #{__method__}"
100
- end
101
-
102
- instance.parent = self
147
+ instance_class = send("#{jsonb_attribute}_classes")[field]
148
+ instance = cast_nested_field_value(value, instance_class, __method__)
149
+
103
150
  new_jsonb_value = (send(jsonb_attribute) || {}).merge(field.to_s => instance.attributes)
104
151
  write_attribute(jsonb_attribute, new_jsonb_value)
105
152
  super(instance)
106
153
  end
107
154
  end
108
155
  end
109
-
110
- include jsonb_accessor_methods
111
156
  end
112
157
  end
113
158
  end
@@ -1,5 +1,7 @@
1
1
  module JsonbAccessor
2
2
  class NestedBase
3
+ include Helpers
4
+
3
5
  attr_accessor :attributes, :parent
4
6
  alias_method :to_h, :attributes
5
7
 
@@ -18,8 +18,17 @@ module JsonbAccessor
18
18
  end
19
19
  end
20
20
 
21
+ def type_cast_as_jsonb(suspect)
22
+ type_cast_hash = jsonb.type_cast_from_user(suspect)
23
+ jsonb.type_cast_for_database(type_cast_hash)
24
+ end
25
+
21
26
  private
22
27
 
28
+ def jsonb
29
+ @jsonb ||= fetch(:jsonb)
30
+ end
31
+
23
32
  def fetch_active_record_array_type(type)
24
33
  subtype = type.to_s.sub(ARRAY_MATCHER, "")
25
34
  new_array(fetch_active_record_type(subtype))
@@ -1,3 +1,3 @@
1
1
  module JsonbAccessor
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  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.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Crismali
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2015-04-17 00:00:00.000000000 Z
12
+ date: 2015-06-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -39,6 +39,20 @@ dependencies:
39
39
  - - ">="
40
40
  - !ruby/object:Gem::Version
41
41
  version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: actionpack
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: 4.2.1
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: 4.2.1
42
56
  - !ruby/object:Gem::Dependency
43
57
  name: appraisal
44
58
  requirement: !ruby/object:Gem::Requirement
@@ -169,16 +183,16 @@ dependencies:
169
183
  name: rubocop
170
184
  requirement: !ruby/object:Gem::Requirement
171
185
  requirements:
172
- - - ">="
186
+ - - '='
173
187
  - !ruby/object:Gem::Version
174
- version: '0'
188
+ version: 0.31.0
175
189
  type: :development
176
190
  prerelease: false
177
191
  version_requirements: !ruby/object:Gem::Requirement
178
192
  requirements:
179
- - - ">="
193
+ - - '='
180
194
  - !ruby/object:Gem::Version
181
- version: '0'
195
+ version: 0.31.0
182
196
  - !ruby/object:Gem::Dependency
183
197
  name: shoulda-matchers
184
198
  requirement: !ruby/object:Gem::Requirement
@@ -234,9 +248,13 @@ files:
234
248
  - docker-compose.yml
235
249
  - gemfiles/activerecord_4.2.1.gemfile
236
250
  - gemfiles/activerecord_4.2.1.gemfile.lock
251
+ - gemfiles/activerecord_4.2.2.gemfile
252
+ - gemfiles/activerecord_4.2.2.gemfile.lock
237
253
  - jsonb_accessor.gemspec
238
254
  - lib/jsonb_accessor.rb
239
255
  - lib/jsonb_accessor/class_builder.rb
256
+ - lib/jsonb_accessor/fields_map.rb
257
+ - lib/jsonb_accessor/helpers.rb
240
258
  - lib/jsonb_accessor/macro.rb
241
259
  - lib/jsonb_accessor/nested_base.rb
242
260
  - lib/jsonb_accessor/type_helper.rb