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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 618fc576fe1ec96d02b3b6745406aaa0921297a3
4
- data.tar.gz: 10fcb8a2866501c48f4126cbbb907f76c64c3b26
3
+ metadata.gz: fc026e950ca7cd42a452b17f7ea42e69513298ff
4
+ data.tar.gz: de6db447af2b2ba0f845dffeb29efd02c130089a
5
5
  SHA512:
6
- metadata.gz: c252b339e93bb7e0f544e3bdc80639086a6c2378df48a1d6c7fbf8646c45774048bd0aae5f6177f9485fd7235c7bdedb3ee9c6cd785903b1d044960188ade606
7
- data.tar.gz: 0f4d3bd51fcb1255f54cd206477eba8eeeeaddc1bc33194fb5538406233a291d499990f5854f2833fa7599eeff5fe6e694c52c706701c6ec73f39d388bb433d2
6
+ metadata.gz: 11102ebb57aa4da829575d9b7bc70d94a9fcf670175aaaa9089868d3df011c11f23e63f5e4c12ef85276532e1b76f2aaec1a0f004ee0e3a3052cb140c9a700be
7
+ data.tar.gz: 0ea08435b36bda86588c0ed4c771e3e879c5d4ababd309b0c8f0969b6c269c25d2f001bdc16f33b80b40424376a33955d03438c13c57da98fda19a837a2837d8
@@ -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
- attributes :name, :id
34
- attribute :name, label: :test_name
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
- attributes :id
91
- attribute :active, label: :deleted { |v| "NOT #{v}" }
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": true
166
+ "deleted": false
99
167
  },
100
168
  {
101
169
  "id": 502,
102
- "deleted": true
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
- attributes :id, :name
119
- belongs_to: :label
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
- attributes :id, :name
147
- has_many: :variations
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
- attributes :id, :hex
154
- belongs_to: :color
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
- attributes :id, :hex
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
- attributes :id
219
- has_many :categories
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
- attributes :name, :id
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
@@ -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/aliaser'
4
+ require 'pg_serializable/visitable'
6
5
  require 'pg_serializable/nodes'
7
- require 'pg_serializable/serializer'
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
- def json
13
- ActiveRecord::Base.connection.select_one(
14
- self.class.where(id: id).limit(1).as_json_object.to_sql
15
- )['json_build_object']
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
- ActiveRecord::Base.connection.select_one(
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 as_json_array(table_alias = Aliaser.new)
27
- serializer.as_json_array(pg_scope, table_alias)
36
+ def serializable(&blk)
37
+ trait_manager.instance_eval &blk
38
+ validate_traits!
28
39
  end
29
40
 
30
- def as_json_object(table_alias = Aliaser.new)
31
- serializer.as_json_object(pg_scope, table_alias)
41
+ def trait_manager
42
+ @trait_manager ||= TraitManager.new(self)
32
43
  end
33
44
 
34
- def serializable(&blk)
35
- serializer.instance_eval &blk
36
- serializer.check_for_cycles!
45
+ def accept visitor, **kwargs
46
+ visitor.visit self, **kwargs
37
47
  end
38
48
 
39
- def serializer
40
- @serializer ||= Serializer.new(self)
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
- def pg_scope
44
- respond_to?(:to_sql) ? self : all
45
- end
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 Serializer
3
- class AttributeError < ::StandardError;end
4
- class AssociationError < ::StandardError;end
5
- end
2
+ class AttributeError < ::StandardError;end
3
+ class AssociationError < ::StandardError;end
4
+ class UnknownAttributeError < ::StandardError;end
6
5
  end
@@ -6,3 +6,8 @@ require 'pg_serializable/nodes/coalesce'
6
6
  require 'pg_serializable/nodes/json_agg'
7
7
  require 'pg_serializable/nodes/json_array'
8
8
  require 'pg_serializable/nodes/json_build_object'
9
+
10
+ module PgSerializable
11
+ module Nodes
12
+ end
13
+ 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
- def initialize(column_name, label: nil, &prc)
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
@@ -1,3 +1,3 @@
1
1
  module PgSerializable
2
- VERSION = "0.1.1"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -0,0 +1,7 @@
1
+ module PgSerializable
2
+ module Visitable
3
+ def accept visitor, **kwargs
4
+ visitor.visit self, **kwargs
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ require 'pg_serializable/visitors/base'
2
+ require 'pg_serializable/visitors/validation'
3
+ require 'pg_serializable/visitors/json'
4
+
5
+ module PgSerializable
6
+ module Visitors
7
+ end
8
+ 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
@@ -38,4 +38,5 @@ Gem::Specification.new do |spec|
38
38
 
39
39
  spec.add_runtime_dependency "activesupport", "~> 5.2"
40
40
  spec.add_runtime_dependency "activerecord", "~> 5.2"
41
+ spec.add_runtime_dependency "oj", "~> 3.6"
41
42
  end
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.1.1
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-03 00:00:00.000000000 Z
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/serializer.rb
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,15 +0,0 @@
1
- module PgSerializable
2
- class Aliaser
3
- def initialize(current=nil)
4
- @current = current || 'a0'
5
- end
6
-
7
- def next!
8
- @current = @current.next
9
- end
10
-
11
- def to_s
12
- @current
13
- end
14
- end
15
- end
@@ -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