graph_weaver 0.3.0 → 0.4.4
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 +88 -0
- data/CLAUDE.md +1 -2
- data/Gemfile.lock +22 -22
- data/docs/federation.md +36 -58
- data/docs/generated_modules.md +40 -4
- data/docs/getting_started.md +52 -0
- data/docs/scalars.md +65 -9
- data/graph_weaver.gemspec +3 -1
- data/lib/graph_weaver/client.rb +6 -4
- data/lib/graph_weaver/codegen/emit.rb +56 -11
- data/lib/graph_weaver/codegen/enum_type.rb +37 -5
- data/lib/graph_weaver/codegen/nodes.rb +27 -3
- data/lib/graph_weaver/codegen.rb +261 -12
- data/lib/graph_weaver/schema_loader.rb +134 -2
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +112 -45
- metadata +3 -4
- data/lib/graph_weaver/directive_defaults_patch.rb +0 -32
|
@@ -26,19 +26,151 @@ module GraphWeaver::SchemaLoader
|
|
|
26
26
|
raise ArgumentError, "unsupported schema content: #{source.lstrip[0, 80].inspect}"
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
build_sdl(source)
|
|
30
30
|
else # a file path
|
|
31
31
|
case File.extname(source)
|
|
32
32
|
when ".json"
|
|
33
33
|
GraphQL::Schema.from_introspection(JSON.parse(File.read(source)))
|
|
34
34
|
when ".graphql", ".gql"
|
|
35
|
-
|
|
35
|
+
build_sdl(File.read(source))
|
|
36
36
|
else
|
|
37
37
|
raise ArgumentError, "unsupported schema format: #{source}"
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# Build a schema from SDL, first stripping Apollo Federation composition
|
|
43
|
+
# machinery when the SDL is a composed supergraph — so a supergraph dump
|
|
44
|
+
# (often the only artifact for the merged graph, and what the router
|
|
45
|
+
# actually serves) loads like any schema, with the federation plumbing
|
|
46
|
+
# gone rather than leaked into schema.types. Plain SDL passes through.
|
|
47
|
+
def self.build_sdl(sdl)
|
|
48
|
+
GraphQL::Schema.from_definition(federation_sdl?(sdl) ? strip_federation(sdl) : sdl)
|
|
49
|
+
end
|
|
50
|
+
private_class_method :build_sdl
|
|
51
|
+
|
|
52
|
+
# A composed Fed2 supergraph is marked by @join__* directives (every merged
|
|
53
|
+
# type carries them); a plain schema has none.
|
|
54
|
+
def self.federation_sdl?(sdl)
|
|
55
|
+
sdl.match?(/@join__\w/)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
FEDERATION_PREFIXES = %w[join__ link__ core__].freeze
|
|
59
|
+
FEDERATION_DIRECTIVES = %w[link core inaccessible].to_set.freeze
|
|
60
|
+
|
|
61
|
+
# Whether a type/directive name belongs to the federation composition layer.
|
|
62
|
+
def self.federation_name?(name)
|
|
63
|
+
!!name && (name.start_with?(*FEDERATION_PREFIXES) || FEDERATION_DIRECTIVES.include?(name))
|
|
64
|
+
end
|
|
65
|
+
private_class_method :federation_name?
|
|
66
|
+
|
|
67
|
+
# Drop the composition machinery from supergraph SDL: the synthetic
|
|
68
|
+
# join__*/link__* type and directive definitions, and every @join__*/@link
|
|
69
|
+
# application on the types that remain. What's left is the merged graph's
|
|
70
|
+
# ordinary type shapes — exactly what codegen reads. Parsing is lenient (it's
|
|
71
|
+
# schema *building* that rejects the join directives), so we parse, filter the
|
|
72
|
+
# AST, and reprint clean SDL for from_definition — no graphql-ruby monkeypatch
|
|
73
|
+
# and no join__* leaking into schema.types.
|
|
74
|
+
def self.strip_federation(sdl)
|
|
75
|
+
doc = GraphQL.parse(sdl)
|
|
76
|
+
defs = remove_inaccessible(doc.definitions)
|
|
77
|
+
.reject { |defn| defn.respond_to?(:name) && federation_name?(defn.name) }
|
|
78
|
+
.map { |defn| strip_federation_directives(defn) }
|
|
79
|
+
GraphQL::Language::Nodes::Document.new(definitions: defs).to_query_string
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Derive the API schema by dropping every element marked @inaccessible —
|
|
83
|
+
# present in the federated graph but hidden from the public API the router
|
|
84
|
+
# serves (its common use is safely rolling out a field on a shared type).
|
|
85
|
+
# Cascades: a field/argument/union-member/implements referencing a removed
|
|
86
|
+
# type goes too, and a type left with no fields/values/members is itself
|
|
87
|
+
# removed — repeated to a fixpoint. So codegen matches exactly what clients
|
|
88
|
+
# can query, without the over-permitting a raw supergraph would allow and
|
|
89
|
+
# without Apollo's JS tooling to subtract the API schema.
|
|
90
|
+
def self.remove_inaccessible(definitions)
|
|
91
|
+
removed = definitions.select { |d| type_definition?(d) && inaccessible?(d) }.map(&:name).to_set
|
|
92
|
+
loop do
|
|
93
|
+
survivors = definitions
|
|
94
|
+
.reject { |d| type_definition?(d) && removed.include?(d.name) }
|
|
95
|
+
.map { |d| prune_inaccessible(d, removed) }
|
|
96
|
+
newly = survivors.select { |d| type_definition?(d) && type_emptied?(d) }.map(&:name)
|
|
97
|
+
return survivors if (newly - removed.to_a).empty?
|
|
98
|
+
|
|
99
|
+
removed.merge(newly)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
private_class_method :remove_inaccessible
|
|
103
|
+
|
|
104
|
+
# A type-system type definition (object/interface/union/enum/input/scalar) —
|
|
105
|
+
# not a directive or schema definition.
|
|
106
|
+
def self.type_definition?(node)
|
|
107
|
+
node.respond_to?(:name) &&
|
|
108
|
+
!node.is_a?(GraphQL::Language::Nodes::DirectiveDefinition) &&
|
|
109
|
+
node.class.name.end_with?("TypeDefinition")
|
|
110
|
+
end
|
|
111
|
+
private_class_method :type_definition?
|
|
112
|
+
|
|
113
|
+
def self.inaccessible?(node)
|
|
114
|
+
node.respond_to?(:directives) && node.directives.any? { |d| d.name == "inaccessible" }
|
|
115
|
+
end
|
|
116
|
+
private_class_method :inaccessible?
|
|
117
|
+
|
|
118
|
+
# Whether pruning left the type with nothing the SDL grammar allows to be
|
|
119
|
+
# empty — a fieldless object/interface/input, a valueless enum, a memberless
|
|
120
|
+
# union — so it must be removed and its references cascaded.
|
|
121
|
+
def self.type_emptied?(node)
|
|
122
|
+
(node.respond_to?(:fields) && node.fields && node.fields.empty?) ||
|
|
123
|
+
(node.is_a?(GraphQL::Language::Nodes::EnumTypeDefinition) && node.values.empty?) ||
|
|
124
|
+
(node.is_a?(GraphQL::Language::Nodes::UnionTypeDefinition) && node.types.empty?)
|
|
125
|
+
end
|
|
126
|
+
private_class_method :type_emptied?
|
|
127
|
+
|
|
128
|
+
# the unwrapped (through NON_NULL/LIST) type name a field or argument references
|
|
129
|
+
def self.unwrapped_type_name(node)
|
|
130
|
+
type = node.type
|
|
131
|
+
type = type.of_type while type.respond_to?(:of_type)
|
|
132
|
+
type.name
|
|
133
|
+
end
|
|
134
|
+
private_class_method :unwrapped_type_name
|
|
135
|
+
|
|
136
|
+
# Remove @inaccessible children and children referencing a removed type,
|
|
137
|
+
# from a type's fields (and their arguments), enum values, union members,
|
|
138
|
+
# and implemented interfaces.
|
|
139
|
+
def self.prune_inaccessible(node, removed)
|
|
140
|
+
gone = lambda do |child|
|
|
141
|
+
inaccessible?(child) || (child.respond_to?(:type) && removed.include?(unwrapped_type_name(child)))
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
changes = {}
|
|
145
|
+
if node.respond_to?(:fields) && node.fields
|
|
146
|
+
changes[:fields] = node.fields.reject(&gone).map do |field|
|
|
147
|
+
args = field.respond_to?(:arguments) && field.arguments
|
|
148
|
+
args && args.any? ? field.merge(arguments: args.reject(&gone)) : field
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
changes[:values] = node.values.reject(&gone) if node.respond_to?(:values) && node.values
|
|
152
|
+
changes[:types] = node.types.reject { |t| removed.include?(t.name) } if node.respond_to?(:types) && node.types
|
|
153
|
+
if node.respond_to?(:interfaces) && node.interfaces
|
|
154
|
+
changes[:interfaces] = node.interfaces.reject { |i| removed.include?(i.name) }
|
|
155
|
+
end
|
|
156
|
+
changes.empty? ? node : node.merge(changes)
|
|
157
|
+
end
|
|
158
|
+
private_class_method :prune_inaccessible
|
|
159
|
+
|
|
160
|
+
# Recursively remove @join__*/@link applications from a definition and its
|
|
161
|
+
# fields, arguments, and enum values.
|
|
162
|
+
def self.strip_federation_directives(node)
|
|
163
|
+
changes = {}
|
|
164
|
+
if node.respond_to?(:directives) && node.directives
|
|
165
|
+
changes[:directives] = node.directives.reject { |d| federation_name?(d.name) }
|
|
166
|
+
end
|
|
167
|
+
changes[:fields] = node.fields.map { |c| strip_federation_directives(c) } if node.respond_to?(:fields) && node.fields
|
|
168
|
+
changes[:arguments] = node.arguments.map { |c| strip_federation_directives(c) } if node.respond_to?(:arguments) && node.arguments
|
|
169
|
+
changes[:values] = node.values.map { |c| strip_federation_directives(c) } if node.respond_to?(:values) && node.values
|
|
170
|
+
changes.empty? ? node : node.merge(changes)
|
|
171
|
+
end
|
|
172
|
+
private_class_method :strip_federation_directives
|
|
173
|
+
|
|
42
174
|
# Run the standard introspection query through a transport and build a
|
|
43
175
|
# schema from the result:
|
|
44
176
|
#
|
data/lib/graph_weaver/version.rb
CHANGED
data/lib/graph_weaver.rb
CHANGED
|
@@ -17,9 +17,6 @@ require_relative "graph_weaver/railtie" if defined?(::Rails::Railtie)
|
|
|
17
17
|
|
|
18
18
|
# opt-in extras:
|
|
19
19
|
# require "graph_weaver/transport/faraday" # Faraday transport
|
|
20
|
-
# require "graph_weaver/directive_defaults_patch" # fix graphql-ruby
|
|
21
|
-
# dropping directive argument defaults when loading SDL (needed for
|
|
22
|
-
# Apollo supergraph SDL until rmosolgo/graphql-ruby#5659 ships)
|
|
23
20
|
module GraphWeaver
|
|
24
21
|
class << self
|
|
25
22
|
# A client for one GraphQL server — transport, schema, and scoped
|
|
@@ -67,7 +64,7 @@ module GraphWeaver
|
|
|
67
64
|
#
|
|
68
65
|
# The singular accessors read the first entry (the default target
|
|
69
66
|
# for generate! and the rake tasks); assigning one replaces the list.
|
|
70
|
-
attr_writer :queries_paths, :generated_paths, :schema_path
|
|
67
|
+
attr_writer :queries_paths, :generated_paths, :schema_path, :fragments_paths
|
|
71
68
|
|
|
72
69
|
# Entries may be glob patterns — the generated default also matches
|
|
73
70
|
# per-schema layouts (app/graphql/github/generated). Queries stay
|
|
@@ -75,8 +72,14 @@ module GraphWeaver
|
|
|
75
72
|
def queries_paths = @queries_paths ||= ["app/graphql/queries"]
|
|
76
73
|
def generated_paths = @generated_paths ||= ["app/graphql/generated", "app/graphql/*/generated"]
|
|
77
74
|
|
|
75
|
+
# Reusable named fragments, defined once and available to every query —
|
|
76
|
+
# each query inlines only the ones it (transitively) spreads, so the sent
|
|
77
|
+
# query stays self-contained.
|
|
78
|
+
def fragments_paths = @fragments_paths ||= ["app/graphql/fragments"]
|
|
79
|
+
|
|
78
80
|
def queries_path = queries_paths.first
|
|
79
81
|
def generated_path = generated_paths.first
|
|
82
|
+
def fragments_path = fragments_paths.first
|
|
80
83
|
|
|
81
84
|
def queries_path=(path)
|
|
82
85
|
@queries_paths = path.nil? ? nil : [path]
|
|
@@ -88,26 +91,35 @@ module GraphWeaver
|
|
|
88
91
|
|
|
89
92
|
def schema_path = @schema_path || "app/graphql/schema.json"
|
|
90
93
|
|
|
91
|
-
# The shared-inputs module
|
|
92
|
-
# per generate!, or let
|
|
93
|
-
# directory above generated/ names the schema in
|
|
94
|
-
# layouts (app/graphql/github/generated => GithubInputs
|
|
95
|
-
# conventional layout (and anything unrecognizable)
|
|
96
|
-
# GraphQLInputs.
|
|
97
|
-
attr_writer :inputs_module
|
|
94
|
+
# The shared-inputs / shared-unions module names: set them globally, pass
|
|
95
|
+
# inputs_module:/unions_module: per generate!, or let them derive from the
|
|
96
|
+
# output path — the directory above generated/ names the schema in
|
|
97
|
+
# multi-schema layouts (app/graphql/github/generated => GithubInputs /
|
|
98
|
+
# GithubUnions); the conventional layout (and anything unrecognizable)
|
|
99
|
+
# stays GraphQLInputs / GraphQLUnions.
|
|
100
|
+
attr_writer :inputs_module, :unions_module
|
|
98
101
|
|
|
99
102
|
def inputs_module(output = generated_path)
|
|
100
|
-
|
|
103
|
+
@inputs_module || derive_module("Inputs", output)
|
|
104
|
+
end
|
|
101
105
|
|
|
106
|
+
def unions_module(output = generated_path)
|
|
107
|
+
@unions_module || derive_module("Unions", output)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Name a shared module from the output path: <Schema><suffix> in a
|
|
111
|
+
# multi-schema layout, else GraphQL<suffix>.
|
|
112
|
+
def derive_module(suffix, output)
|
|
102
113
|
segments = File.expand_path(output.to_s).split(File::SEPARATOR)
|
|
103
114
|
segments.pop if segments.last == "generated"
|
|
104
115
|
parent = segments.last.to_s
|
|
105
116
|
if parent.match?(/\A[a-zA-Z]\w*\z/) && !%w[graphql app lib spec support test].include?(parent)
|
|
106
|
-
"#{Inflect.camelize(parent)}
|
|
117
|
+
"#{Inflect.camelize(parent)}#{suffix}"
|
|
107
118
|
else
|
|
108
|
-
"
|
|
119
|
+
"GraphQL#{suffix}"
|
|
109
120
|
end
|
|
110
121
|
end
|
|
122
|
+
private :derive_module
|
|
111
123
|
|
|
112
124
|
# Generate every .graphql query in a directory into checked-in Ruby
|
|
113
125
|
# files. Paths default to the conventions above; schema: defaults to
|
|
@@ -118,11 +130,12 @@ module GraphWeaver
|
|
|
118
130
|
# person.graphql => person_query.rb defining PersonQuery. Returns the
|
|
119
131
|
# written paths. Pair with a freshness spec (docs/generated_modules.md).
|
|
120
132
|
def generate!(schema: nil, queries: queries_path, output: generated_path, client: nil,
|
|
121
|
-
|
|
133
|
+
inputs_module: nil, unions_module: nil)
|
|
122
134
|
schema ||= locate_schema!
|
|
123
135
|
inputs_module ||= self.inputs_module(output)
|
|
136
|
+
unions_module ||= self.unions_module(output)
|
|
124
137
|
|
|
125
|
-
plan = generation_plan(queries:, schema:, client:,
|
|
138
|
+
plan = generation_plan(queries:, schema:, client:, inputs_module:, unions_module:)
|
|
126
139
|
written = plan.map do |filename, source|
|
|
127
140
|
target = File.join(output, filename)
|
|
128
141
|
FileUtils.mkdir_p(File.dirname(target))
|
|
@@ -131,9 +144,9 @@ module GraphWeaver
|
|
|
131
144
|
target
|
|
132
145
|
end
|
|
133
146
|
|
|
134
|
-
# a type dropped from the schema
|
|
135
|
-
# inputs/
|
|
136
|
-
(
|
|
147
|
+
# a type dropped from the schema (or a union no longer hoisted) must not
|
|
148
|
+
# linger as a stale file — inputs/ and unions.rb are wholly generated
|
|
149
|
+
(shared_artifacts(output) - written).each do |orphan|
|
|
137
150
|
File.delete(orphan)
|
|
138
151
|
log(:info) { "pruned #{orphan}" }
|
|
139
152
|
end
|
|
@@ -141,6 +154,13 @@ module GraphWeaver
|
|
|
141
154
|
written
|
|
142
155
|
end
|
|
143
156
|
|
|
157
|
+
# The wholly-generated shared-artifact files under output (inputs/*.rb and
|
|
158
|
+
# unions.rb) — safe to prune when regeneration no longer produces them.
|
|
159
|
+
def shared_artifacts(output)
|
|
160
|
+
Dir[File.join(output, "inputs", "*.rb")] + Dir[File.join(output, "unions.rb")]
|
|
161
|
+
end
|
|
162
|
+
private :shared_artifacts
|
|
163
|
+
|
|
144
164
|
# The freshness guard: raise unless every generated file matches what
|
|
145
165
|
# the current schema + queries + scalar registrations would produce.
|
|
146
166
|
# One line in a spec, or `rake graph_weaver:verify` in CI:
|
|
@@ -149,16 +169,17 @@ module GraphWeaver
|
|
|
149
169
|
# GraphWeaver.verify_generated!
|
|
150
170
|
# end
|
|
151
171
|
def verify_generated!(schema: nil, queries: queries_path, output: generated_path, client: nil,
|
|
152
|
-
|
|
172
|
+
inputs_module: nil, unions_module: nil)
|
|
153
173
|
schema ||= locate_schema!
|
|
154
174
|
inputs_module ||= self.inputs_module(output)
|
|
155
|
-
|
|
175
|
+
unions_module ||= self.unions_module(output)
|
|
176
|
+
plan = generation_plan(queries:, schema:, client:, inputs_module:, unions_module:)
|
|
156
177
|
stale = plan.filter_map do |filename, source|
|
|
157
178
|
target = File.join(output, filename)
|
|
158
179
|
target unless File.exist?(target) && File.read(target) == source
|
|
159
180
|
end
|
|
160
|
-
# strays: a
|
|
161
|
-
stale +=
|
|
181
|
+
# strays: a shared-artifact file the current schema + queries no longer produce
|
|
182
|
+
stale += shared_artifacts(output) - plan.map { |f, _| File.join(output, f) }
|
|
162
183
|
|
|
163
184
|
unless stale.empty?
|
|
164
185
|
raise Error, "stale generated queries — regenerate (rake graph_weaver:generate): #{stale.join(", ")}"
|
|
@@ -192,34 +213,49 @@ module GraphWeaver
|
|
|
192
213
|
end
|
|
193
214
|
private :locate_schema!
|
|
194
215
|
|
|
195
|
-
# (filename, source) per artifact
|
|
196
|
-
#
|
|
197
|
-
# aliasing what they use — the
|
|
198
|
-
# duplicated bool_exp structs
|
|
199
|
-
|
|
200
|
-
|
|
216
|
+
# (filename, source) per artifact. Every variable type is emitted once into
|
|
217
|
+
# inputs.rb, and each named shared fragment spread as a whole-union field
|
|
218
|
+
# once into unions.rb, with query modules aliasing what they use — the
|
|
219
|
+
# difference between hundreds of duplicated bool_exp structs (or the same
|
|
220
|
+
# union re-typed per query) and one copy per schema. (Single-query parse
|
|
221
|
+
# inlines both — there's no cross-query set to share against.)
|
|
222
|
+
def generation_plan(queries:, schema:, client:, inputs_module: self.inputs_module,
|
|
223
|
+
unions_module: self.unions_module, fragments: fragments_paths)
|
|
201
224
|
used = { inputs: [], enums: [], mapped: [] }
|
|
225
|
+
used_unions = []
|
|
226
|
+
shared = Codegen.load_fragments(fragments)
|
|
202
227
|
|
|
203
228
|
plan = Dir[File.join(queries, "*.graphql")].sort.map do |path|
|
|
204
229
|
base = File.basename(path, ".graphql")
|
|
230
|
+
source = File.read(path)
|
|
205
231
|
codegen = Codegen.new(
|
|
206
232
|
schema:,
|
|
207
|
-
query:
|
|
233
|
+
query: Codegen.inline_fragments(source, shared),
|
|
208
234
|
module_name: "#{Inflect.camelize(base)}Query",
|
|
209
235
|
client:,
|
|
210
|
-
inputs_namespace:
|
|
236
|
+
inputs_namespace: inputs_module,
|
|
237
|
+
unions_namespace: unions_module,
|
|
238
|
+
hoistable_unions: Codegen.shared_fragment_spreads(source, shared),
|
|
211
239
|
)
|
|
212
|
-
|
|
240
|
+
out = codegen.generate
|
|
213
241
|
codegen.variable_type_names.each { |kind, names| used[kind] |= names }
|
|
214
|
-
|
|
242
|
+
used_unions |= codegen.used_union_names
|
|
243
|
+
["#{base}_query.rb", out]
|
|
215
244
|
end
|
|
216
245
|
|
|
217
|
-
if
|
|
218
|
-
|
|
219
|
-
schema:, module_name:
|
|
246
|
+
if inputs_module && used.values.any?(&:any?)
|
|
247
|
+
inputs = Codegen.generate_inputs(
|
|
248
|
+
schema:, module_name: inputs_module,
|
|
220
249
|
input_types: used[:inputs], enum_types: used[:enums] + used[:mapped],
|
|
221
250
|
)
|
|
222
|
-
plan =
|
|
251
|
+
plan = inputs.to_a + plan
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
if unions_module && used_unions.any?
|
|
255
|
+
unions = Codegen.generate_unions(
|
|
256
|
+
schema:, module_name: unions_module, fragments: shared, names: used_unions,
|
|
257
|
+
)
|
|
258
|
+
plan = unions.to_a + plan
|
|
223
259
|
end
|
|
224
260
|
|
|
225
261
|
plan
|
|
@@ -238,6 +274,25 @@ module GraphWeaver
|
|
|
238
274
|
# a registration always wins.
|
|
239
275
|
attr_accessor :auto_coerce
|
|
240
276
|
|
|
277
|
+
# Whether generated modules/structs emit `extend T::Sig` (so `sig`
|
|
278
|
+
# resolves standalone). Default (nil) auto-detects: an app that globally
|
|
279
|
+
# injects T::Sig (`class Module; include T::Sig`) makes the per-struct
|
|
280
|
+
# extend redundant — rubocop's Sorbet/RedundantExtendTSig flags it — so
|
|
281
|
+
# generation skips it. Force with true/false. Resolved at generation time.
|
|
282
|
+
#
|
|
283
|
+
# GraphWeaver.extend_t_sig = false # never emit (rely on a global include)
|
|
284
|
+
attr_writer :extend_t_sig
|
|
285
|
+
|
|
286
|
+
# The resolved boolean codegen uses: the explicit setting, else emit
|
|
287
|
+
# unless T::Sig is globally injected into Module.
|
|
288
|
+
def extend_t_sig?
|
|
289
|
+
@extend_t_sig.nil? ? !global_tsig? : @extend_t_sig
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Whether the host app has globally injected T::Sig into every module
|
|
293
|
+
# (`class Module; include T::Sig`) — extracted so it's stubbable in tests.
|
|
294
|
+
def global_tsig? = Module.include?(T::Sig)
|
|
295
|
+
|
|
241
296
|
# Teach the generator how a GraphQL custom scalar deserializes into a
|
|
242
297
|
# rich Ruby object (and serializes back onto the wire when used as a
|
|
243
298
|
# variable):
|
|
@@ -256,7 +311,15 @@ module GraphWeaver
|
|
|
256
311
|
# raw input (e.g. "12.00"), running the latter through the cast before
|
|
257
312
|
# serializing — it raises on bad input, so some safety survives. Built-in
|
|
258
313
|
# scalars are pre-registered the same way, so this also overrides them.
|
|
259
|
-
#
|
|
314
|
+
#
|
|
315
|
+
# Pass a `Type.field` coordinate instead of a scalar name to override just
|
|
316
|
+
# that one field — so the same scalar can deserialize as different Ruby
|
|
317
|
+
# types across fields (a `Date` for `User.birthday`, a `Time` elsewhere):
|
|
318
|
+
#
|
|
319
|
+
# GraphWeaver.register_scalar("User.birthday", Date)
|
|
320
|
+
#
|
|
321
|
+
# A field-level override wins over the scalar-name registration. Same
|
|
322
|
+
# signature either way. Call before generating.
|
|
260
323
|
def register_scalar(graphql_name, type, cast: nil, serialize: nil, requires: nil, coerce: nil)
|
|
261
324
|
Codegen.register_scalar(graphql_name, type, cast:, serialize:, requires:, coerce:)
|
|
262
325
|
end
|
|
@@ -282,13 +345,15 @@ module GraphWeaver
|
|
|
282
345
|
end
|
|
283
346
|
|
|
284
347
|
# Include app-owned helper modules into every struct generated from a
|
|
285
|
-
# GraphQL type — derived values live as methods next to the honest
|
|
286
|
-
#
|
|
348
|
+
# GraphQL type — derived values live as methods next to the honest wire
|
|
349
|
+
# data, on the struct at runtime (fakes/cassettes included):
|
|
287
350
|
#
|
|
288
351
|
# GraphWeaver.extend_type("Pet", PetHelpers)
|
|
289
352
|
#
|
|
290
|
-
#
|
|
291
|
-
#
|
|
353
|
+
# A helper that reads a wire field is checked by srb tc in the module's
|
|
354
|
+
# own scope, not the struct's, so the field won't resolve — write it
|
|
355
|
+
# # typed: false or reach it via T.unsafe(self). Or build the mixin inline
|
|
356
|
+
# with a block (module_eval'd into an auto-named module — invisible to srb):
|
|
292
357
|
#
|
|
293
358
|
# GraphWeaver.extend_type("Pet") do
|
|
294
359
|
# def display_name = "#{name} the pet"
|
|
@@ -296,8 +361,8 @@ module GraphWeaver
|
|
|
296
361
|
#
|
|
297
362
|
# Additive (repeated and client-scoped registrations stack). Global;
|
|
298
363
|
# client.extend_type scopes to one client.
|
|
299
|
-
def extend_type(graphql_name, *mixins, requires: nil, &block)
|
|
300
|
-
Codegen.extend_type(graphql_name, *mixins, requires:, &block)
|
|
364
|
+
def extend_type(graphql_name, *mixins, requires: nil, **kw, &block)
|
|
365
|
+
Codegen.extend_type(graphql_name, *mixins, requires:, **kw, &block)
|
|
301
366
|
end
|
|
302
367
|
|
|
303
368
|
# Restore the built-in scalars, dropping every custom registration —
|
|
@@ -322,11 +387,13 @@ module GraphWeaver
|
|
|
322
387
|
# falling back to "Query" for anonymous operations — collisions are
|
|
323
388
|
# impossible since each parse gets its own container). Pass name: to
|
|
324
389
|
# override, client: to bake the module's default client/transport.
|
|
325
|
-
def parse(schema:, query:, name: nil, client: nil, scalars: nil, enums: nil, types: nil
|
|
390
|
+
def parse(schema:, query:, name: nil, client: nil, scalars: nil, enums: nil, types: nil,
|
|
391
|
+
fragments: fragments_paths)
|
|
326
392
|
if query.end_with?(".graphql", ".gql")
|
|
327
393
|
name ||= "#{Inflect.camelize(File.basename(query, ".*"))}Query"
|
|
328
394
|
query = File.read(query)
|
|
329
395
|
end
|
|
396
|
+
query = Codegen.inline_fragments(query, Codegen.load_fragments(fragments))
|
|
330
397
|
|
|
331
398
|
Codegen.parse(schema:, query:, module_name: name, client:, scalars:, enums:, types:)
|
|
332
399
|
end
|
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.
|
|
4
|
+
version: 0.4.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Pepper
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version:
|
|
18
|
+
version: 2.6.7
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version:
|
|
25
|
+
version: 2.6.7
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: sorbet-runtime
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -253,7 +253,6 @@ files:
|
|
|
253
253
|
- lib/graph_weaver/codegen/enum_type.rb
|
|
254
254
|
- lib/graph_weaver/codegen/nodes.rb
|
|
255
255
|
- lib/graph_weaver/codegen/scalar_type.rb
|
|
256
|
-
- lib/graph_weaver/directive_defaults_patch.rb
|
|
257
256
|
- lib/graph_weaver/errors.rb
|
|
258
257
|
- lib/graph_weaver/hints.rb
|
|
259
258
|
- lib/graph_weaver/inflect.rb
|
|
@@ -1,32 +0,0 @@
|
|
|
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)
|