graph_weaver 0.2.0 → 0.2.2
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/CHANGELOG.md +49 -0
- data/Gemfile.lock +21 -30
- data/PLAN.md +7 -0
- data/docs/generated_modules.md +78 -4
- data/docs/getting_started.md +8 -6
- data/docs/testing.md +21 -14
- data/lib/graph_weaver/client.rb +3 -2
- data/lib/graph_weaver/codegen/emit.rb +274 -60
- data/lib/graph_weaver/codegen/nodes.rb +35 -80
- data/lib/graph_weaver/codegen.rb +41 -104
- data/lib/graph_weaver/input_struct.rb +59 -0
- data/lib/graph_weaver/railtie.rb +2 -1
- data/lib/graph_weaver/rspec.rb +4 -6
- data/lib/graph_weaver/testing.rb +4 -4
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +103 -22
- metadata +2 -1
data/lib/graph_weaver/codegen.rb
CHANGED
|
@@ -19,6 +19,7 @@ require "sorbet-runtime"
|
|
|
19
19
|
# codegen/nodes.rb (the typed IR), codegen/emit.rb (source emission);
|
|
20
20
|
# this file holds the public API and the query walk.
|
|
21
21
|
require_relative "hints"
|
|
22
|
+
require_relative "input_struct"
|
|
22
23
|
require_relative "inflect"
|
|
23
24
|
require_relative "selection"
|
|
24
25
|
require_relative "codegen/enum_type"
|
|
@@ -45,9 +46,11 @@ class GraphWeaver::Codegen
|
|
|
45
46
|
# generation stays strict — a checked-in file deserves a deliberate
|
|
46
47
|
# name). scalars:/enums:/types: are client-scoped overlays consulted
|
|
47
48
|
# before the global registries (ScalarType, EnumType, and arrays of
|
|
48
|
-
# mixin modules, each keyed by GraphQL name).
|
|
49
|
+
# mixin modules, each keyed by GraphQL name). inputs_namespace: is the
|
|
50
|
+
# shared-inputs workflow (see GraphWeaver.generate!): variable types
|
|
51
|
+
# live once in that module and the query module aliases what it uses.
|
|
49
52
|
def initialize(schema:, query:, module_name: nil, client: nil, default_module_name: nil,
|
|
50
|
-
scalars: nil, enums: nil, types: nil)
|
|
53
|
+
scalars: nil, enums: nil, types: nil, inputs_namespace: nil)
|
|
51
54
|
@schema = schema
|
|
52
55
|
@query = query.strip
|
|
53
56
|
@module_name = module_name
|
|
@@ -55,6 +58,7 @@ class GraphWeaver::Codegen
|
|
|
55
58
|
@scalars = scalars || {}
|
|
56
59
|
@enums = enums || {}
|
|
57
60
|
@types = types || {}
|
|
61
|
+
@inputs_namespace = inputs_namespace
|
|
58
62
|
@client_const = self.class.client_const(client)
|
|
59
63
|
|
|
60
64
|
if client && @client_const.nil?
|
|
@@ -100,6 +104,40 @@ class GraphWeaver::Codegen
|
|
|
100
104
|
mod
|
|
101
105
|
end
|
|
102
106
|
|
|
107
|
+
# The schema-level variable types this query touched, by GraphQL
|
|
108
|
+
# name — the generate! workflow unions these across queries to decide
|
|
109
|
+
# what the shared inputs module must contain.
|
|
110
|
+
def variable_type_names
|
|
111
|
+
{ inputs: @variable_inputs.keys, enums: @variable_enums.keys, mapped: @mapped_enums.keys }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# The shared inputs artifact: the named input/enum types — plus
|
|
115
|
+
# everything they transitively reference — emitted once per schema as
|
|
116
|
+
# a manifest (inputs.rb) plus one file per type under inputs/, so a
|
|
117
|
+
# schema migration diffs only the types it touched. Returns
|
|
118
|
+
# { relative_filename => source }.
|
|
119
|
+
def self.generate_inputs(schema:, module_name:, input_types: [], enum_types: [],
|
|
120
|
+
scalars: nil, enums: nil, types: nil)
|
|
121
|
+
codegen = new(schema:, query: "", module_name:, scalars:, enums:, types:)
|
|
122
|
+
codegen.generate_inputs(input_types, enum_types)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def generate_inputs(input_types, enum_types)
|
|
126
|
+
unless @module_name&.match?(/\A[A-Z]\w*(::[A-Z]\w*)*\z/)
|
|
127
|
+
raise ArgumentError, "inputs module name must be a constant name, got #{@module_name.inspect}"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
@variable_enums = {}
|
|
131
|
+
@variable_inputs = {}
|
|
132
|
+
@mapped_enums = {}
|
|
133
|
+
@requires = []
|
|
134
|
+
|
|
135
|
+
enum_types.sort.each { |name| variable_core(@schema.get_type(name)) }
|
|
136
|
+
input_types.sort.each { |name| input_node(@schema.get_type(name)) }
|
|
137
|
+
|
|
138
|
+
emit_inputs_files
|
|
139
|
+
end
|
|
140
|
+
|
|
103
141
|
VarDef = Struct.new(:kwarg, :wire, :node, :required)
|
|
104
142
|
|
|
105
143
|
# Names that cannot appear bare in generated Ruby: keywords aren't
|
|
@@ -164,61 +202,7 @@ class GraphWeaver::Codegen
|
|
|
164
202
|
|
|
165
203
|
root = object_node(root_type, operation.selections, "Result")
|
|
166
204
|
|
|
167
|
-
|
|
168
|
-
out << "# typed: strict"
|
|
169
|
-
out << "# frozen_string_literal: true"
|
|
170
|
-
out << ""
|
|
171
|
-
out << "# Generated by GraphWeaver — do not edit."
|
|
172
|
-
out << ""
|
|
173
|
-
requires = @requires.uniq.sort
|
|
174
|
-
if requires.any?
|
|
175
|
-
requires.each { |req| out << "require #{req.inspect}" }
|
|
176
|
-
out << ""
|
|
177
|
-
end
|
|
178
|
-
out << "module #{@module_name}"
|
|
179
|
-
out << " extend T::Sig"
|
|
180
|
-
out << ""
|
|
181
|
-
# a GraphQL block string could contain a bare GRAPHQL line, which
|
|
182
|
-
# would terminate the heredoc early — pick a delimiter the query
|
|
183
|
-
# can't collide with
|
|
184
|
-
delimiter = "GRAPHQL"
|
|
185
|
-
delimiter += "_" while @query.match?(/^\s*#{delimiter}\s*$/)
|
|
186
|
-
out << " QUERY = T.let(<<~'#{delimiter}', String)"
|
|
187
|
-
@query.each_line { |line| out << " #{line}".rstrip }
|
|
188
|
-
out << " #{delimiter}"
|
|
189
|
-
out << ""
|
|
190
|
-
@mapped_enums.each_value do |mapped|
|
|
191
|
-
emit_mapped_enum(mapped, out, 1)
|
|
192
|
-
out << ""
|
|
193
|
-
end
|
|
194
|
-
@variable_enums.each_value do |enum|
|
|
195
|
-
emit_enum(enum, out, 1)
|
|
196
|
-
out << ""
|
|
197
|
-
end
|
|
198
|
-
inputs, cyclic = ordered_inputs
|
|
199
|
-
if cyclic
|
|
200
|
-
# Recursive input types (Hasura bool_exp et al) reference each other,
|
|
201
|
-
# so no definition order satisfies the runtime — forward-declare every
|
|
202
|
-
# class empty, then let the full definitions below reopen with props.
|
|
203
|
-
# eval'd so srb sees only the full bodies (reopening a T::Struct to
|
|
204
|
-
# add props is a static error; adding them at runtime is fine).
|
|
205
|
-
out << " # runtime-only forward declarations: these input types reference"
|
|
206
|
-
out << " # each other, so the full definitions below need the constants"
|
|
207
|
-
out << " eval(<<~RUBY, binding, __FILE__, __LINE__ + 1)"
|
|
208
|
-
inputs.each { |input| out << " class #{input.class_name} < T::Struct; end" }
|
|
209
|
-
out << " RUBY"
|
|
210
|
-
out << ""
|
|
211
|
-
end
|
|
212
|
-
inputs.each do |input|
|
|
213
|
-
emit_input(input, out, 1)
|
|
214
|
-
out << ""
|
|
215
|
-
end
|
|
216
|
-
emit_nested(root, out, 1)
|
|
217
|
-
out << ""
|
|
218
|
-
emit_execute(out, variables, flatten: flatten_input(variables))
|
|
219
|
-
out << "end"
|
|
220
|
-
|
|
221
|
-
out.join("\n") + "\n"
|
|
205
|
+
emit_module(root, variables)
|
|
222
206
|
end
|
|
223
207
|
|
|
224
208
|
private
|
|
@@ -251,20 +235,6 @@ class GraphWeaver::Codegen
|
|
|
251
235
|
end
|
|
252
236
|
end
|
|
253
237
|
|
|
254
|
-
# The Relay convention — an operation whose only variable is a required
|
|
255
|
-
# input object — reads better flattened: the input's fields become
|
|
256
|
-
# execute's kwargs directly, and the wrapping level is rebuilt on the
|
|
257
|
-
# wire. Multi-variable (or nullable-input) operations keep the
|
|
258
|
-
# variable-per-kwarg surface.
|
|
259
|
-
def flatten_input(variables)
|
|
260
|
-
return unless variables.size == 1
|
|
261
|
-
|
|
262
|
-
var = variables.first
|
|
263
|
-
return unless var.required && var.node.is_a?(NonNull)
|
|
264
|
-
|
|
265
|
-
input = var.node.of
|
|
266
|
-
input if input.is_a?(InputNode)
|
|
267
|
-
end
|
|
268
238
|
|
|
269
239
|
# Selection#each_field, collected by result key (codegen groups
|
|
270
240
|
# repeated selections of one field so it can merge them)
|
|
@@ -467,40 +437,7 @@ class GraphWeaver::Codegen
|
|
|
467
437
|
|
|
468
438
|
# The InputNodes a struct's fields reference, through NON_NULL/LIST
|
|
469
439
|
# wrappers — the edges of the input dependency graph.
|
|
470
|
-
def input_references(node)
|
|
471
|
-
node.fields.filter_map do |field|
|
|
472
|
-
child = field.node
|
|
473
|
-
child = child.of while child.respond_to?(:of)
|
|
474
|
-
child if child.is_a?(InputNode)
|
|
475
|
-
end
|
|
476
|
-
end
|
|
477
|
-
|
|
478
|
-
# Input structs in dependency order (referenced types before their
|
|
479
|
-
# referrers) so each emitted const names an already-defined class.
|
|
480
|
-
# Cycles make that impossible — flagged so emission can forward-declare
|
|
481
|
-
# every input class first, then reopen each to add its props.
|
|
482
|
-
def ordered_inputs
|
|
483
|
-
ordered = []
|
|
484
|
-
seen = {} # node => :done | :visiting (bool_exp graphs get big)
|
|
485
|
-
cyclic = T.let(false, T::Boolean)
|
|
486
|
-
|
|
487
|
-
visit = lambda do |node|
|
|
488
|
-
next if seen[node] == :done
|
|
489
|
-
|
|
490
|
-
if seen[node] == :visiting
|
|
491
|
-
cyclic = true
|
|
492
|
-
next
|
|
493
|
-
end
|
|
494
|
-
|
|
495
|
-
seen[node] = :visiting
|
|
496
|
-
input_references(node).each(&visit)
|
|
497
|
-
seen[node] = :done
|
|
498
|
-
ordered << node
|
|
499
|
-
end
|
|
500
|
-
@variable_inputs.each_value(&visit)
|
|
501
440
|
|
|
502
|
-
[ordered, cyclic]
|
|
503
|
-
end
|
|
504
441
|
|
|
505
442
|
# Registered helper-module names for a GraphQL type (additive: global
|
|
506
443
|
# registrations plus this client's), collecting their requires.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
require_relative "hints"
|
|
7
|
+
|
|
8
|
+
module GraphWeaver
|
|
9
|
+
# Runtime for generated input structs. Each struct declares its typed
|
|
10
|
+
# consts plus a compact FIELDS table — (prop, wire name, requiredness,
|
|
11
|
+
# serializer, coercer) per field, with the conversions emitted as
|
|
12
|
+
# lambdas — and this module is the loop that drives it. One copy here
|
|
13
|
+
# instead of unrolled methods in every struct, which is the difference
|
|
14
|
+
# between ~2 lines and ~6 lines per field when a Hasura bool_exp pulls
|
|
15
|
+
# hundreds of input types into one module.
|
|
16
|
+
module InputStruct
|
|
17
|
+
include Kernel # for sorbet: hosts are T::Structs
|
|
18
|
+
|
|
19
|
+
# serializer/coercer are code-as-data from the generated file; nil
|
|
20
|
+
# means identity (the wire value passes through untouched)
|
|
21
|
+
Field = Struct.new(:prop, :wire, :required, :serializer, :coercer)
|
|
22
|
+
|
|
23
|
+
def self.included(base)
|
|
24
|
+
base.extend(ClassMethods)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# the wire hash — optional fields left nil stay off the wire
|
|
28
|
+
def serialize
|
|
29
|
+
self.class.const_get(:FIELDS).each_with_object({}) do |field, wire|
|
|
30
|
+
value = public_send(field.prop)
|
|
31
|
+
next if value.nil? && !field.required
|
|
32
|
+
|
|
33
|
+
wire[field.wire] = field.serializer && !value.nil? ? field.serializer.call(value) : value
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
alias_method :to_h, :serialize
|
|
37
|
+
|
|
38
|
+
module ClassMethods
|
|
39
|
+
include Kernel
|
|
40
|
+
|
|
41
|
+
# Build from a plain hash (underscored keys, Symbol or String):
|
|
42
|
+
# enums accept their wire values, nested inputs accept hashes; the
|
|
43
|
+
# struct's types are enforced on construction, and unknown keys
|
|
44
|
+
# raise with a spellchecked hint.
|
|
45
|
+
def coerce(value)
|
|
46
|
+
return value if value.is_a?(self)
|
|
47
|
+
|
|
48
|
+
# a typo'd key must not silently drop off the wire
|
|
49
|
+
GraphWeaver::Hints.validate_keys!(self, value)
|
|
50
|
+
|
|
51
|
+
fields = T.unsafe(self).const_get(:FIELDS)
|
|
52
|
+
T.unsafe(self).new(**fields.to_h do |field|
|
|
53
|
+
raw = value.key?(field.prop) ? value[field.prop] : value[field.prop.to_s]
|
|
54
|
+
[field.prop, raw.nil? || field.coercer.nil? ? raw : field.coercer.call(raw)]
|
|
55
|
+
end)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/graph_weaver/railtie.rb
CHANGED
|
@@ -24,6 +24,7 @@ class GraphWeaver::Railtie < Rails::Railtie
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
initializer "graph_weaver.load_generated", after: :load_config_initializers do
|
|
27
|
-
|
|
27
|
+
# entries may be globs, so Dir[] rather than Dir.exist?
|
|
28
|
+
GraphWeaver.load_generated! if GraphWeaver.generated_paths.any? { |path| Dir[path].any? }
|
|
28
29
|
end
|
|
29
30
|
end
|
data/lib/graph_weaver/rspec.rb
CHANGED
|
@@ -8,14 +8,12 @@ require_relative "testing"
|
|
|
8
8
|
#
|
|
9
9
|
# require "graph_weaver/rspec"
|
|
10
10
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# auto_fake defaults on, so every example runs against a schema-correct
|
|
14
|
-
# fake. Configure to override:
|
|
11
|
+
# Then opt in to per-example fakes (explicit on purpose — silently
|
|
12
|
+
# swapping every example onto a fake is too surprising to be a default):
|
|
15
13
|
#
|
|
16
14
|
# GraphWeaver::Testing.configure do |config|
|
|
17
|
-
# config.
|
|
18
|
-
# config.
|
|
15
|
+
# config.auto_fake = true # every example runs against a fake
|
|
16
|
+
# # config.schema = MySchema # optional: the committed dump auto-locates
|
|
19
17
|
# end
|
|
20
18
|
#
|
|
21
19
|
# What it wires up:
|
data/lib/graph_weaver/testing.rb
CHANGED
|
@@ -49,10 +49,10 @@ module GraphWeaver
|
|
|
49
49
|
@mode = nil # auto
|
|
50
50
|
@schema = nil
|
|
51
51
|
@cassette_dir = "spec/cassettes"
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
@auto_fake =
|
|
52
|
+
# explicit opt-in: swapping every example onto a fake is too
|
|
53
|
+
# surprising to be a default — a little friction beats unexpected
|
|
54
|
+
# behavior (the schema still auto-locates once you opt in)
|
|
55
|
+
@auto_fake = false
|
|
56
56
|
# GRAPHWEAVER_RECORD=1 rspec ... -> Cassette.use re-records
|
|
57
57
|
@record = !ENV["GRAPHWEAVER_RECORD"].to_s.empty?
|
|
58
58
|
# anonymize responses as they're recorded (needs config.schema)
|
data/lib/graph_weaver/version.rb
CHANGED
data/lib/graph_weaver.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "sorbet-runtime"
|
|
|
4
4
|
require_relative "graph_weaver/logging"
|
|
5
5
|
require_relative "graph_weaver/errors"
|
|
6
6
|
require_relative "graph_weaver/hints"
|
|
7
|
+
require_relative "graph_weaver/input_struct"
|
|
7
8
|
require_relative "graph_weaver/response"
|
|
8
9
|
require_relative "graph_weaver/inflect"
|
|
9
10
|
require_relative "graph_weaver/codegen"
|
|
@@ -56,15 +57,58 @@ module GraphWeaver
|
|
|
56
57
|
target.is_a?(Client) ? target.transport! : target
|
|
57
58
|
end
|
|
58
59
|
|
|
59
|
-
# Conventional locations, factory_bot-style
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
|
|
60
|
+
# Conventional locations, factory_bot-style — LISTS, so extra
|
|
61
|
+
# locations (a test-only dir, an engine's) can be appended and every
|
|
62
|
+
# loader walks them all:
|
|
63
|
+
#
|
|
64
|
+
# # e.g. in spec/support/graph_weaver.rb
|
|
65
|
+
# GraphWeaver.generated_paths << "spec/support/graphql/generated"
|
|
66
|
+
# GraphWeaver.queries_paths << "spec/support/graphql/queries"
|
|
67
|
+
#
|
|
68
|
+
# The singular accessors read the first entry (the default target
|
|
69
|
+
# for generate! and the rake tasks); assigning one replaces the list.
|
|
70
|
+
attr_writer :queries_paths, :generated_paths, :schema_path
|
|
71
|
+
|
|
72
|
+
# Entries may be glob patterns — the generated default also matches
|
|
73
|
+
# per-schema layouts (app/graphql/github/generated). Queries stay
|
|
74
|
+
# single-schema: load_queries! parses everything against one client.
|
|
75
|
+
def queries_paths = @queries_paths ||= ["app/graphql/queries"]
|
|
76
|
+
def generated_paths = @generated_paths ||= ["app/graphql/generated", "app/graphql/*/generated"]
|
|
77
|
+
|
|
78
|
+
def queries_path = queries_paths.first
|
|
79
|
+
def generated_path = generated_paths.first
|
|
80
|
+
|
|
81
|
+
def queries_path=(path)
|
|
82
|
+
@queries_paths = path.nil? ? nil : [path]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def generated_path=(path)
|
|
86
|
+
@generated_paths = path.nil? ? nil : [path]
|
|
87
|
+
end
|
|
63
88
|
|
|
64
|
-
def queries_path = @queries_path || "app/graphql/queries"
|
|
65
|
-
def generated_path = @generated_path || "app/graphql/generated"
|
|
66
89
|
def schema_path = @schema_path || "app/graphql/schema.json"
|
|
67
90
|
|
|
91
|
+
# The shared-inputs module name: set it globally, pass inputs_module:
|
|
92
|
+
# per generate!, or let it derive from the output path — the
|
|
93
|
+
# directory above generated/ names the schema in multi-schema
|
|
94
|
+
# layouts (app/graphql/github/generated => GithubInputs); the
|
|
95
|
+
# conventional layout (and anything unrecognizable) stays
|
|
96
|
+
# GraphQLInputs.
|
|
97
|
+
attr_writer :inputs_module
|
|
98
|
+
|
|
99
|
+
def inputs_module(output = generated_path)
|
|
100
|
+
return @inputs_module if @inputs_module
|
|
101
|
+
|
|
102
|
+
segments = File.expand_path(output.to_s).split(File::SEPARATOR)
|
|
103
|
+
segments.pop if segments.last == "generated"
|
|
104
|
+
parent = segments.last.to_s
|
|
105
|
+
if parent.match?(/\A[a-zA-Z]\w*\z/) && !%w[graphql app lib spec support test].include?(parent)
|
|
106
|
+
"#{Inflect.camelize(parent)}Inputs"
|
|
107
|
+
else
|
|
108
|
+
"GraphQLInputs"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
68
112
|
# Generate every .graphql query in a directory into checked-in Ruby
|
|
69
113
|
# files. Paths default to the conventions above; schema: defaults to
|
|
70
114
|
# the dump at schema_path (any supported extension):
|
|
@@ -73,16 +117,28 @@ module GraphWeaver
|
|
|
73
117
|
#
|
|
74
118
|
# person.graphql => person_query.rb defining PersonQuery. Returns the
|
|
75
119
|
# written paths. Pair with a freshness spec (docs/generated_modules.md).
|
|
76
|
-
def generate!(schema: nil, queries: queries_path, output: generated_path, client: nil
|
|
120
|
+
def generate!(schema: nil, queries: queries_path, output: generated_path, client: nil,
|
|
121
|
+
shared_inputs: true, inputs_module: nil)
|
|
77
122
|
schema ||= locate_schema!
|
|
78
|
-
|
|
123
|
+
inputs_module ||= self.inputs_module(output)
|
|
79
124
|
|
|
80
|
-
|
|
81
|
-
|
|
125
|
+
plan = generation_plan(queries:, schema:, client:, shared_inputs:, inputs_module:)
|
|
126
|
+
written = plan.map do |filename, source|
|
|
127
|
+
target = File.join(output, filename)
|
|
128
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
82
129
|
File.write(target, source)
|
|
83
130
|
log(:info) { "generated #{target}" }
|
|
84
131
|
target
|
|
85
132
|
end
|
|
133
|
+
|
|
134
|
+
# a type dropped from the schema must not linger as a stale file —
|
|
135
|
+
# inputs/ is wholly generated, so pruning is safe
|
|
136
|
+
(Dir[File.join(output, "inputs", "*.rb")] - written).each do |orphan|
|
|
137
|
+
File.delete(orphan)
|
|
138
|
+
log(:info) { "pruned #{orphan}" }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
written
|
|
86
142
|
end
|
|
87
143
|
|
|
88
144
|
# The freshness guard: raise unless every generated file matches what
|
|
@@ -92,12 +148,17 @@ module GraphWeaver
|
|
|
92
148
|
# it "generated queries are current" do
|
|
93
149
|
# GraphWeaver.verify_generated!
|
|
94
150
|
# end
|
|
95
|
-
def verify_generated!(schema: nil, queries: queries_path, output: generated_path, client: nil
|
|
151
|
+
def verify_generated!(schema: nil, queries: queries_path, output: generated_path, client: nil,
|
|
152
|
+
shared_inputs: true, inputs_module: nil)
|
|
96
153
|
schema ||= locate_schema!
|
|
97
|
-
|
|
98
|
-
|
|
154
|
+
inputs_module ||= self.inputs_module(output)
|
|
155
|
+
plan = generation_plan(queries:, schema:, client:, shared_inputs:, inputs_module:)
|
|
156
|
+
stale = plan.filter_map do |filename, source|
|
|
157
|
+
target = File.join(output, filename)
|
|
99
158
|
target unless File.exist?(target) && File.read(target) == source
|
|
100
159
|
end
|
|
160
|
+
# strays: a type file the current schema no longer produces
|
|
161
|
+
stale += Dir[File.join(output, "inputs", "*.rb")] - plan.map { |f, _| File.join(output, f) }
|
|
101
162
|
|
|
102
163
|
unless stale.empty?
|
|
103
164
|
raise Error, "stale generated queries — regenerate (rake graph_weaver:generate): #{stale.join(", ")}"
|
|
@@ -116,10 +177,11 @@ module GraphWeaver
|
|
|
116
177
|
# Generated::PersonQuery from generated/person_query.rb, and
|
|
117
178
|
# generated code only changes on regeneration anyway (restart, like
|
|
118
179
|
# a schema migration).
|
|
119
|
-
def load_generated!(path =
|
|
120
|
-
|
|
180
|
+
def load_generated!(path = nil)
|
|
181
|
+
paths = path ? [path] : generated_paths
|
|
182
|
+
files = paths.flat_map { |dir| Dir[File.join(dir, "**/*.rb")].sort }.uniq
|
|
121
183
|
files.each { |file| require File.expand_path(file) }
|
|
122
|
-
log(:info) { "loaded #{files.size} generated module(s) from #{
|
|
184
|
+
log(:info) { "loaded #{files.size} generated module(s) from #{paths.join(", ")}" }
|
|
123
185
|
files
|
|
124
186
|
end
|
|
125
187
|
|
|
@@ -130,20 +192,39 @@ module GraphWeaver
|
|
|
130
192
|
end
|
|
131
193
|
private :locate_schema!
|
|
132
194
|
|
|
133
|
-
# (
|
|
134
|
-
|
|
135
|
-
|
|
195
|
+
# (filename, source) per artifact: shared_inputs (the default) emits
|
|
196
|
+
# every variable type once into inputs.rb, with query modules
|
|
197
|
+
# aliasing what they use — the difference between hundreds of
|
|
198
|
+
# duplicated bool_exp structs and one copy per schema.
|
|
199
|
+
def generation_plan(queries:, schema:, client:, shared_inputs:, inputs_module: self.inputs_module)
|
|
200
|
+
namespace = shared_inputs ? inputs_module : nil
|
|
201
|
+
used = { inputs: [], enums: [], mapped: [] }
|
|
202
|
+
|
|
203
|
+
plan = Dir[File.join(queries, "*.graphql")].sort.map do |path|
|
|
136
204
|
base = File.basename(path, ".graphql")
|
|
137
|
-
|
|
205
|
+
codegen = Codegen.new(
|
|
138
206
|
schema:,
|
|
139
207
|
query: File.read(path),
|
|
140
208
|
module_name: "#{Inflect.camelize(base)}Query",
|
|
141
209
|
client:,
|
|
210
|
+
inputs_namespace: namespace,
|
|
142
211
|
)
|
|
143
|
-
|
|
212
|
+
source = codegen.generate
|
|
213
|
+
codegen.variable_type_names.each { |kind, names| used[kind] |= names }
|
|
214
|
+
["#{base}_query.rb", source]
|
|
144
215
|
end
|
|
216
|
+
|
|
217
|
+
if namespace && used.values.any?(&:any?)
|
|
218
|
+
shared = Codegen.generate_inputs(
|
|
219
|
+
schema:, module_name: namespace,
|
|
220
|
+
input_types: used[:inputs], enum_types: used[:enums] + used[:mapped],
|
|
221
|
+
)
|
|
222
|
+
plan = shared.to_a + plan
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
plan
|
|
145
226
|
end
|
|
146
|
-
private :
|
|
227
|
+
private :generation_plan
|
|
147
228
|
|
|
148
229
|
# Default input coercion for scalars that don't say coerce: themselves,
|
|
149
230
|
# resolved lazily at generation time (so set it any time before you
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: graph_weaver
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Pepper
|
|
@@ -255,6 +255,7 @@ files:
|
|
|
255
255
|
- lib/graph_weaver/errors.rb
|
|
256
256
|
- lib/graph_weaver/hints.rb
|
|
257
257
|
- lib/graph_weaver/inflect.rb
|
|
258
|
+
- lib/graph_weaver/input_struct.rb
|
|
258
259
|
- lib/graph_weaver/logging.rb
|
|
259
260
|
- lib/graph_weaver/railtie.rb
|
|
260
261
|
- lib/graph_weaver/response.rb
|