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 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