graph_weaver 0.0.1

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.
@@ -0,0 +1,567 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "graphql"
5
+ require "sorbet-runtime"
6
+
7
+ # Generates plain, statically-typecheckable Ruby from a GraphQL query +
8
+ # schema: nested T::Structs, from_h casting code, and a sig'd execute
9
+ # method. Unlike StructTypes (which builds classes at parse time, visible
10
+ # only at runtime), the output is source on disk — srb tc sees the exact
11
+ # result type of each query.
12
+ #
13
+ # Supports queries and mutations; plain fields, inline fragments, named
14
+ # fragment spreads (including interface type conditions), union- and
15
+ # interface-typed fields (dispatch on __typename), enums (generated
16
+ # T::Enum), and typed variables (kwargs on execute). Input objects and
17
+ # subscriptions are still open.
18
+ class GraphWeaver::Codegen
19
+ # sorbet types for scalars
20
+ SCALARS = {
21
+ "ID" => "String",
22
+ "String" => "String",
23
+ "Int" => "Integer",
24
+ "Float" => "Float",
25
+ "Boolean" => "T::Boolean",
26
+ "Date" => "Date",
27
+ }.freeze
28
+
29
+ # deserialization for scalars whose wire format differs from their Ruby
30
+ # type; anything absent passes through untouched
31
+ SCALAR_CASTS = {
32
+ "Date" => ->(expr) { "Date.iso8601(#{expr})" },
33
+ }.freeze
34
+
35
+ # the inverse, for serializing variables onto the wire
36
+ SCALAR_SERIALIZERS = {
37
+ "Date" => ->(expr) { "#{expr}.iso8601" },
38
+ }.freeze
39
+
40
+ class Scalar
41
+ def initialize(name)
42
+ @name = name
43
+ end
44
+
45
+ def bare_type
46
+ SCALARS.fetch(@name, "T.untyped")
47
+ end
48
+
49
+ def prop_type
50
+ "T.nilable(#{bare_type})"
51
+ end
52
+
53
+ def cast(expr, _depth)
54
+ SCALAR_CASTS.fetch(@name) { return expr }.call(expr)
55
+ end
56
+
57
+ def identity?
58
+ !SCALAR_CASTS.key?(@name)
59
+ end
60
+
61
+ def serialize(expr, _depth)
62
+ SCALAR_SERIALIZERS.fetch(@name) { return expr }.call(expr)
63
+ end
64
+
65
+ def serialize_identity?
66
+ !SCALAR_SERIALIZERS.key?(@name)
67
+ end
68
+
69
+ def non_null? = false
70
+ def nested = nil
71
+ end
72
+
73
+ class NonNull
74
+ def initialize(of)
75
+ @of = of
76
+ end
77
+
78
+ def bare_type = @of.bare_type
79
+ def prop_type = bare_type
80
+ def cast(expr, depth) = @of.cast(expr, depth)
81
+ def identity? = @of.identity?
82
+ def serialize(expr, depth) = @of.serialize(expr, depth)
83
+ def serialize_identity? = @of.serialize_identity?
84
+ def non_null? = true
85
+ def nested = @of.nested
86
+ end
87
+
88
+ class List
89
+ def initialize(of)
90
+ @of = of
91
+ end
92
+
93
+ def bare_type
94
+ "T::Array[#{@of.prop_type}]"
95
+ end
96
+
97
+ def prop_type
98
+ "T.nilable(#{bare_type})"
99
+ end
100
+
101
+ def cast(expr, depth)
102
+ var = "v#{depth}"
103
+ element = if @of.non_null? || @of.identity?
104
+ @of.identity? ? var : @of.cast(var, depth + 1)
105
+ else
106
+ "#{var}&.then { |v#{depth + 1}| #{@of.cast("v#{depth + 1}", depth + 2)} }"
107
+ end
108
+
109
+ "#{expr}.map { |#{var}| #{element} }"
110
+ end
111
+
112
+ def identity? = @of.identity?
113
+
114
+ def serialize(expr, depth)
115
+ var = "v#{depth}"
116
+ element = if @of.non_null? || @of.serialize_identity?
117
+ @of.serialize_identity? ? var : @of.serialize(var, depth + 1)
118
+ else
119
+ "#{var}&.then { |v#{depth + 1}| #{@of.serialize("v#{depth + 1}", depth + 2)} }"
120
+ end
121
+
122
+ "#{expr}.map { |#{var}| #{element} }"
123
+ end
124
+
125
+ def serialize_identity? = @of.serialize_identity?
126
+ def non_null? = false
127
+ def nested = @of.nested
128
+ end
129
+
130
+ class ObjectNode
131
+ Field = Struct.new(:prop, :key, :node)
132
+
133
+ attr_reader :class_name, :fields
134
+
135
+ def initialize(class_name)
136
+ @class_name = class_name
137
+ @fields = []
138
+ end
139
+
140
+ def bare_type = class_name
141
+
142
+ def prop_type
143
+ "T.nilable(#{bare_type})"
144
+ end
145
+
146
+ def cast(expr, _depth)
147
+ "#{class_name}.from_h(#{expr})"
148
+ end
149
+
150
+ def identity? = false
151
+ def non_null? = false
152
+ def nested = self
153
+ end
154
+
155
+ class EnumNode
156
+ attr_reader :class_name, :values
157
+
158
+ def initialize(class_name, values)
159
+ @class_name = class_name
160
+ @values = values
161
+ end
162
+
163
+ def bare_type = class_name
164
+
165
+ def prop_type
166
+ "T.nilable(#{bare_type})"
167
+ end
168
+
169
+ def cast(expr, _depth)
170
+ "#{class_name}.deserialize(#{expr})"
171
+ end
172
+
173
+ def identity? = false
174
+
175
+ def serialize(expr, _depth)
176
+ "#{expr}.serialize"
177
+ end
178
+
179
+ def serialize_identity? = false
180
+ def non_null? = false
181
+ def nested = self
182
+ end
183
+
184
+ class UnionNode
185
+ attr_reader :class_name, :members # graphql type name => ObjectNode
186
+
187
+ def initialize(class_name, members)
188
+ @class_name = class_name
189
+ @members = members
190
+ end
191
+
192
+ def bare_type = "#{class_name}::Type"
193
+
194
+ def prop_type
195
+ "T.nilable(#{bare_type})"
196
+ end
197
+
198
+ def cast(expr, _depth)
199
+ "#{class_name}.from_h(#{expr})"
200
+ end
201
+
202
+ def identity? = false
203
+ def non_null? = false
204
+ def nested = self
205
+ end
206
+
207
+ # executor_const names anything responding to
208
+ # `execute(query, variables:)` whose result `to_h`s into
209
+ # {"data" => ..., "errors" => ...} — a Schema class for in-process
210
+ # execution, or an HttpExecutor for a remote endpoint. The generated
211
+ # execute also accepts an executor: override at runtime.
212
+ def initialize(schema:, executor_const:, query:, module_name:)
213
+ @schema = schema
214
+ @executor_const = executor_const
215
+ @query = query.strip
216
+ @module_name = module_name
217
+ end
218
+
219
+ # Development convenience: generate + eval in one step, no build
220
+ # artifact or checked-in file. Same runtime semantics as the generated
221
+ # file, but invisible to srb tc — use the build step for static typing.
222
+ def self.load(schema:, executor_const:, query:, module_name:)
223
+ source = new(schema:, executor_const:, query:, module_name:).generate
224
+ Object.class_eval(source, "(struct_codegen)", 1)
225
+ Object.const_get(module_name)
226
+ end
227
+
228
+ VarDef = Struct.new(:kwarg, :wire, :node, :required)
229
+
230
+ def generate
231
+ errors = @schema.validate(@query)
232
+ if errors.any?
233
+ raise ArgumentError, "invalid query: #{errors.map(&:message).join("; ")}"
234
+ end
235
+
236
+ doc = GraphQL.parse(@query)
237
+ @fragments = doc.definitions
238
+ .grep(GraphQL::Language::Nodes::FragmentDefinition)
239
+ .to_h { |fragment| [fragment.name, fragment] }
240
+ @variable_enums = {}
241
+
242
+ operation = doc.definitions.grep(GraphQL::Language::Nodes::OperationDefinition).first
243
+ root_type = case operation&.operation_type
244
+ when "query", nil then @schema.query
245
+ when "mutation" then @schema.mutation
246
+ else raise NotImplementedError, "unsupported operation: #{operation.operation_type}"
247
+ end
248
+
249
+ variables = operation.variables.map do |var|
250
+ node = ast_type_ref(var.type)
251
+ # a variable is optional when nullable or defaulted; optional kwargs
252
+ # default to nil and are omitted from the wire
253
+ required = node.non_null? && var.default_value.nil?
254
+ VarDef.new(underscore(var.name), var.name, node, required)
255
+ end
256
+
257
+ root = object_node(root_type, operation.selections, "Result")
258
+
259
+ out = []
260
+ out << "# typed: strict"
261
+ out << "# frozen_string_literal: true"
262
+ out << ""
263
+ out << "# Generated by GraphWeaver — do not edit."
264
+ out << ""
265
+ out << "module #{@module_name}"
266
+ out << " extend T::Sig"
267
+ out << ""
268
+ out << " QUERY = T.let(<<~'GRAPHQL', String)"
269
+ @query.each_line { |line| out << " #{line}".rstrip }
270
+ out << " GRAPHQL"
271
+ out << ""
272
+ @variable_enums.each_value do |enum|
273
+ emit_enum(enum, out, 1)
274
+ out << ""
275
+ end
276
+ emit_nested(root, out, 1)
277
+ out << ""
278
+ emit_execute(out, variables)
279
+ out << "end"
280
+
281
+ out.join("\n") + "\n"
282
+ end
283
+
284
+ private
285
+
286
+ # GraphQL names are plain camelCase/SCREAMING_SNAKE — no acronym edge
287
+ # cases, so minimal inflection beats an activesupport dependency
288
+ def underscore(name)
289
+ name.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
290
+ end
291
+
292
+ def camelize(name)
293
+ name.split("_").map { |part| "#{part[0].upcase}#{part[1..]}" }.join
294
+ end
295
+
296
+ # Flatten a selection set as seen by `type`: plain fields collect
297
+ # directly; inline fragments and named spreads recurse when their type
298
+ # condition matches (exact name match — interface conditions are out of
299
+ # scope for this spike).
300
+ def gather(type, selections, out = {})
301
+ selections.each do |selection|
302
+ case selection
303
+ when GraphQL::Language::Nodes::Field
304
+ (out[selection.alias || selection.name] ||= []) << selection
305
+ when GraphQL::Language::Nodes::InlineFragment
306
+ gather(type, selection.selections, out) if applies?(selection.type&.name, type)
307
+ when GraphQL::Language::Nodes::FragmentSpread
308
+ fragment = @fragments.fetch(selection.name) do
309
+ raise ArgumentError, "unknown fragment: #{selection.name}"
310
+ end
311
+ gather(type, fragment.selections, out) if applies?(fragment.type.name, type)
312
+ else
313
+ raise NotImplementedError, "unsupported selection: #{selection.class}"
314
+ end
315
+ end
316
+
317
+ out
318
+ end
319
+
320
+ # A fragment's type condition applies when it names this type exactly,
321
+ # or an interface/union this type belongs to (`... on Named { ... }`).
322
+ def applies?(condition, type)
323
+ return true if condition.nil? || condition == type.graphql_name
324
+
325
+ condition_type = @schema.get_type(condition)
326
+ return false unless condition_type
327
+
328
+ kind = condition_type.kind.name
329
+ (kind == "INTERFACE" || kind == "UNION") &&
330
+ @schema.possible_types(condition_type).include?(type)
331
+ end
332
+
333
+ def object_node(type, selections, class_name)
334
+ node = ObjectNode.new(class_name)
335
+ taken = [class_name]
336
+
337
+ gather(type, selections).each do |key, field_nodes|
338
+ field_name = field_nodes.first.name
339
+ prop = underscore(key)
340
+
341
+ child = if field_name == "__typename"
342
+ NonNull.new(Scalar.new("String"))
343
+ else
344
+ field_type = @schema.get_field(type.graphql_name, field_name).type
345
+ sub_selections = field_nodes.flat_map(&:selections)
346
+
347
+ case (core = unwrap(field_type)).kind.name
348
+ when "OBJECT"
349
+ name = pick_name(core.graphql_name, key, taken)
350
+ type_ref(field_type) { object_node(core, sub_selections, name) }
351
+ when "UNION", "INTERFACE"
352
+ name = pick_name(core.graphql_name, key, taken)
353
+ type_ref(field_type) { union_node(core, sub_selections, name) }
354
+ when "ENUM"
355
+ name = pick_name(core.graphql_name, key, taken)
356
+ # sorted so output is deterministic across schema sources
357
+ # (SDL round-trips reorder values alphabetically)
358
+ type_ref(field_type) { EnumNode.new(name, core.values.keys.sort) }
359
+ when "SCALAR"
360
+ type_ref(field_type) { Scalar.new(core.graphql_name) }
361
+ else
362
+ raise NotImplementedError, "unsupported kind: #{core.kind.name}"
363
+ end
364
+ end
365
+
366
+ node.fields << ObjectNode::Field.new(prop, key, child)
367
+ end
368
+
369
+ node
370
+ end
371
+
372
+ # Abstract types (unions AND interfaces): one member struct per
373
+ # possible type; wire dispatch reads __typename, so the query must
374
+ # select it. For interfaces, the interface's own field selections
375
+ # gather into every member.
376
+ def union_node(type, selections, class_name)
377
+ unless gather(type, selections).key?("__typename")
378
+ raise ArgumentError, "select __typename on #{type.graphql_name} so #{class_name} can dispatch"
379
+ end
380
+
381
+ # sorted so output is deterministic across schema sources
382
+ members = @schema.possible_types(type).sort_by(&:graphql_name).to_h do |possible|
383
+ [possible.graphql_name, object_node(possible, selections, possible.graphql_name)]
384
+ end
385
+
386
+ UnionNode.new(class_name, members)
387
+ end
388
+
389
+ # Build a node from an AST type reference (variable definitions), where
390
+ # only the type NAME is known — resolve the core through the schema.
391
+ def ast_type_ref(ast_type)
392
+ case ast_type
393
+ when GraphQL::Language::Nodes::NonNullType
394
+ NonNull.new(ast_type_ref(ast_type.of_type))
395
+ when GraphQL::Language::Nodes::ListType
396
+ List.new(ast_type_ref(ast_type.of_type))
397
+ when GraphQL::Language::Nodes::TypeName
398
+ core = @schema.get_type(ast_type.name)
399
+ case core.kind.name
400
+ when "SCALAR"
401
+ Scalar.new(core.graphql_name)
402
+ when "ENUM"
403
+ @variable_enums[core.graphql_name] ||=
404
+ EnumNode.new(core.graphql_name, core.values.keys.sort)
405
+ else
406
+ raise NotImplementedError, "unsupported variable kind: #{core.kind.name}"
407
+ end
408
+ else
409
+ raise NotImplementedError, "unsupported type node: #{ast_type.class}"
410
+ end
411
+ end
412
+
413
+ # rebuild the NON_NULL/LIST wrappers around the core node
414
+ def type_ref(type, &core)
415
+ case type.kind.name
416
+ when "NON_NULL"
417
+ NonNull.new(type_ref(type.of_type, &core))
418
+ when "LIST"
419
+ List.new(type_ref(type.of_type, &core))
420
+ else
421
+ core.call
422
+ end
423
+ end
424
+
425
+ def unwrap(type)
426
+ type = type.of_type while type.kind.name == "NON_NULL" || type.kind.name == "LIST"
427
+ type
428
+ end
429
+
430
+ def pick_name(type_name, key, taken)
431
+ candidate = type_name
432
+ candidate = "#{camelize(key)}#{type_name}" if taken.include?(candidate)
433
+ raise NotImplementedError, "class name collision: #{candidate}" if taken.include?(candidate)
434
+
435
+ taken << candidate
436
+ candidate
437
+ end
438
+
439
+ def emit_nested(node, out, indent)
440
+ case node
441
+ when UnionNode then emit_union(node, out, indent)
442
+ when EnumNode then emit_enum(node, out, indent)
443
+ else emit_object(node, out, indent)
444
+ end
445
+ end
446
+
447
+ def emit_enum(node, out, indent)
448
+ pad = " " * indent
449
+
450
+ out << "#{pad}class #{node.class_name} < T::Enum"
451
+ out << "#{pad} enums do"
452
+ node.values.each do |value|
453
+ out << "#{pad} #{camelize(value.downcase)} = new(#{value.inspect})"
454
+ end
455
+ out << "#{pad} end"
456
+ out << "#{pad}end"
457
+ end
458
+
459
+ def emit_object(node, out, indent)
460
+ pad = " " * indent
461
+
462
+ out << "#{pad}class #{node.class_name} < T::Struct"
463
+ out << "#{pad} extend T::Sig"
464
+ out << ""
465
+
466
+ node.fields.filter_map { |field| field.node.nested }.each do |child|
467
+ emit_nested(child, out, indent + 1)
468
+ out << ""
469
+ end
470
+
471
+ node.fields.each do |field|
472
+ out << "#{pad} const :#{field.prop}, #{field.node.prop_type}"
473
+ end
474
+
475
+ out << ""
476
+ out << "#{pad} sig { params(data: T::Hash[String, T.untyped]).returns(#{node.class_name}) }"
477
+ out << "#{pad} def self.from_h(data)"
478
+ out << "#{pad} new("
479
+ node.fields.each do |field|
480
+ out << "#{pad} #{field.prop}: #{field_cast(field)},"
481
+ end
482
+ out << "#{pad} )"
483
+ out << "#{pad} end"
484
+ out << "#{pad}end"
485
+ end
486
+
487
+ def emit_union(node, out, indent)
488
+ pad = " " * indent
489
+
490
+ out << "#{pad}module #{node.class_name}"
491
+ out << "#{pad} extend T::Sig"
492
+ out << ""
493
+
494
+ node.members.each_value do |member|
495
+ emit_object(member, out, indent + 1)
496
+ out << ""
497
+ end
498
+
499
+ member_names = node.members.values.map(&:class_name)
500
+ type_alias = member_names.size == 1 ? member_names.first : "T.any(#{member_names.join(", ")})"
501
+ out << "#{pad} Type = T.type_alias { #{type_alias} }"
502
+ out << ""
503
+ out << "#{pad} sig { params(data: T::Hash[String, T.untyped]).returns(Type) }"
504
+ out << "#{pad} def self.from_h(data)"
505
+ out << "#{pad} case (typename = data.fetch(\"__typename\"))"
506
+ node.members.each do |graphql_name, member|
507
+ out << "#{pad} when #{graphql_name.inspect} then #{member.class_name}.from_h(data)"
508
+ end
509
+ out << "#{pad} else raise \"unexpected __typename: \#{typename}\""
510
+ out << "#{pad} end"
511
+ out << "#{pad} end"
512
+ out << "#{pad}end"
513
+ end
514
+
515
+ def emit_execute(out, variables)
516
+ sig_params = variables.map do |var|
517
+ kwarg_type = var.required ? var.node.prop_type : "T.nilable(#{var.node.bare_type})"
518
+ "#{var.kwarg}: #{kwarg_type}"
519
+ end
520
+ sig_params << "executor: T.untyped"
521
+
522
+ kwargs = variables.map { |var| var.required ? "#{var.kwarg}:" : "#{var.kwarg}: nil" }
523
+ kwargs << "executor: #{@executor_const}"
524
+
525
+ out << " sig { params(#{sig_params.join(", ")}).returns(Result) }"
526
+ out << " def self.execute(#{kwargs.join(", ")})"
527
+
528
+ required, optional = variables.partition(&:required)
529
+ if required.empty?
530
+ out << " variables = {}"
531
+ else
532
+ out << " variables = {"
533
+ required.each do |var|
534
+ out << " #{var.wire.inspect} => #{variable_serialize(var)},"
535
+ end
536
+ out << " }"
537
+ end
538
+ optional.each do |var|
539
+ out << " variables[#{var.wire.inspect}] = #{variable_serialize(var)} unless #{var.kwarg}.nil?"
540
+ end
541
+
542
+ out << ""
543
+ out << " result = executor.execute(QUERY, variables: variables).to_h"
544
+ out << " if (errors = result[\"errors\"])"
545
+ out << " raise \"query failed: \#{errors.inspect}\""
546
+ out << " end"
547
+ out << ""
548
+ out << " Result.from_h(result.fetch(\"data\"))"
549
+ out << " end"
550
+ end
551
+
552
+ def variable_serialize(var)
553
+ var.node.serialize_identity? ? var.kwarg : var.node.serialize(var.kwarg, 1)
554
+ end
555
+
556
+ def field_cast(field)
557
+ node = field.node
558
+
559
+ if node.non_null?
560
+ raw = "data.fetch(#{field.key.inspect})"
561
+ node.identity? ? raw : node.cast(raw, 1)
562
+ else
563
+ raw = "data[#{field.key.inspect}]"
564
+ node.identity? ? raw : "#{raw}&.then { |v1| #{node.cast("v1", 2)} }"
565
+ end
566
+ end
567
+ end
@@ -0,0 +1,32 @@
1
+ # typed: false — monkeypatch; `self.class` resolves against the prepended host
2
+ # frozen_string_literal: true
3
+
4
+ require "graphql"
5
+
6
+ # graphql-ruby's SDL builder (BuildFromDefinition#prepare_directives)
7
+ # passes only the directive arguments present at the usage site, but
8
+ # Directive#initialize validates ALL defined arguments — so a defaulted
9
+ # non-null argument (`extension: Boolean! = false`) raises
10
+ # InvalidArgumentError when omitted, even though the SDL spec makes it
11
+ # optional. Real Apollo supergraph SDL (join v0.3) hits this on every
12
+ # @join__type usage.
13
+ #
14
+ # Fill in the declared defaults before validation. This is what upstream
15
+ # should do; present in graphql 2.6.3 (latest at time of writing).
16
+ #
17
+ # TODO: delete this file (and its requires) once
18
+ # https://github.com/rmosolgo/graphql-ruby/pull/5659 lands in a released
19
+ # graphql version and the Gemfile picks it up.
20
+ module DirectiveDefaultsPatch
21
+ def initialize(owner, **arguments)
22
+ self.class.all_argument_definitions.each do |arg_defn|
23
+ if !arguments.key?(arg_defn.keyword) && arg_defn.default_value?
24
+ arguments[arg_defn.keyword] = arg_defn.default_value
25
+ end
26
+ end
27
+
28
+ super(owner, **arguments)
29
+ end
30
+ end
31
+
32
+ GraphQL::Schema::Directive.prepend(DirectiveDefaultsPatch)
@@ -0,0 +1,31 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "sorbet-runtime"
6
+ require "net/http"
7
+ require "uri"
8
+
9
+ # Minimal HTTP transport satisfying the generated modules' executor
10
+ # interface: execute(query, variables:) => {"data" => ..., "errors" => ...}
11
+ class GraphWeaver::HttpExecutor
12
+ def initialize(url, headers: {})
13
+ @uri = URI(url)
14
+ @headers = headers
15
+ end
16
+
17
+ def execute(query, variables: {})
18
+ request = Net::HTTP::Post.new(@uri, { "Content-Type" => "application/json" }.merge(@headers))
19
+ request.body = JSON.generate(query:, variables:)
20
+
21
+ response = Net::HTTP.start(@uri.hostname, @uri.port, use_ssl: @uri.scheme == "https") do |http|
22
+ http.request(request)
23
+ end
24
+
25
+ unless response.is_a?(Net::HTTPSuccess)
26
+ raise "HTTP #{response.code}: #{response.body}"
27
+ end
28
+
29
+ JSON.parse(T.must(response.body))
30
+ end
31
+ end
@@ -0,0 +1,20 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "graphql"
5
+ require "json"
6
+
7
+ # Load a schema for codegen from either format a remote service can hand
8
+ # you: an introspection dump (.json) or SDL (.graphql/.gql).
9
+ module GraphWeaver::SchemaLoader
10
+ def self.load(path)
11
+ case File.extname(path)
12
+ when ".json"
13
+ GraphQL::Schema.from_introspection(JSON.parse(File.read(path)))
14
+ when ".graphql", ".gql"
15
+ GraphQL::Schema.from_definition(File.read(path))
16
+ else
17
+ raise ArgumentError, "unsupported schema format: #{path}"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ module GraphWeaver
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,11 @@
1
+ require "graphql"
2
+ require "sorbet-runtime"
3
+
4
+ require_relative "graph_weaver/codegen"
5
+ require_relative "graph_weaver/http_executor"
6
+ require_relative "graph_weaver/schema_loader"
7
+ require_relative "graph_weaver/version"
8
+
9
+ # opt-in: require "graph_weaver/directive_defaults_patch" to fix
10
+ # graphql-ruby dropping directive argument defaults when loading SDL
11
+ # (needed for Apollo supergraph SDL until rmosolgo/graphql-ruby#5659 ships)