pg_serializable 0.1.1 → 1.0.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 +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
|