pg_serializable 0.1.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +58 -0
- data/README.md +227 -16
- data/lib/pg_serializable.rb +35 -24
- data/lib/pg_serializable/errors.rb +3 -4
- data/lib/pg_serializable/nodes.rb +5 -0
- data/lib/pg_serializable/nodes/association.rb +3 -39
- data/lib/pg_serializable/nodes/attribute.rb +4 -15
- data/lib/pg_serializable/trait.rb +32 -0
- data/lib/pg_serializable/trait_manager.rb +32 -0
- data/lib/pg_serializable/version.rb +1 -1
- data/lib/pg_serializable/visitable.rb +7 -0
- data/lib/pg_serializable/visitors.rb +8 -0
- data/lib/pg_serializable/visitors/base.rb +89 -0
- data/lib/pg_serializable/visitors/json.rb +137 -0
- data/lib/pg_serializable/visitors/validation.rb +68 -0
- data/pg_serializable.gemspec +1 -0
- metadata +24 -4
- data/lib/pg_serializable/aliaser.rb +0 -15
- data/lib/pg_serializable/serializer.rb +0 -102
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fc026e950ca7cd42a452b17f7ea42e69513298ff
|
4
|
+
data.tar.gz: de6db447af2b2ba0f845dffeb29efd02c130089a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 11102ebb57aa4da829575d9b7bc70d94a9fcf670175aaaa9089868d3df011c11f23e63f5e4c12ef85276532e1b76f2aaec1a0f004ee0e3a3052cb140c9a700be
|
7
|
+
data.tar.gz: 0ea08435b36bda86588c0ed4c771e3e879c5d4ababd309b0c8f0969b6c269c25d2f001bdc16f33b80b40424376a33955d03438c13c57da98fda19a837a2837d8
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
pg_serializable (1.0.0)
|
5
|
+
activerecord (~> 5.2)
|
6
|
+
activesupport (~> 5.2)
|
7
|
+
oj (~> 3.6)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
activemodel (5.2.1)
|
13
|
+
activesupport (= 5.2.1)
|
14
|
+
activerecord (5.2.1)
|
15
|
+
activemodel (= 5.2.1)
|
16
|
+
activesupport (= 5.2.1)
|
17
|
+
arel (>= 9.0)
|
18
|
+
activesupport (5.2.1)
|
19
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
20
|
+
i18n (>= 0.7, < 2)
|
21
|
+
minitest (~> 5.1)
|
22
|
+
tzinfo (~> 1.1)
|
23
|
+
arel (9.0.0)
|
24
|
+
concurrent-ruby (1.0.5)
|
25
|
+
diff-lcs (1.3)
|
26
|
+
i18n (1.1.1)
|
27
|
+
concurrent-ruby (~> 1.0)
|
28
|
+
minitest (5.11.3)
|
29
|
+
oj (3.6.11)
|
30
|
+
rake (10.5.0)
|
31
|
+
rspec (3.8.0)
|
32
|
+
rspec-core (~> 3.8.0)
|
33
|
+
rspec-expectations (~> 3.8.0)
|
34
|
+
rspec-mocks (~> 3.8.0)
|
35
|
+
rspec-core (3.8.0)
|
36
|
+
rspec-support (~> 3.8.0)
|
37
|
+
rspec-expectations (3.8.2)
|
38
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
39
|
+
rspec-support (~> 3.8.0)
|
40
|
+
rspec-mocks (3.8.0)
|
41
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
42
|
+
rspec-support (~> 3.8.0)
|
43
|
+
rspec-support (3.8.0)
|
44
|
+
thread_safe (0.3.6)
|
45
|
+
tzinfo (1.2.5)
|
46
|
+
thread_safe (~> 0.1)
|
47
|
+
|
48
|
+
PLATFORMS
|
49
|
+
ruby
|
50
|
+
|
51
|
+
DEPENDENCIES
|
52
|
+
bundler (~> 1.16)
|
53
|
+
pg_serializable!
|
54
|
+
rake (~> 10.0)
|
55
|
+
rspec (~> 3.0)
|
56
|
+
|
57
|
+
BUNDLED WITH
|
58
|
+
1.16.5
|
data/README.md
CHANGED
@@ -4,6 +4,58 @@ This is experimental.
|
|
4
4
|
|
5
5
|
Serialize json directly from postgres (9.4+).
|
6
6
|
|
7
|
+
## Why?
|
8
|
+
Models:
|
9
|
+
```ruby
|
10
|
+
class Product < ApplicationRecord
|
11
|
+
has_many :categories_products
|
12
|
+
has_many :categories, through: :categories_products
|
13
|
+
has_many :variations
|
14
|
+
belongs_to :label
|
15
|
+
end
|
16
|
+
class Variation < ApplicationRecord
|
17
|
+
belongs_to :product
|
18
|
+
belongs_to :color
|
19
|
+
end
|
20
|
+
class Color < ApplicationRecord
|
21
|
+
has_many :variations
|
22
|
+
end
|
23
|
+
class Label < ApplicationRecord
|
24
|
+
has_many :products
|
25
|
+
end
|
26
|
+
class Category < ApplicationRecord
|
27
|
+
has_many :categories_products
|
28
|
+
has_many :products, through: :categories_products
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
Using Jbuilder+ActiveRecord:
|
33
|
+
```ruby
|
34
|
+
class Api::ProductsController < ApplicationController
|
35
|
+
def index
|
36
|
+
@products = Product.limit(200)
|
37
|
+
.order(updated_at: :desc)
|
38
|
+
.includes(:categories, :label, variations: :color)
|
39
|
+
render 'api/products/index.json.jbuilder'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
```
|
43
|
+
```shell
|
44
|
+
Completed 200 OK in 2521ms (Views: 2431.8ms | ActiveRecord: 45.7ms)
|
45
|
+
```
|
46
|
+
|
47
|
+
Using PgSerializable:
|
48
|
+
```ruby
|
49
|
+
class Api::ProductsController < ApplicationController
|
50
|
+
def index
|
51
|
+
render json: Product.limit(200).order(updated_at: :desc).json
|
52
|
+
end
|
53
|
+
end
|
54
|
+
```
|
55
|
+
```shell
|
56
|
+
Completed 200 OK in 89ms (Views: 0.1ms | ActiveRecord: 78.9ms)
|
57
|
+
```
|
58
|
+
|
7
59
|
## Installation
|
8
60
|
|
9
61
|
Add this line to your application's Gemfile:
|
@@ -30,8 +82,10 @@ class Product < ApplicationRecord
|
|
30
82
|
include PgSerializable
|
31
83
|
|
32
84
|
serializable do
|
33
|
-
|
34
|
-
|
85
|
+
default do
|
86
|
+
attributes :name, :id
|
87
|
+
attribute :name, label: :test_name
|
88
|
+
end
|
35
89
|
end
|
36
90
|
end
|
37
91
|
```
|
@@ -87,23 +141,68 @@ attribute :name, label: :different_name
|
|
87
141
|
Wrap attributes in custom sql
|
88
142
|
```ruby
|
89
143
|
serializable do
|
90
|
-
|
91
|
-
|
144
|
+
default do
|
145
|
+
attributes :id
|
146
|
+
attribute :active, label: :deleted { |v| "NOT #{v}" }
|
147
|
+
end
|
92
148
|
end
|
93
149
|
```
|
150
|
+
```sql
|
151
|
+
SELECT
|
152
|
+
COALESCE(json_agg(
|
153
|
+
json_build_object('id', a0.id, 'deleted', NOT a0.active)
|
154
|
+
), '[]'::json)
|
155
|
+
FROM (
|
156
|
+
SELECT "products".*
|
157
|
+
FROM "products"
|
158
|
+
ORDER BY "products"."updated_at" DESC
|
159
|
+
LIMIT 2
|
160
|
+
) a0
|
161
|
+
```
|
94
162
|
```json
|
95
163
|
[
|
96
164
|
{
|
97
165
|
"id": 503,
|
98
|
-
"deleted":
|
166
|
+
"deleted": false
|
99
167
|
},
|
100
168
|
{
|
101
169
|
"id": 502,
|
102
|
-
"deleted":
|
170
|
+
"deleted": false
|
103
171
|
}
|
104
172
|
]
|
105
173
|
```
|
174
|
+
### Traits
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
serializable do
|
178
|
+
default do
|
179
|
+
attributes :id, :name
|
180
|
+
end
|
181
|
+
|
182
|
+
trait :simple do
|
183
|
+
attributes :id
|
184
|
+
end
|
185
|
+
end
|
186
|
+
```
|
106
187
|
|
188
|
+
```ruby
|
189
|
+
render json: Product.limit(10).json(trait: :simple)
|
190
|
+
```
|
191
|
+
|
192
|
+
```json
|
193
|
+
[
|
194
|
+
{ "id": 1 },
|
195
|
+
{ "id": 2 },
|
196
|
+
{ "id": 3 },
|
197
|
+
{ "id": 4 },
|
198
|
+
{ "id": 5 },
|
199
|
+
{ "id": 6 },
|
200
|
+
{ "id": 7 },
|
201
|
+
{ "id": 8 },
|
202
|
+
{ "id": 9 },
|
203
|
+
{ "id": 10 }
|
204
|
+
]
|
205
|
+
```
|
107
206
|
|
108
207
|
### Associations
|
109
208
|
Supported associations:
|
@@ -115,8 +214,10 @@ Supported associations:
|
|
115
214
|
#### belongs_to
|
116
215
|
```ruby
|
117
216
|
serializable do
|
118
|
-
|
119
|
-
|
217
|
+
default do
|
218
|
+
attributes :id, :name
|
219
|
+
belongs_to: :label
|
220
|
+
end
|
120
221
|
end
|
121
222
|
```
|
122
223
|
```json
|
@@ -143,21 +244,27 @@ Works for nested relationships
|
|
143
244
|
```ruby
|
144
245
|
class Product < ApplicationRecord
|
145
246
|
serializable do
|
146
|
-
|
147
|
-
|
247
|
+
default do
|
248
|
+
attributes :id, :name
|
249
|
+
has_many: :variations
|
250
|
+
end
|
148
251
|
end
|
149
252
|
end
|
150
253
|
|
151
254
|
class Variation < ApplicationRecord
|
152
255
|
serializable do
|
153
|
-
|
154
|
-
|
256
|
+
default do
|
257
|
+
attributes :id, :name
|
258
|
+
belongs_to: :color
|
259
|
+
end
|
155
260
|
end
|
156
261
|
end
|
157
262
|
|
158
263
|
class Color < ApplicationRecord
|
159
264
|
serializable do
|
160
|
-
|
265
|
+
default do
|
266
|
+
attributes :id, :hex
|
267
|
+
end
|
161
268
|
end
|
162
269
|
end
|
163
270
|
```
|
@@ -215,14 +322,18 @@ class Product < ApplicationRecord
|
|
215
322
|
has_many :categories, through: :categories_products
|
216
323
|
|
217
324
|
serializable do
|
218
|
-
|
219
|
-
|
325
|
+
default do
|
326
|
+
attributes :id
|
327
|
+
has_many :categories
|
328
|
+
end
|
220
329
|
end
|
221
330
|
end
|
222
331
|
|
223
332
|
class Category < ApplicationRecord
|
224
333
|
serializable do
|
225
|
-
|
334
|
+
default do
|
335
|
+
attributes :name, :id
|
336
|
+
end
|
226
337
|
end
|
227
338
|
end
|
228
339
|
```
|
@@ -260,6 +371,106 @@ end
|
|
260
371
|
#### has_one
|
261
372
|
TODO: write examples
|
262
373
|
|
374
|
+
### Association Traits
|
375
|
+
Models:
|
376
|
+
```ruby
|
377
|
+
class Product < ApplicationRecord
|
378
|
+
has_many :variations
|
379
|
+
|
380
|
+
serializable do
|
381
|
+
default do
|
382
|
+
attributes :id, :name
|
383
|
+
end
|
384
|
+
|
385
|
+
trait :with_variations do
|
386
|
+
attributes :id
|
387
|
+
has_many :variations, trait: :for_products
|
388
|
+
end
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
class Variation < ApplicationRecord
|
393
|
+
serializable do
|
394
|
+
default do
|
395
|
+
attributes :id
|
396
|
+
belongs_to: :color
|
397
|
+
end
|
398
|
+
|
399
|
+
trait :for_products do
|
400
|
+
attributes :id
|
401
|
+
end
|
402
|
+
end
|
403
|
+
end
|
404
|
+
```
|
405
|
+
|
406
|
+
Controller:
|
407
|
+
```ruby
|
408
|
+
render json: Product.limit(3).json(trait: :with_variations)
|
409
|
+
```
|
410
|
+
|
411
|
+
```json
|
412
|
+
[
|
413
|
+
{
|
414
|
+
"id":1,
|
415
|
+
"variations":[
|
416
|
+
|
417
|
+
]
|
418
|
+
},
|
419
|
+
{
|
420
|
+
"id":2,
|
421
|
+
"variations":[
|
422
|
+
{
|
423
|
+
"id":5
|
424
|
+
},
|
425
|
+
{
|
426
|
+
"id":4
|
427
|
+
},
|
428
|
+
{
|
429
|
+
"id":3
|
430
|
+
},
|
431
|
+
{
|
432
|
+
"id":2
|
433
|
+
},
|
434
|
+
{
|
435
|
+
"id":1
|
436
|
+
}
|
437
|
+
]
|
438
|
+
},
|
439
|
+
{
|
440
|
+
"id":3,
|
441
|
+
"variations":[
|
442
|
+
{
|
443
|
+
"id":14
|
444
|
+
},
|
445
|
+
{
|
446
|
+
"id":13
|
447
|
+
},
|
448
|
+
{
|
449
|
+
"id":12
|
450
|
+
},
|
451
|
+
{
|
452
|
+
"id":11
|
453
|
+
},
|
454
|
+
{
|
455
|
+
"id":10
|
456
|
+
},
|
457
|
+
{
|
458
|
+
"id":9
|
459
|
+
},
|
460
|
+
{
|
461
|
+
"id":8
|
462
|
+
},
|
463
|
+
{
|
464
|
+
"id":7
|
465
|
+
},
|
466
|
+
{
|
467
|
+
"id":6
|
468
|
+
}
|
469
|
+
]
|
470
|
+
}
|
471
|
+
]
|
472
|
+
```
|
473
|
+
|
263
474
|
## Development
|
264
475
|
|
265
476
|
TODO
|
data/lib/pg_serializable.rb
CHANGED
@@ -1,47 +1,58 @@
|
|
1
|
+
require 'oj'
|
1
2
|
require 'active_support/concern'
|
2
|
-
|
3
|
-
require 'pg_serializable/version'
|
4
3
|
require 'pg_serializable/errors'
|
5
|
-
require 'pg_serializable/
|
4
|
+
require 'pg_serializable/visitable'
|
6
5
|
require 'pg_serializable/nodes'
|
7
|
-
require 'pg_serializable/
|
6
|
+
require 'pg_serializable/trait_manager'
|
7
|
+
require 'pg_serializable/trait'
|
8
|
+
require 'pg_serializable/visitors'
|
9
|
+
|
10
|
+
module ActiveRecord
|
11
|
+
class Relation
|
12
|
+
include PgSerializable::Visitable
|
13
|
+
|
14
|
+
def json(trait: :default)
|
15
|
+
to_pg_json accept(PgSerializable::Visitors::Json.new, trait: trait)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
8
19
|
|
9
20
|
module PgSerializable
|
10
21
|
extend ActiveSupport::Concern
|
22
|
+
|
11
23
|
included do
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
)
|
24
|
+
include Visitable
|
25
|
+
|
26
|
+
def json(trait: :default)
|
27
|
+
self.class.to_pg_json accept(PgSerializable::Visitors::Json.new, trait: trait)
|
16
28
|
end
|
17
29
|
end
|
18
30
|
|
19
31
|
class_methods do
|
20
|
-
def json
|
21
|
-
|
22
|
-
serializer.as_json_array(pg_scope, Aliaser.new).to_sql
|
23
|
-
)['coalesce']
|
32
|
+
def json(trait: :default)
|
33
|
+
to_pg_json accept(PgSerializable::Visitors::Json.new, trait: trait)
|
24
34
|
end
|
25
35
|
|
26
|
-
def
|
27
|
-
|
36
|
+
def serializable(&blk)
|
37
|
+
trait_manager.instance_eval &blk
|
38
|
+
validate_traits!
|
28
39
|
end
|
29
40
|
|
30
|
-
def
|
31
|
-
|
41
|
+
def trait_manager
|
42
|
+
@trait_manager ||= TraitManager.new(self)
|
32
43
|
end
|
33
44
|
|
34
|
-
def
|
35
|
-
|
36
|
-
serializer.check_for_cycles!
|
45
|
+
def accept visitor, **kwargs
|
46
|
+
visitor.visit self, **kwargs
|
37
47
|
end
|
38
48
|
|
39
|
-
def
|
40
|
-
|
49
|
+
def to_pg_json(scope)
|
50
|
+
res = scope.as_json.first
|
51
|
+
::Oj.dump(res['coalesce'] || res['json_build_object'])
|
41
52
|
end
|
42
53
|
|
43
|
-
|
44
|
-
|
45
|
-
|
54
|
+
private
|
55
|
+
|
56
|
+
delegate :validate_traits!, to: :trait_manager
|
46
57
|
end
|
47
58
|
end
|
@@ -1,6 +1,5 @@
|
|
1
1
|
module PgSerializable
|
2
|
-
class
|
3
|
-
|
4
|
-
|
5
|
-
end
|
2
|
+
class AttributeError < ::StandardError;end
|
3
|
+
class AssociationError < ::StandardError;end
|
4
|
+
class UnknownAttributeError < ::StandardError;end
|
6
5
|
end
|
@@ -1,13 +1,14 @@
|
|
1
1
|
module PgSerializable
|
2
2
|
module Nodes
|
3
3
|
class Association < Base
|
4
|
-
attr_reader :klass, :name
|
4
|
+
attr_reader :klass, :name, :trait, :type, :label
|
5
5
|
|
6
|
-
def initialize(klass, name, type, label: nil)
|
6
|
+
def initialize(klass, name, type, label: nil, trait: :default)
|
7
7
|
@name = name
|
8
8
|
@klass = klass
|
9
9
|
@type = type
|
10
10
|
@label = label || name
|
11
|
+
@trait = trait
|
11
12
|
end
|
12
13
|
|
13
14
|
def to_sql(outer_alias, aliaser)
|
@@ -18,43 +19,6 @@ module PgSerializable
|
|
18
19
|
@target ||= association.klass
|
19
20
|
end
|
20
21
|
|
21
|
-
private
|
22
|
-
|
23
|
-
def value(outer_alias, aliaser)
|
24
|
-
next_alias = aliaser.next!
|
25
|
-
self.send(@type, outer_alias, next_alias, aliaser)
|
26
|
-
end
|
27
|
-
|
28
|
-
def has_many(outer_alias, next_alias, aliaser)
|
29
|
-
return has_many_through(outer_alias, next_alias, aliaser) if association.through_reflection?
|
30
|
-
target.as_json_array(aliaser).where("#{next_alias}.#{primary_key}=#{outer_alias}.#{foreign_key}").to_sql
|
31
|
-
end
|
32
|
-
|
33
|
-
def has_many_through(outer_alias, next_alias, aliaser)
|
34
|
-
through = association.through_reflection
|
35
|
-
source = association.source_reflection
|
36
|
-
# NOTE: this will fail if the source table shares the same foreign key as your join table
|
37
|
-
# i.e. products and categories have a join table but categories has a product_id column
|
38
|
-
association
|
39
|
-
.klass
|
40
|
-
.select("#{source.table_name}.*, #{through.table_name}.#{source.join_foreign_key}, #{through.table_name}.#{through.join_primary_key}")
|
41
|
-
.joins(through.name)
|
42
|
-
.as_json_array(aliaser)
|
43
|
-
.where("#{next_alias}.#{through.join_primary_key}=#{outer_alias}.#{foreign_key}")
|
44
|
-
.to_sql
|
45
|
-
end
|
46
|
-
|
47
|
-
def has_one(outer_alias, next_alias, aliaser)
|
48
|
-
subquery_alias = "#{next_alias[0]}#{next_alias[1]}#{next_alias[0]}" # avoid alias collision
|
49
|
-
target.select("DISTINCT ON (#{primary_key}) #{subquery_alias}.*").from(
|
50
|
-
"#{target.table_name} #{subquery_alias}"
|
51
|
-
).as_json_object(aliaser).where("#{next_alias}.#{primary_key}=#{outer_alias}.#{foreign_key}").to_sql
|
52
|
-
end
|
53
|
-
|
54
|
-
def belongs_to(outer_alias, next_alias, aliaser)
|
55
|
-
target.as_json_object(aliaser).where("#{next_alias}.#{primary_key}=#{outer_alias}.#{foreign_key}").to_sql
|
56
|
-
end
|
57
|
-
|
58
22
|
def association
|
59
23
|
@association ||= @klass.reflect_on_association(@name)
|
60
24
|
end
|
@@ -1,25 +1,14 @@
|
|
1
1
|
module PgSerializable
|
2
2
|
module Nodes
|
3
3
|
class Attribute < Base
|
4
|
-
|
4
|
+
attr_reader :column_name, :klass, :label, :prc
|
5
|
+
|
6
|
+
def initialize(klass, column_name, label: nil, &prc)
|
7
|
+
@klass = klass
|
5
8
|
@column_name = column_name
|
6
9
|
@label = label || column_name
|
7
10
|
@prc = prc if block_given?
|
8
11
|
end
|
9
|
-
|
10
|
-
def to_sql(table_alias=nil)
|
11
|
-
[key, value(table_alias)].join(',')
|
12
|
-
end
|
13
|
-
|
14
|
-
private
|
15
|
-
def key
|
16
|
-
"\'#{@label}\'"
|
17
|
-
end
|
18
|
-
|
19
|
-
def value(tbl)
|
20
|
-
val = "#{tbl && "#{tbl}."}#{@column_name}"
|
21
|
-
@prc ? @prc.call(val) : val
|
22
|
-
end
|
23
12
|
end
|
24
13
|
end
|
25
14
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module PgSerializable
|
2
|
+
class Trait
|
3
|
+
attr_reader :klass, :attribute_nodes
|
4
|
+
|
5
|
+
def initialize(klass)
|
6
|
+
@klass = klass
|
7
|
+
@attribute_nodes = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def attributes(*attrs)
|
11
|
+
attrs.each do |attribute|
|
12
|
+
@attribute_nodes << Nodes::Attribute.new(klass, attribute)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def attribute(column_name, label: nil, &blk)
|
17
|
+
@attribute_nodes << Nodes::Attribute.new(klass, column_name, label: label, &blk)
|
18
|
+
end
|
19
|
+
|
20
|
+
def has_many(association, label: nil, trait: :default)
|
21
|
+
@attribute_nodes << Nodes::Association.new(klass, association, :has_many, label: label, trait: trait)
|
22
|
+
end
|
23
|
+
|
24
|
+
def belongs_to(association, label: nil, trait: :default)
|
25
|
+
@attribute_nodes << Nodes::Association.new(klass, association, :belongs_to, label: label, trait: trait)
|
26
|
+
end
|
27
|
+
|
28
|
+
def has_one(association, label: nil, trait: :default)
|
29
|
+
@attribute_nodes << Nodes::Association.new(klass, association, :has_one, label: label, trait: trait)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module PgSerializable
|
2
|
+
class TraitManager
|
3
|
+
include PgSerializable::Visitable
|
4
|
+
|
5
|
+
attr_reader :klass, :traits
|
6
|
+
|
7
|
+
def initialize(klass)
|
8
|
+
@klass = klass
|
9
|
+
@traits = ActiveSupport::HashWithIndifferentAccess.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def default(&blk)
|
13
|
+
default_trait = PgSerializable::Trait.new(klass)
|
14
|
+
default_trait.instance_eval &blk
|
15
|
+
@traits[:default] = default_trait
|
16
|
+
end
|
17
|
+
|
18
|
+
def trait(trait_name, &blk)
|
19
|
+
trait_instance = PgSerializable::Trait.new(klass)
|
20
|
+
trait_instance.instance_eval &blk
|
21
|
+
@traits[trait_name] = trait_instance
|
22
|
+
end
|
23
|
+
|
24
|
+
def get_trait(trait)
|
25
|
+
@traits[trait]
|
26
|
+
end
|
27
|
+
|
28
|
+
def validate_traits!
|
29
|
+
accept(PgSerializable::Visitors::Validation.new)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module PgSerializable
|
2
|
+
module Visitors
|
3
|
+
class Base
|
4
|
+
def visit(subject, **kwargs)
|
5
|
+
send(visit_method_for(subject), subject, **kwargs)
|
6
|
+
end
|
7
|
+
|
8
|
+
def visit_method_for(subject)
|
9
|
+
case subject
|
10
|
+
when record? then :visit_record
|
11
|
+
when class? then :visit_class
|
12
|
+
when relation? then :visit_relation
|
13
|
+
when trait_manager? then :visit_trait_manager
|
14
|
+
when trait? then :visit_trait
|
15
|
+
when node? then :visit_node
|
16
|
+
else :visit_other
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# activerecord
|
21
|
+
|
22
|
+
def visit_record(subject, **kwargs)
|
23
|
+
raise NotImplementedError.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def visit_class(subject, **kwargs)
|
27
|
+
raise NotImplementedError.new
|
28
|
+
end
|
29
|
+
|
30
|
+
def visit_scope(subject, **kwargs)
|
31
|
+
raise NotImplementedError.new
|
32
|
+
end
|
33
|
+
|
34
|
+
# pg_serializable
|
35
|
+
|
36
|
+
def visit_trait_manager(subject, **kwargs)
|
37
|
+
raise NotImplementedError.new
|
38
|
+
end
|
39
|
+
|
40
|
+
def visit_trait(subject, **kwargs)
|
41
|
+
raise NotImplementedError.new
|
42
|
+
end
|
43
|
+
|
44
|
+
def visit_node(subject, **kwargs)
|
45
|
+
raise NotImplementedError.new
|
46
|
+
end
|
47
|
+
|
48
|
+
# everything else
|
49
|
+
|
50
|
+
def visit_other(subject, **kwargs)
|
51
|
+
raise NotImplementedError.new
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def record?
|
57
|
+
->(x) { x.is_a? ApplicationRecord }
|
58
|
+
end
|
59
|
+
|
60
|
+
def class?
|
61
|
+
->(x) { x.is_a? Class }
|
62
|
+
end
|
63
|
+
|
64
|
+
def relation?
|
65
|
+
->(x) { x.is_a? ActiveRecord::Relation }
|
66
|
+
end
|
67
|
+
|
68
|
+
def trait_manager?
|
69
|
+
->(x) { x.is_a? PgSerializable::TraitManager }
|
70
|
+
end
|
71
|
+
|
72
|
+
def trait?
|
73
|
+
->(x) { x.is_a? PgSerializable::Trait }
|
74
|
+
end
|
75
|
+
|
76
|
+
def node?
|
77
|
+
->(x) { x.is_a? PgSerializable::Nodes::Base }
|
78
|
+
end
|
79
|
+
|
80
|
+
def attribute?
|
81
|
+
->(x) { x.is_a? PgSerializable::Nodes::Attribute }
|
82
|
+
end
|
83
|
+
|
84
|
+
def association?
|
85
|
+
->(x) { x.is_a? PgSerializable::Nodes::Association }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
module PgSerializable
|
2
|
+
module Visitors
|
3
|
+
class Json < Base
|
4
|
+
def visit_record(subject, trait: :default)
|
5
|
+
table_alias = next_alias!
|
6
|
+
|
7
|
+
klass = subject.class
|
8
|
+
select_sql = json_build_object(visit(klass.trait_manager, trait: trait, table_alias: table_alias)).to_sql
|
9
|
+
from_sql = klass.where(id: subject.id).limit(1).to_sql
|
10
|
+
|
11
|
+
klass.select(select_sql).from("#{as(from_sql, table_alias)}")
|
12
|
+
end
|
13
|
+
|
14
|
+
def visit_relation(subject, table_alias: nil, trait: :default)
|
15
|
+
table_alias ||= next_alias!
|
16
|
+
|
17
|
+
klass = subject.klass
|
18
|
+
select_sql = coalesce(json_agg(json_build_object(visit(klass.trait_manager, trait: trait, table_alias: table_alias)))).to_sql
|
19
|
+
from_sql = subject.to_sql
|
20
|
+
|
21
|
+
klass.select(select_sql).from("#{as(from_sql, table_alias)}")
|
22
|
+
end
|
23
|
+
|
24
|
+
def visit_class(subject, trait: :default, **kwargs)
|
25
|
+
visit(subject.all, trait: trait, **kwargs)
|
26
|
+
end
|
27
|
+
|
28
|
+
def visit_trait_manager(subject, trait: :default, **kwargs)
|
29
|
+
visit subject.get_trait(trait), **kwargs
|
30
|
+
end
|
31
|
+
|
32
|
+
def visit_trait(subject, **kwargs)
|
33
|
+
subject.attribute_nodes.map { |attribute| visit attribute, **kwargs }.join(', ')
|
34
|
+
end
|
35
|
+
|
36
|
+
def visit_node(subject, **kwargs)
|
37
|
+
case subject
|
38
|
+
when attribute? then visit_attribute subject, **kwargs
|
39
|
+
when association? then visit_association subject, **kwargs
|
40
|
+
else raise UnknownAttributeError.new
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def visit_attribute(subject, table_alias: nil)
|
45
|
+
table_alias ||= alias_tracker
|
46
|
+
key = "\'#{subject.label}\'"
|
47
|
+
val = "\"#{table_alias}\".\"#{subject.prc ? subject.prc.call(column_name) : subject.column_name}\""
|
48
|
+
"#{key}, #{val}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def visit_association(subject, **kwargs)
|
52
|
+
send("visit_#{subject.type}", subject, **kwargs)
|
53
|
+
end
|
54
|
+
|
55
|
+
def visit_belongs_to(subject, table_alias:, **kwargs)
|
56
|
+
current_alias = next_alias!
|
57
|
+
klass = subject.target
|
58
|
+
select_sql = json_build_object(visit(klass.trait_manager, trait: subject.trait, table_alias: current_alias)).to_sql
|
59
|
+
query = klass.select(select_sql).from("#{klass.table_name} #{current_alias}")
|
60
|
+
.where("#{current_alias}.#{subject.primary_key}=#{table_alias}.#{subject.foreign_key}").to_sql
|
61
|
+
"\'#{subject.label}\', (#{query})"
|
62
|
+
end
|
63
|
+
|
64
|
+
def visit_has_many(subject, table_alias:, **kwargs)
|
65
|
+
return visit_has_many_through(subject, table_alias: table_alias, **kwargs) if subject.association.through_reflection?
|
66
|
+
|
67
|
+
current_alias = next_alias!
|
68
|
+
klass = subject.target
|
69
|
+
select_sql = coalesce(json_agg(json_build_object(visit(klass.trait_manager, trait: subject.trait, table_alias: current_alias)))).to_sql
|
70
|
+
|
71
|
+
query = klass.select(select_sql).from("#{klass.table_name} #{current_alias}")
|
72
|
+
.where("#{current_alias}.#{subject.primary_key}=#{table_alias}.#{subject.foreign_key}").to_sql
|
73
|
+
|
74
|
+
"\'#{subject.label}\', (#{query})"
|
75
|
+
end
|
76
|
+
|
77
|
+
def visit_has_many_through(subject, table_alias:, **kwargs)
|
78
|
+
current_alias = next_alias!
|
79
|
+
|
80
|
+
association = subject.association
|
81
|
+
through = association.through_reflection
|
82
|
+
source = association.source_reflection
|
83
|
+
|
84
|
+
query = visit(association
|
85
|
+
.klass
|
86
|
+
.select("#{source.table_name}.*, #{through.table_name}.#{source.join_foreign_key}, #{through.table_name}.#{through.join_primary_key}")
|
87
|
+
.joins(through.name), table_alias: current_alias)
|
88
|
+
.where("#{current_alias}.#{through.join_primary_key}=#{table_alias}.#{subject.foreign_key}")
|
89
|
+
.to_sql
|
90
|
+
|
91
|
+
"\'#{subject.label}\', (#{query})"
|
92
|
+
end
|
93
|
+
|
94
|
+
def visit_has_one(subject, table_alias:, **kwargs)
|
95
|
+
current_alias = next_alias!
|
96
|
+
subquery_alias = next_alias!
|
97
|
+
klass = subject.target
|
98
|
+
|
99
|
+
select_sql = json_build_object(visit(klass.trait_manager, trait: subject.trait, table_alias: current_alias)).to_sql
|
100
|
+
from_sql = klass
|
101
|
+
.select("DISTINCT ON (#{subject.primary_key}) #{subquery_alias}.*")
|
102
|
+
.from("#{klass.table_name} #{subquery_alias}")
|
103
|
+
|
104
|
+
query = klass.select(select_sql).from("#{as(from_sql, current_alias)}")
|
105
|
+
.where("#{current_alias}.#{subject.primary_key}=#{table_alias}.#{subject.foreign_key}").to_sql
|
106
|
+
|
107
|
+
"\'#{subject.label}\', (#{query})"
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def alias_tracker
|
113
|
+
@alias ||= 'a0'
|
114
|
+
end
|
115
|
+
|
116
|
+
def next_alias!
|
117
|
+
alias_tracker.next!.dup
|
118
|
+
end
|
119
|
+
|
120
|
+
def as(sql, table_alias)
|
121
|
+
PgSerializable::Nodes::As.new(sql, table_alias)
|
122
|
+
end
|
123
|
+
|
124
|
+
def json_build_object(sql)
|
125
|
+
PgSerializable::Nodes::JsonBuildObject.new(sql)
|
126
|
+
end
|
127
|
+
|
128
|
+
def json_agg(sql)
|
129
|
+
PgSerializable::Nodes::JsonAgg.new(sql)
|
130
|
+
end
|
131
|
+
|
132
|
+
def coalesce(sql)
|
133
|
+
PgSerializable::Nodes::Coalesce.new(sql, PgSerializable::Nodes::JsonArray.new)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module PgSerializable
|
2
|
+
module Visitors
|
3
|
+
class Validation < Base
|
4
|
+
def visit_class(subject, **kwargs)
|
5
|
+
visit subject.trait_manager
|
6
|
+
end
|
7
|
+
|
8
|
+
def visit_trait_manager(subject, **kwargs)
|
9
|
+
subject.traits.each do |_, value|
|
10
|
+
visit value
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def visit_trait(subject, **kwargs)
|
15
|
+
subject.attribute_nodes.each { |attribute_node| visit attribute_node }
|
16
|
+
ensure_no_cycles!(subject)
|
17
|
+
end
|
18
|
+
|
19
|
+
def visit_node(subject, **kwargs)
|
20
|
+
if subject.is_a? PgSerializable::Nodes::Attribute
|
21
|
+
visit_attribute_node(subject, **kwargs)
|
22
|
+
elsif subject.is_a? PgSerializable::Nodes::Association
|
23
|
+
visit_association_node(subject, **kwargs)
|
24
|
+
else
|
25
|
+
raise UnknownAttributeError.new('validation visitor called with unknow node type')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def visit_attribute_node(subject, **kwargs)
|
30
|
+
klass = subject.klass
|
31
|
+
column_name = subject.column_name
|
32
|
+
unless klass.column_names.include? column_name.to_s
|
33
|
+
raise PgSerializable::AttributeError.new("column `#{column_name}` doesn't exist for class #{klass}")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def visit_association_node(subject, **kwargs)
|
38
|
+
klass = subject.klass
|
39
|
+
name = subject.name
|
40
|
+
if klass.reflect_on_association(name).nil?
|
41
|
+
raise PgSerializable::AssociationError.new("association `#{name.to_s}` doesn't exist for class #{klass}")
|
42
|
+
end
|
43
|
+
if subject.target.trait_manager.get_trait(subject.trait).nil?
|
44
|
+
raise PgSerializable::AssociationError.new("trait `#{subect.trait}` doesn't exist for class #{subject.target}")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def ensure_no_cycles!(trait)
|
51
|
+
@root_klass = trait.klass
|
52
|
+
check_for_cycles(trait)
|
53
|
+
end
|
54
|
+
|
55
|
+
def check_for_cycles(subject)
|
56
|
+
case subject
|
57
|
+
when trait? then subject.attribute_nodes.each { |node| check_for_cycles(node) }
|
58
|
+
when association?
|
59
|
+
if subject.target == @root_klass
|
60
|
+
raise PgSerializable::AssociationError.new("class #{@root_klass} contains a cycle in nested association #{subject.klass}")
|
61
|
+
end
|
62
|
+
associated_trait = subject.target.trait_manager.get_trait subject.trait
|
63
|
+
check_for_cycles(associated_trait)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/pg_serializable.gemspec
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pg_serializable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- matthewjf
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-10-
|
11
|
+
date: 2018-10-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '5.2'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: oj
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.6'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.6'
|
83
97
|
description: serializes rails models from postgres (9.5+)
|
84
98
|
email:
|
85
99
|
- matthewjf@gmail.com
|
@@ -92,13 +106,13 @@ files:
|
|
92
106
|
- ".travis.yml"
|
93
107
|
- CODE_OF_CONDUCT.md
|
94
108
|
- Gemfile
|
109
|
+
- Gemfile.lock
|
95
110
|
- LICENSE.txt
|
96
111
|
- README.md
|
97
112
|
- Rakefile
|
98
113
|
- bin/console
|
99
114
|
- bin/setup
|
100
115
|
- lib/pg_serializable.rb
|
101
|
-
- lib/pg_serializable/aliaser.rb
|
102
116
|
- lib/pg_serializable/errors.rb
|
103
117
|
- lib/pg_serializable/nodes.rb
|
104
118
|
- lib/pg_serializable/nodes/as.rb
|
@@ -109,8 +123,14 @@ files:
|
|
109
123
|
- lib/pg_serializable/nodes/json_agg.rb
|
110
124
|
- lib/pg_serializable/nodes/json_array.rb
|
111
125
|
- lib/pg_serializable/nodes/json_build_object.rb
|
112
|
-
- lib/pg_serializable/
|
126
|
+
- lib/pg_serializable/trait.rb
|
127
|
+
- lib/pg_serializable/trait_manager.rb
|
113
128
|
- lib/pg_serializable/version.rb
|
129
|
+
- lib/pg_serializable/visitable.rb
|
130
|
+
- lib/pg_serializable/visitors.rb
|
131
|
+
- lib/pg_serializable/visitors/base.rb
|
132
|
+
- lib/pg_serializable/visitors/json.rb
|
133
|
+
- lib/pg_serializable/visitors/validation.rb
|
114
134
|
- pg_serializable.gemspec
|
115
135
|
homepage: https://github.com/matthewjf/pg_serializable
|
116
136
|
licenses:
|
@@ -1,102 +0,0 @@
|
|
1
|
-
module PgSerializable
|
2
|
-
class Serializer
|
3
|
-
attr_reader :klass
|
4
|
-
|
5
|
-
def initialize(klass)
|
6
|
-
@klass = klass
|
7
|
-
@attributes = []
|
8
|
-
end
|
9
|
-
|
10
|
-
def attributes(*attrs)
|
11
|
-
attrs.each do |attribute|
|
12
|
-
@attributes << Nodes::Attribute.new(attribute) if column_exists?(attribute)
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
def attribute(column_name, label: nil, &blk)
|
17
|
-
@attributes << Nodes::Attribute.new(column_name, label: label, &blk) if column_exists?(column_name)
|
18
|
-
end
|
19
|
-
|
20
|
-
def has_many(association, label: nil)
|
21
|
-
@attributes << Nodes::Association.new(klass, association, :has_many, label: label) if association_exists?(association)
|
22
|
-
end
|
23
|
-
|
24
|
-
def belongs_to(association, label: nil)
|
25
|
-
@attributes << Nodes::Association.new(klass, association, :belongs_to, label: label) if association_exists?(association)
|
26
|
-
end
|
27
|
-
|
28
|
-
def has_one(association, label: nil)
|
29
|
-
@attributes << Nodes::Association.new(klass, association, :has_one, label: label) if association_exists?(association)
|
30
|
-
end
|
31
|
-
|
32
|
-
def as_json_array(skope, aliaser)
|
33
|
-
@aliaser = aliaser
|
34
|
-
@table_alias = @aliaser.to_s
|
35
|
-
query(json_agg.to_sql, skope)
|
36
|
-
end
|
37
|
-
|
38
|
-
def as_json_object(skope, aliaser)
|
39
|
-
@aliaser = aliaser
|
40
|
-
@table_alias = @aliaser.to_s
|
41
|
-
query(json_build_object.to_sql, skope)
|
42
|
-
end
|
43
|
-
|
44
|
-
def check_for_cycles!(klass=self.klass)
|
45
|
-
@attributes.each do |attribute|
|
46
|
-
next unless attribute.is_a?(Nodes::Association)
|
47
|
-
target_klass = attribute.target
|
48
|
-
raise AssociationError.new("serializers contain a cycle, check class `#{attribute.klass}` for association `:#{attribute.name}`") if target_klass == klass
|
49
|
-
target_klass.serializer.check_for_cycles!(self.klass)
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
private
|
54
|
-
|
55
|
-
def query(select_sql, from_scope)
|
56
|
-
klass
|
57
|
-
.unscoped
|
58
|
-
.select(select_sql)
|
59
|
-
.from(Nodes::As.new(from_scope, @table_alias).to_sql)
|
60
|
-
end
|
61
|
-
|
62
|
-
def build_attributes
|
63
|
-
res = []
|
64
|
-
@attributes.each do |attribute|
|
65
|
-
if attribute.is_a?(Nodes::Attribute)
|
66
|
-
res << attribute.to_sql(@table_alias)
|
67
|
-
elsif attribute.is_a?(Nodes::Association)
|
68
|
-
res << attribute.to_sql(@table_alias, @aliaser)
|
69
|
-
else
|
70
|
-
raise 'unknown attribute type'
|
71
|
-
end
|
72
|
-
end
|
73
|
-
res.join(',')
|
74
|
-
end
|
75
|
-
|
76
|
-
def json_build_object
|
77
|
-
Nodes::JsonBuildObject.new(build_attributes)
|
78
|
-
end
|
79
|
-
|
80
|
-
def json_agg
|
81
|
-
Nodes::Coalesce.new(Nodes::JsonAgg.new(json_build_object), Nodes::JsonArray.new)
|
82
|
-
end
|
83
|
-
|
84
|
-
def table_alias
|
85
|
-
@table_alias
|
86
|
-
end
|
87
|
-
|
88
|
-
def association(name)
|
89
|
-
klass.reflect_on_association(name)
|
90
|
-
end
|
91
|
-
|
92
|
-
def column_exists?(column_name)
|
93
|
-
raise AttributeError.new("`#{column_name.to_s}` column doesn't exist for table #{klass.table_name}") unless klass.column_names.include? column_name.to_s
|
94
|
-
true
|
95
|
-
end
|
96
|
-
|
97
|
-
def association_exists?(name)
|
98
|
-
raise AssociationError.new("`#{name.to_s}` association doesn't exist for table #{klass.table_name}") if klass.reflect_on_association(name).nil?
|
99
|
-
true
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|