jsonb_accessor 0.2.0 → 0.3.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
  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