turbojson 0.1.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 +7 -0
- data/README.md +121 -0
- data/lib/turbojson/ast.rb +34 -0
- data/lib/turbojson/post_processor.rb +26 -0
- data/lib/turbojson/schema_inspector.rb +22 -0
- data/lib/turbojson/serializer.rb +141 -0
- data/lib/turbojson/sql_generator.rb +94 -0
- data/lib/turbojson/version.rb +5 -0
- data/lib/turbojson.rb +10 -0
- metadata +58 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9ceaa05c135c84cc26f2b51912b98901841146dee317aa371c81ed752de1b1cf
|
|
4
|
+
data.tar.gz: 2c3d2b7af3d7b3bd7e270401c49ec5eff2f590a7a7bad17cac895e6d6e287dd4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 639625b06b99467f6714620a7a9bfe2bb4b1667b706834bfd56c9950a6fb8c56a1d0709fa8de95616e2bb716f6d705a38bf5719e1d06b97e0c0aecf7d7a805e7
|
|
7
|
+
data.tar.gz: 284b34ac41fedb2f1b37b547efda528a644bdb91c9a40b4534161f7eac66a7ebd2def8602080a1273aa6e2836cb90fceea97b827e8a7170567ff6784023350f7
|
data/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# turbojson
|
|
2
|
+
|
|
3
|
+
Compile a Ruby serializer DSL into fast PostgreSQL JSON SQL for read-heavy APIs. Define attributes and associations in Ruby, then generate SQL that returns JSON without instantiating thousands of ActiveRecord objects.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
ActiveRecord is excellent for writes and business logic, but JSON APIs are often read-heavy. Hydrating large object graphs just to serialize them is slow and memory-intensive. `turbojson` lets you keep a familiar serializer DSL while shifting JSON construction to PostgreSQL (`json_build_object`, `json_agg`).
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
Add to your Gemfile:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem "turbojson"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or use a local path during development:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem "turbojson", path: "/path/to/turbojson"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bundle install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
Define serializers:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
class CategorySerializer < Turbojson::Serializer
|
|
35
|
+
model Category
|
|
36
|
+
attributes :id, :name
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class BarPhotoSerializer < Turbojson::Serializer
|
|
40
|
+
model BarPhoto
|
|
41
|
+
attributes :id, :url
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class BarSerializer < Turbojson::Serializer
|
|
45
|
+
model Bar
|
|
46
|
+
attributes :id, :name
|
|
47
|
+
|
|
48
|
+
has_one :category, serializer: CategorySerializer
|
|
49
|
+
has_many :bar_photos, serializer: BarPhotoSerializer
|
|
50
|
+
|
|
51
|
+
attribute :distance do |scope|
|
|
52
|
+
"ST_Distance(#{scope.table_name}.location, 'POINT(...)')"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Serialize an ActiveRecord scope:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
scope = Bar.where(active: true).limit(10)
|
|
61
|
+
json = BarSerializer.serialize(scope)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Generate SQL directly:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
sql = BarSerializer.to_sql
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## How It Works
|
|
71
|
+
|
|
72
|
+
1. DSL definitions build an AST describing the model, attributes, and associations.
|
|
73
|
+
2. The SQL generator compiles the AST into `json_build_object` and `json_agg` SQL.
|
|
74
|
+
3. `serialize(scope)` wraps the generated SQL around your existing relation to respect `where`, `limit`, and `offset`.
|
|
75
|
+
4. The database returns JSON; the gem parses it to Ruby hashes and arrays.
|
|
76
|
+
|
|
77
|
+
## Strict Column Validation
|
|
78
|
+
|
|
79
|
+
By default, attributes must be real columns. If a name is not a column, a `Turbojson::ConfigurationError` is raised. Column checks use `schema_cache` when available, then fall back to `columns_hash`.
|
|
80
|
+
|
|
81
|
+
## Active Storage Key Post-Processing
|
|
82
|
+
|
|
83
|
+
If you need signed URLs, fetch the key in SQL and post-process in Ruby:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
payload = BarSerializer.serialize(scope)
|
|
87
|
+
|
|
88
|
+
signed = Turbojson::PostProcessor.process(payload) do |key|
|
|
89
|
+
blob = ActiveStorage::Blob.find_by!(key: key)
|
|
90
|
+
Rails.application.routes.url_helpers.rails_blob_url(blob, only_path: true)
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Performance Notes
|
|
95
|
+
|
|
96
|
+
- Uses `LATERAL` joins for associations to avoid correlated subquery hotspots.
|
|
97
|
+
- Avoids instantiating ActiveRecord objects for read-heavy endpoints.
|
|
98
|
+
|
|
99
|
+
## Testing
|
|
100
|
+
|
|
101
|
+
Run the test suite:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
bundle exec rspec
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
SQL snapshot tests live under `spec/fixtures/sql/`.
|
|
108
|
+
|
|
109
|
+
## Roadmap
|
|
110
|
+
|
|
111
|
+
- Add ActiveModel::Serializer parity specs with a real test database.
|
|
112
|
+
- Expand SQL generation to handle advanced filters and custom join strategies.
|
|
113
|
+
- Add Benchmark.ips and memory benchmarks under `bench/`.
|
|
114
|
+
|
|
115
|
+
## Contributing
|
|
116
|
+
|
|
117
|
+
See `AGENTS.md` for repository guidelines, structure, and development notes.
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Turbojson
|
|
4
|
+
module Ast
|
|
5
|
+
class Root
|
|
6
|
+
attr_reader :model, :table_name, :nodes
|
|
7
|
+
|
|
8
|
+
def initialize(model:, table_name:, nodes:)
|
|
9
|
+
@model = model
|
|
10
|
+
@table_name = table_name
|
|
11
|
+
@nodes = nodes
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class Attribute
|
|
16
|
+
attr_reader :name, :sql
|
|
17
|
+
|
|
18
|
+
def initialize(name:, sql: nil)
|
|
19
|
+
@name = name
|
|
20
|
+
@sql = sql
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class Association
|
|
25
|
+
attr_reader :name, :type, :serializer_class
|
|
26
|
+
|
|
27
|
+
def initialize(name:, type:, serializer_class:)
|
|
28
|
+
@name = name
|
|
29
|
+
@type = type
|
|
30
|
+
@serializer_class = serializer_class
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Turbojson
|
|
4
|
+
class PostProcessor
|
|
5
|
+
def self.process(payload, key_suffix: "_key", &block)
|
|
6
|
+
case payload
|
|
7
|
+
when Array
|
|
8
|
+
payload.map { |item| process(item, key_suffix: key_suffix, &block) }
|
|
9
|
+
when Hash
|
|
10
|
+
payload.each_with_object({}) do |(key, value), acc|
|
|
11
|
+
acc[key] = transform_value(key, value, key_suffix, &block)
|
|
12
|
+
end
|
|
13
|
+
else
|
|
14
|
+
payload
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.transform_value(key, value, key_suffix, &block)
|
|
19
|
+
if key.to_s.end_with?(key_suffix) && block
|
|
20
|
+
block.call(value)
|
|
21
|
+
else
|
|
22
|
+
process(value, key_suffix: key_suffix, &block)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Turbojson
|
|
4
|
+
class SchemaInspector
|
|
5
|
+
def self.column?(model_class, name)
|
|
6
|
+
table = model_class.respond_to?(:table_name) ? model_class.table_name : nil
|
|
7
|
+
|
|
8
|
+
if model_class.respond_to?(:connection)
|
|
9
|
+
cache = model_class.connection.schema_cache if model_class.connection.respond_to?(:schema_cache)
|
|
10
|
+
if cache && cache.respond_to?(:columns_hash) && table
|
|
11
|
+
return cache.columns_hash(table).key?(name.to_s)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
if model_class.respond_to?(:columns_hash)
|
|
16
|
+
return model_class.columns_hash.key?(name.to_s)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
false
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "ostruct"
|
|
5
|
+
require_relative "ast"
|
|
6
|
+
require_relative "schema_inspector"
|
|
7
|
+
|
|
8
|
+
module Turbojson
|
|
9
|
+
class ConfigurationError < Error; end
|
|
10
|
+
|
|
11
|
+
class Serializer
|
|
12
|
+
class << self
|
|
13
|
+
def model(klass = nil)
|
|
14
|
+
if klass
|
|
15
|
+
config[:model] = klass
|
|
16
|
+
else
|
|
17
|
+
config[:model]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def attributes(*names, &block)
|
|
22
|
+
raise ArgumentError, "attributes requires at least one name" if names.empty?
|
|
23
|
+
|
|
24
|
+
names.each do |name|
|
|
25
|
+
config[:attributes] << {
|
|
26
|
+
name: name.to_sym,
|
|
27
|
+
sql: block&.call(model_scope)
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def has_one(name, serializer:)
|
|
33
|
+
config[:associations] << {
|
|
34
|
+
name: name.to_sym,
|
|
35
|
+
type: :has_one,
|
|
36
|
+
serializer: serializer
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def has_many(name, serializer:)
|
|
41
|
+
config[:associations] << {
|
|
42
|
+
name: name.to_sym,
|
|
43
|
+
type: :has_many,
|
|
44
|
+
serializer: serializer
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def ast
|
|
49
|
+
model_class = config[:model]
|
|
50
|
+
raise ConfigurationError, "Serializer missing model" unless model_class
|
|
51
|
+
|
|
52
|
+
table_name = infer_table_name(model_class)
|
|
53
|
+
nodes = []
|
|
54
|
+
|
|
55
|
+
config[:attributes].each do |attr|
|
|
56
|
+
validate_column!(model_class, attr[:name]) if attr[:sql].nil?
|
|
57
|
+
nodes << Ast::Attribute.new(name: attr[:name], sql: attr[:sql])
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
config[:associations].each do |assoc|
|
|
61
|
+
nodes << Ast::Association.new(
|
|
62
|
+
name: assoc[:name],
|
|
63
|
+
type: assoc[:type],
|
|
64
|
+
serializer_class: assoc[:serializer]
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
Ast::Root.new(model: model_class, table_name: table_name, nodes: nodes)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def to_sql
|
|
72
|
+
SqlGenerator.new(ast).to_sql
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def serialize(scope)
|
|
76
|
+
raise ArgumentError, "scope is required" if scope.nil?
|
|
77
|
+
raise ConfigurationError, "Arel is required to build SQL" unless defined?(Arel)
|
|
78
|
+
|
|
79
|
+
generator = SqlGenerator.new(ast)
|
|
80
|
+
object_sql, joins = generator.select_parts
|
|
81
|
+
|
|
82
|
+
unless scope.respond_to?(:select) && scope.respond_to?(:joins) && scope.respond_to?(:to_sql)
|
|
83
|
+
raise ConfigurationError, "serialize expects an ActiveRecord::Relation-like scope"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
relation = scope.select(Arel.sql("#{object_sql} AS data"))
|
|
87
|
+
joins.each do |join_sql|
|
|
88
|
+
relation = relation.joins(Arel.sql(join_sql))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
sql = <<~SQL
|
|
92
|
+
SELECT COALESCE(json_agg(rows.data), '[]'::json) AS data
|
|
93
|
+
FROM (#{relation.to_sql}) rows
|
|
94
|
+
SQL
|
|
95
|
+
|
|
96
|
+
connection = scope.connection
|
|
97
|
+
result = connection.select_value(sql)
|
|
98
|
+
|
|
99
|
+
return result unless result.is_a?(String)
|
|
100
|
+
|
|
101
|
+
JSON.parse(result)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def model_scope
|
|
105
|
+
model_class = config[:model]
|
|
106
|
+
return nil unless model_class
|
|
107
|
+
|
|
108
|
+
if model_class.respond_to?(:table_name)
|
|
109
|
+
OpenStruct.new(table_name: model_class.table_name)
|
|
110
|
+
else
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def config
|
|
118
|
+
@config ||= begin
|
|
119
|
+
parent = superclass.respond_to?(:config) ? superclass.send(:config) : {}
|
|
120
|
+
{
|
|
121
|
+
model: parent[:model],
|
|
122
|
+
attributes: Array(parent[:attributes]).dup,
|
|
123
|
+
associations: Array(parent[:associations]).dup
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def infer_table_name(model_class)
|
|
129
|
+
return model_class.table_name if model_class.respond_to?(:table_name)
|
|
130
|
+
|
|
131
|
+
raise ConfigurationError, "Model must respond to table_name"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def validate_column!(model_class, name)
|
|
135
|
+
unless SchemaInspector.column?(model_class, name)
|
|
136
|
+
raise ConfigurationError, "Unknown column #{name} for #{model_class}"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ast"
|
|
4
|
+
|
|
5
|
+
module Turbojson
|
|
6
|
+
class SqlGenerator
|
|
7
|
+
AssociationInfo = Struct.new(:name, :table_name, :foreign_key, :parent_key)
|
|
8
|
+
|
|
9
|
+
def initialize(ast)
|
|
10
|
+
@ast = ast
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_sql
|
|
14
|
+
table_name = @ast.table_name
|
|
15
|
+
object_sql, joins = build_json_object(@ast, table_name)
|
|
16
|
+
|
|
17
|
+
sql = []
|
|
18
|
+
sql << "SELECT #{object_sql} AS data"
|
|
19
|
+
sql << "FROM #{table_name}"
|
|
20
|
+
sql.concat(joins) unless joins.empty?
|
|
21
|
+
sql.join("\n")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def select_parts
|
|
25
|
+
build_json_object(@ast, @ast.table_name)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def build_json_object(ast, table_alias)
|
|
31
|
+
select_pairs = []
|
|
32
|
+
joins = []
|
|
33
|
+
|
|
34
|
+
ast.nodes.each do |node|
|
|
35
|
+
case node
|
|
36
|
+
when Ast::Attribute
|
|
37
|
+
value = node.sql || "#{table_alias}.#{node.name}"
|
|
38
|
+
select_pairs << "'#{node.name}', #{value}"
|
|
39
|
+
when Ast::Association
|
|
40
|
+
assoc = resolve_association(ast.model, node)
|
|
41
|
+
child_ast = node.serializer_class.ast
|
|
42
|
+
lateral_alias = "#{assoc.name}_json"
|
|
43
|
+
|
|
44
|
+
subquery = if node.type == :has_many
|
|
45
|
+
build_has_many_subquery(child_ast, assoc, table_alias)
|
|
46
|
+
else
|
|
47
|
+
build_has_one_subquery(child_ast, assoc, table_alias)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
joins << "LEFT JOIN LATERAL (#{subquery}) #{lateral_alias} ON TRUE"
|
|
51
|
+
select_pairs << "'#{node.name}', #{lateral_alias}.#{node.name}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
["json_build_object(#{select_pairs.join(', ')})", joins]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def build_has_one_subquery(child_ast, assoc, parent_table)
|
|
59
|
+
object_sql, child_joins = build_json_object(child_ast, assoc.table_name)
|
|
60
|
+
sql = []
|
|
61
|
+
sql << "SELECT #{object_sql} AS #{assoc.name}"
|
|
62
|
+
sql << "FROM #{assoc.table_name}"
|
|
63
|
+
sql.concat(child_joins) unless child_joins.empty?
|
|
64
|
+
sql << "WHERE #{assoc.table_name}.#{assoc.foreign_key} = #{parent_table}.#{assoc.parent_key}"
|
|
65
|
+
sql << "LIMIT 1"
|
|
66
|
+
sql.join("\n")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def build_has_many_subquery(child_ast, assoc, parent_table)
|
|
70
|
+
object_sql, child_joins = build_json_object(child_ast, assoc.table_name)
|
|
71
|
+
inner = []
|
|
72
|
+
inner << "SELECT #{object_sql} AS data"
|
|
73
|
+
inner << "FROM #{assoc.table_name}"
|
|
74
|
+
inner.concat(child_joins) unless child_joins.empty?
|
|
75
|
+
inner << "WHERE #{assoc.table_name}.#{assoc.foreign_key} = #{parent_table}.#{assoc.parent_key}"
|
|
76
|
+
|
|
77
|
+
sql = []
|
|
78
|
+
sql << "SELECT COALESCE(json_agg(child_rows.data), '[]'::json) AS #{assoc.name}"
|
|
79
|
+
sql << "FROM (#{inner.join("\n")}) child_rows"
|
|
80
|
+
sql.join("\n")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def resolve_association(model, node)
|
|
84
|
+
reflection = model.respond_to?(:reflect_on_association) ? model.reflect_on_association(node.name) : nil
|
|
85
|
+
raise ConfigurationError, "Association #{node.name} not found for #{model}" unless reflection
|
|
86
|
+
|
|
87
|
+
table_name = reflection.klass.table_name
|
|
88
|
+
foreign_key = reflection.foreign_key
|
|
89
|
+
parent_key = model.respond_to?(:primary_key) ? model.primary_key : "id"
|
|
90
|
+
|
|
91
|
+
AssociationInfo.new(node.name, table_name, foreign_key, parent_key)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
data/lib/turbojson.rb
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "turbojson/version"
|
|
4
|
+
require_relative "turbojson/serializer"
|
|
5
|
+
require_relative "turbojson/sql_generator"
|
|
6
|
+
require_relative "turbojson/post_processor"
|
|
7
|
+
|
|
8
|
+
module Turbojson
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: turbojson
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Turbojson Contributors
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rspec
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.12'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.12'
|
|
26
|
+
executables: []
|
|
27
|
+
extensions: []
|
|
28
|
+
extra_rdoc_files: []
|
|
29
|
+
files:
|
|
30
|
+
- README.md
|
|
31
|
+
- lib/turbojson.rb
|
|
32
|
+
- lib/turbojson/ast.rb
|
|
33
|
+
- lib/turbojson/post_processor.rb
|
|
34
|
+
- lib/turbojson/schema_inspector.rb
|
|
35
|
+
- lib/turbojson/serializer.rb
|
|
36
|
+
- lib/turbojson/sql_generator.rb
|
|
37
|
+
- lib/turbojson/version.rb
|
|
38
|
+
licenses:
|
|
39
|
+
- MIT
|
|
40
|
+
metadata: {}
|
|
41
|
+
rdoc_options: []
|
|
42
|
+
require_paths:
|
|
43
|
+
- lib
|
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '3.0'
|
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
requirements: []
|
|
55
|
+
rubygems_version: 3.6.7
|
|
56
|
+
specification_version: 4
|
|
57
|
+
summary: Compile serializer DSL into Postgres JSON SQL
|
|
58
|
+
test_files: []
|