graph_weaver 0.7.4 → 0.7.5
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/Gemfile.lock +2 -2
- data/README.md +1 -0
- data/docs/errors.md +5 -2
- data/docs/generated_modules.md +127 -14
- data/docs/getting_started.md +159 -14
- data/docs/migrating.md +119 -0
- data/docs/scalars.md +39 -4
- data/docs/testing.md +3 -1
- data/docs/upgrading.md +21 -1
- data/lib/generators/graph_weaver/install_generator.rb +32 -3
- data/lib/graph_weaver/codegen/aliases.rb +23 -2
- data/lib/graph_weaver/codegen/emit.rb +13 -9
- data/lib/graph_weaver/codegen/enum_type.rb +26 -9
- data/lib/graph_weaver/codegen/nodes.rb +55 -31
- data/lib/graph_weaver/codegen.rb +198 -90
- data/lib/graph_weaver/coerce.rb +1 -1
- data/lib/graph_weaver/federation.rb +1 -6
- data/lib/graph_weaver/hints.rb +20 -5
- data/lib/graph_weaver/in_process.rb +1 -3
- data/lib/graph_weaver/input_struct.rb +21 -6
- data/lib/graph_weaver/internal/subgraphs.rb +1 -10
- data/lib/graph_weaver/internal/unused.rb +32 -7
- data/lib/graph_weaver/internal/values.rb +7 -2
- data/lib/graph_weaver/internal.rb +17 -0
- data/lib/graph_weaver/logging.rb +26 -29
- data/lib/graph_weaver/query_module.rb +20 -5
- data/lib/graph_weaver/railtie.rb +7 -2
- data/lib/graph_weaver/rspec.rb +0 -1
- data/lib/graph_weaver/schema_loader.rb +7 -8
- data/lib/graph_weaver/tasks.rb +60 -5
- data/lib/graph_weaver/testing/fake_client.rb +4 -10
- data/lib/graph_weaver/testing/router.rb +26 -25
- data/lib/graph_weaver/testing.rb +1 -2
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +13 -7
- metadata +3 -2
data/docs/scalars.md
CHANGED
|
@@ -428,7 +428,7 @@ It raises naming every scalar that disagreed and which way:
|
|
|
428
428
|
```
|
|
429
429
|
2 scalar(s) disagree with Catalog::Schema:
|
|
430
430
|
Money: the server refused "12.5", the wire form serialize: writes (expected "12.50 USD")
|
|
431
|
-
Decimal: round-trips lossily — sent
|
|
431
|
+
Decimal: round-trips lossily — sent 123456789.123456789, got back 123456789.1234567
|
|
432
432
|
```
|
|
433
433
|
|
|
434
434
|
The fabricated value is all it has to work with, so pin the one that matters:
|
|
@@ -505,13 +505,48 @@ Two safety properties do the real work:
|
|
|
505
505
|
- **`fallback:` for forward-compat**: `fallback: PetKind::Unknown` makes *casting*
|
|
506
506
|
absorb wire values the server added after you generated, so responses keep
|
|
507
507
|
flowing instead of raising. Inputs stay strict either way: a typo'd input is your
|
|
508
|
-
bug, not drift.
|
|
509
|
-
|
|
510
|
-
|
|
508
|
+
bug, not drift. You don't need an enum of your own for that:
|
|
509
|
+
`GraphWeaver.register_enum("Species", fallback: true)` gives the *generated* enum
|
|
510
|
+
the same forward-compat, [below](#values-the-server-hasnt-told-you-about-yet).
|
|
511
511
|
|
|
512
512
|
The translation tables are emitted into the generated source (`SPECIES_FROM_WIRE` /
|
|
513
513
|
`SPECIES_TO_WIRE`) — reviewable in the diff, no runtime registry.
|
|
514
514
|
|
|
515
|
+
### Values the server hasn't told you about yet
|
|
516
|
+
|
|
517
|
+
A server adding an enum value is a deploy you weren't part of, and by default the
|
|
518
|
+
next response carrying it raises. `fallback: true` says take it anyway:
|
|
519
|
+
|
|
520
|
+
```ruby
|
|
521
|
+
GraphWeaver.register_enum("Species", fallback: true)
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
The generated `Species` gains one member, `Other`, and every wire value the schema
|
|
525
|
+
doesn't declare casts to it — the leniency a union or interface already has for
|
|
526
|
+
free, where a member added upstream lands in the catch-all `Other` its dispatch
|
|
527
|
+
always carries ([generated modules](generated_modules.md#abstract-types)).
|
|
528
|
+
|
|
529
|
+
```ruby
|
|
530
|
+
pet.species # => GraphQLTypes::Species::Other, for "AXOLOTL"
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
Three things follow from that, and they are the whole rule:
|
|
534
|
+
|
|
535
|
+
- **`Other` doesn't carry the value it absorbed.** A `T::Enum` member is a
|
|
536
|
+
singleton, so there is nowhere to put it; the cast writes one debug line
|
|
537
|
+
(`GraphQLTypes::Species absorbed "AXOLOTL" into Other`) and that is the record
|
|
538
|
+
— so a presenter humanising the server's spelling can't, and falls back to a
|
|
539
|
+
label of its own.
|
|
540
|
+
- **Inputs stay strict.** No wire value means `Other`, so a variable carrying it
|
|
541
|
+
is refused rather than sent. Everything else is unchanged: a typo'd input is
|
|
542
|
+
your bug, not drift.
|
|
543
|
+
- **A schema that already declares `OTHER` keeps it.** The declared value stays
|
|
544
|
+
`Other`, and the fallback member is `Other2` — the same rule a union's
|
|
545
|
+
catch-all struct follows when a member is already named `Other`.
|
|
546
|
+
|
|
547
|
+
It rides along with `alias:` on the one registration, and like every registration
|
|
548
|
+
it belongs to one graph.
|
|
549
|
+
|
|
515
550
|
### Two spellings, one value
|
|
516
551
|
|
|
517
552
|
A schema mid-rename declares both `LEGACY_MODE` and `legacy_mode` so old clients
|
data/docs/testing.md
CHANGED
|
@@ -423,7 +423,9 @@ time** — answering one means setting the client's context for the length of th
|
|
|
423
423
|
dispatch, so the identity a request asked for is the identity it gets, whatever
|
|
424
424
|
else is in flight. The lock is the client's own, so it holds however the endpoint
|
|
425
425
|
is mounted. A `context:` hash is served **concurrently**: nothing writes it, so
|
|
426
|
-
there is nothing to serialize.
|
|
426
|
+
there is nothing to serialize. Either way a query reads its context once, so
|
|
427
|
+
every subgraph it hops through runs as the identity it started with, even if
|
|
428
|
+
the router's context is reassigned while it is in flight.
|
|
427
429
|
|
|
428
430
|
### Making the served endpoint fail
|
|
429
431
|
|
data/docs/upgrading.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Upgrading
|
|
2
2
|
|
|
3
3
|
[Regenerate](#regenerate-on-every-upgrade) whichever version you're on, then read
|
|
4
|
-
the one section that is yours: from [0.7.
|
|
4
|
+
the one section that is yours: from [0.7.4](#upgrading-from-074), from
|
|
5
|
+
[0.7.3](#upgrading-from-073), from
|
|
5
6
|
[0.7.1](#upgrading-from-071), from [0.7.0](#upgrading-from-070) or from
|
|
6
7
|
[0.6.1](#upgrading-from-061). Coming from 0.6.0 or older, the path is that
|
|
7
8
|
version's own upgrade notes — read them at the tag they shipped under
|
|
@@ -23,6 +24,25 @@ That's the reminder working, not a false alarm. Generation is deterministic, so
|
|
|
23
24
|
the diff is exactly what the new version emits differently and nothing else —
|
|
24
25
|
worth reading rather than rubber-stamping.
|
|
25
26
|
|
|
27
|
+
## Upgrading from 0.7.4
|
|
28
|
+
|
|
29
|
+
Read the left column and skip what isn't yours; the
|
|
30
|
+
[changelog](../CHANGELOG.md) says why each one moved.
|
|
31
|
+
|
|
32
|
+
| applies if you… | what changed |
|
|
33
|
+
|---|---|
|
|
34
|
+
| spread a shared fragment as a whole field — `grep -rln '{ *\.\.\.[A-Za-z]* *}' app/graphql/queries` | on an object type it now [hoists](generated_modules.md#a-shared-fragment-is-one-type) into `GraphQLTypes` under the fragment's name, as it already did on a union. **App code naming the struct it used to produce (`PetQuery::Result::Pet`) moves to `GraphQLTypes::PetFields`** — one type instead of one per query, so a `T.type_alias { T.any(…) }` written to paper over that goes away. Regenerating and running `srb tc` finds every site |
|
|
35
|
+
| register an `abstract!` mixin — `grep -rn 'extend_type' app config lib`, then check which of those modules call `abstract!` | **generation refuses** where a query doesn't select everything the mixin declares, instead of leaving it for your `srb tc`. The message names the struct, the members and the fixes: select them in that query, or select the type through one shared fragment so a single hoisted struct answers for every query |
|
|
36
|
+
| keep a hand-maintained schema dump on a graph that also names a `client` — `rake graph_weaver:graphs` lists both per graph | `rake graph_weaver:schema:refresh` **rewrites that dump from the client** where it used to refuse and exit 1. If the file is the source of truth, don't run `refresh` for that graph, or drop the client from it |
|
|
37
|
+
| call `Codegen#used_union_names` | it is `used_fragment_names` — the set it reports now includes hoisted object fragments |
|
|
38
|
+
|
|
39
|
+
Then regenerate, and the gate:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
rake graph_weaver:generate
|
|
43
|
+
rake graph_weaver:verify
|
|
44
|
+
```
|
|
45
|
+
|
|
26
46
|
## Upgrading from 0.7.3
|
|
27
47
|
|
|
28
48
|
A patch release. One change can reach an app that never touched it — how a
|
|
@@ -119,6 +119,28 @@ module GraphWeaver
|
|
|
119
119
|
end
|
|
120
120
|
end
|
|
121
121
|
|
|
122
|
+
# The same thing the header and the Exclude above say, told to GitHub:
|
|
123
|
+
# linguist-generated collapses these files in a diff ("Load diff") and
|
|
124
|
+
# drops them from the language breakdown. Display only — they stay
|
|
125
|
+
# versioned, expandable, and `git diff` here is untouched.
|
|
126
|
+
#
|
|
127
|
+
# Written even when there is no .gitattributes, unlike .rubocop.yml: a
|
|
128
|
+
# .gitattributes turns no tooling on, so there is no app this surprises.
|
|
129
|
+
def mark_generated_for_github
|
|
130
|
+
body = File.read(gitattributes) if File.exist?(gitattributes)
|
|
131
|
+
# already marked — a re-run, or done by hand
|
|
132
|
+
globs = linguist_globs.reject { |glob| body&.include?(glob) }
|
|
133
|
+
return if globs.empty?
|
|
134
|
+
|
|
135
|
+
lines = globs.map { |glob| "#{glob} linguist-generated" }.join("\n")
|
|
136
|
+
marks = <<~TEXT + lines + "\n"
|
|
137
|
+
# Machine-written by `rake graph_weaver:generate` — GitHub collapses it in
|
|
138
|
+
# diffs and leaves it out of the language breakdown.
|
|
139
|
+
TEXT
|
|
140
|
+
|
|
141
|
+
body ? append_to_file(GITATTRIBUTES, "\n#{marks}") : create_file(GITATTRIBUTES, marks)
|
|
142
|
+
end
|
|
143
|
+
|
|
122
144
|
# The `graphql:` tags need this require, and it has to be somewhere
|
|
123
145
|
# rspec actually loads. A spec/support file is not: rspec-rails ships
|
|
124
146
|
# the spec/support glob commented out, so the require sat there doing
|
|
@@ -191,6 +213,7 @@ module GraphWeaver
|
|
|
191
213
|
private
|
|
192
214
|
|
|
193
215
|
RUBOCOP_CONFIG = ".rubocop.yml"
|
|
216
|
+
GITATTRIBUTES = ".gitattributes"
|
|
194
217
|
|
|
195
218
|
# rails_helper first: rspec-rails writes both, and only rails_helper
|
|
196
219
|
# has Rails booted by the time the require runs.
|
|
@@ -203,6 +226,8 @@ module GraphWeaver
|
|
|
203
226
|
|
|
204
227
|
def rubocop_config = File.join(GraphWeaver.root, RUBOCOP_CONFIG)
|
|
205
228
|
|
|
229
|
+
def gitattributes = File.join(GraphWeaver.root, GITATTRIBUTES)
|
|
230
|
+
|
|
206
231
|
# Parsed, not counted: a `---` can also be a line inside a block scalar.
|
|
207
232
|
# A file rubocop itself can't read is left to rubocop to complain about.
|
|
208
233
|
def yaml_documents(body)
|
|
@@ -213,9 +238,13 @@ module GraphWeaver
|
|
|
213
238
|
|
|
214
239
|
# Every graph's output directory, so a multi-schema app is covered by
|
|
215
240
|
# the same run — read off the graphs rather than restated here.
|
|
216
|
-
def
|
|
217
|
-
|
|
218
|
-
|
|
241
|
+
def generated_outputs = GraphWeaver.graphs.map(&:output).uniq
|
|
242
|
+
|
|
243
|
+
def generated_globs = generated_outputs.map { |dir| File.join(dir, "**/*") }
|
|
244
|
+
|
|
245
|
+
# gitattributes patterns are gitignore-style, where `dir/**` is
|
|
246
|
+
# everything beneath dir — spelled as git reads it, not as rubocop does.
|
|
247
|
+
def linguist_globs = generated_outputs.map { |dir| File.join(dir, "**") }
|
|
219
248
|
|
|
220
249
|
# This install run is the one moment the user is guaranteed to be
|
|
221
250
|
# reading, and a composed supergraph changes what the next steps are:
|
|
@@ -119,6 +119,7 @@ class GraphWeaver::Codegen
|
|
|
119
119
|
nilable = true
|
|
120
120
|
else
|
|
121
121
|
obj = object_of(cur)
|
|
122
|
+
refuse_hoisted!(node, name, hoisted_of(cur), seg)
|
|
122
123
|
unless obj
|
|
123
124
|
hint = if list_of(cur)
|
|
124
125
|
" — use .first or .last to pick an element"
|
|
@@ -155,6 +156,19 @@ class GraphWeaver::Codegen
|
|
|
155
156
|
ObjectNode::Alias.new(name, expr, type)
|
|
156
157
|
end
|
|
157
158
|
|
|
159
|
+
# A hop landing on a hoisted shared fragment: the struct is another
|
|
160
|
+
# module's, so the path stops here. Which query hoists is a property of how
|
|
161
|
+
# that query spreads the fragment, so optional: still skips it.
|
|
162
|
+
def refuse_hoisted!(node, name, ref, seg)
|
|
163
|
+
return unless ref
|
|
164
|
+
|
|
165
|
+
raise GraphWeaver::Error,
|
|
166
|
+
"alias #{name.inspect} on #{node.graphql_type}: '#{seg}' is inside the shared fragment " \
|
|
167
|
+
"#{ref.class_name}, which hoists to #{@types_namespace}::#{ref.class_name} — a path can't " \
|
|
168
|
+
"read into it. Register the alias on #{ref.graphql_type}, or select a field beside the " \
|
|
169
|
+
"spread to keep the struct local"
|
|
170
|
+
end
|
|
171
|
+
|
|
158
172
|
# Separate "this query didn't select it" from "no query could": a segment
|
|
159
173
|
# the schema doesn't declare on the type is a mistake in the registration,
|
|
160
174
|
# so it raises even for an optional alias — which otherwise turns a typo
|
|
@@ -185,7 +199,7 @@ class GraphWeaver::Codegen
|
|
|
185
199
|
# The leaf's Sorbet type as referenced from the aliased struct. Generated
|
|
186
200
|
# nested constants (structs, enums, unions) must carry the container path,
|
|
187
201
|
# since the delegator's `sig` is emitted in an outer struct where a bare
|
|
188
|
-
# `Sub` wouldn't resolve; scalars, mapped enums, and hoisted
|
|
202
|
+
# `Sub` wouldn't resolve; scalars, mapped enums, and hoisted refs are
|
|
189
203
|
# already top-level. `containers` is the class-name chain to the leaf.
|
|
190
204
|
def qualified_alias_type(node, containers)
|
|
191
205
|
node = node.of if node.is_a?(NonNull)
|
|
@@ -203,10 +217,17 @@ class GraphWeaver::Codegen
|
|
|
203
217
|
# there from the shared enums module — either way, no container prefix
|
|
204
218
|
when EnumNode then node.class_name
|
|
205
219
|
when UnionNode then "#{prefix}#{node.bare_type}"
|
|
206
|
-
else node.bare_type # Scalar, MappedEnum,
|
|
220
|
+
else node.bare_type # Scalar, MappedEnum, a hoisted ref — already top-level
|
|
207
221
|
end
|
|
208
222
|
end
|
|
209
223
|
|
|
224
|
+
# the hoisted-fragment reference a node resolves to (through NON_NULL), or nil
|
|
225
|
+
def hoisted_of(node)
|
|
226
|
+
node = T.let(node, T.untyped)
|
|
227
|
+
node = node.of while node.is_a?(NonNull)
|
|
228
|
+
node if node.is_a?(HoistedRefNode)
|
|
229
|
+
end
|
|
230
|
+
|
|
210
231
|
# the List a node wraps (through NON_NULL), or nil
|
|
211
232
|
def list_of(node)
|
|
212
233
|
node = T.let(node, T.untyped)
|
|
@@ -130,8 +130,8 @@ class GraphWeaver::Codegen
|
|
|
130
130
|
# The shared types artifact as files: one file per type under types/, plus
|
|
131
131
|
# types.rb — the manifest that requires them in the order the runtime needs
|
|
132
132
|
# (see below). One rule for all three kinds, so a schema migration diffs
|
|
133
|
-
# exactly the types it touched whether they're inputs, enums or
|
|
134
|
-
def emit_types_files(
|
|
133
|
+
# exactly the types it touched whether they're inputs, enums or fragments.
|
|
134
|
+
def emit_types_files(fragments)
|
|
135
135
|
files = {}
|
|
136
136
|
# a mapped enum's constants are its wire tables, but the file is still
|
|
137
137
|
# named for the GraphQL enum — one type, one file, whichever it is
|
|
@@ -142,15 +142,15 @@ class GraphWeaver::Codegen
|
|
|
142
142
|
}
|
|
143
143
|
inputs, = ordered_inputs
|
|
144
144
|
structs = inputs.map { |input| type_file(files, input.class_name) { |out| emit_input(input, out, 1) } }
|
|
145
|
-
hoisted =
|
|
145
|
+
hoisted = fragments.map { |node| type_file(files, node.class_name) { |out| emit_nested(node, out, 1) } }
|
|
146
146
|
|
|
147
147
|
out = []
|
|
148
148
|
out << "# typed: strict"
|
|
149
149
|
out << "# frozen_string_literal: true"
|
|
150
150
|
out << ""
|
|
151
151
|
out << "# Generated by GraphWeaver #{GraphWeaver::VERSION} — do not edit. Shared types for this schema —"
|
|
152
|
-
out << "# input types, enums, and
|
|
153
|
-
out << "# per type; query modules alias what they use."
|
|
152
|
+
out << "# input types, enums, and the types hoisted out of shared fragments — one"
|
|
153
|
+
out << "# file per type; query modules alias what they use."
|
|
154
154
|
out << ""
|
|
155
155
|
requires = @requires.uniq.sort
|
|
156
156
|
if requires.any?
|
|
@@ -175,7 +175,7 @@ class GraphWeaver::Codegen
|
|
|
175
175
|
# both spell them bare, and a T::Enum can't be forward-declared the way
|
|
176
176
|
# an input struct can
|
|
177
177
|
if enums.any? && (structs.any? || hoisted.any?)
|
|
178
|
-
out << "# enums first — input structs and
|
|
178
|
+
out << "# enums first — input structs and hoisted fragments spell them bare"
|
|
179
179
|
end
|
|
180
180
|
(enums.sort + structs.sort + hoisted.sort).each do |file|
|
|
181
181
|
out << "require_relative #{file.delete_suffix(".rb").inspect}"
|
|
@@ -210,11 +210,11 @@ class GraphWeaver::Codegen
|
|
|
210
210
|
# assembled from the generator's walked state.
|
|
211
211
|
def emit_module(root, variables, representations = [], operation_name = nil)
|
|
212
212
|
# Every shared type this module names: its variable root inputs, the
|
|
213
|
-
# enums it reached, and the
|
|
214
|
-
#
|
|
213
|
+
# enums it reached, and the fragments it hoisted (aliased so <Name> and
|
|
214
|
+
# <Name>.from_h resolve to the shared module).
|
|
215
215
|
aliases = if @types_namespace
|
|
216
216
|
(shared_input_names(variables) + shared_enum_names +
|
|
217
|
-
@
|
|
217
|
+
@used_fragments.map { |name| camelize(name) }).uniq.sort
|
|
218
218
|
else
|
|
219
219
|
[]
|
|
220
220
|
end
|
|
@@ -338,6 +338,10 @@ class GraphWeaver::Codegen
|
|
|
338
338
|
node.values.each do |value|
|
|
339
339
|
out << "#{pad} #{camelize(value.downcase)} = new(#{value.inspect})"
|
|
340
340
|
end
|
|
341
|
+
if node.fallback?
|
|
342
|
+
out << "#{pad} # every other wire value casts here (register_enum fallback: true)"
|
|
343
|
+
out << "#{pad} #{node.fallback} = new(#{GraphWeaver::Internal::ENUM_FALLBACK_WIRE.inspect})"
|
|
344
|
+
end
|
|
341
345
|
out << "#{pad} end"
|
|
342
346
|
out << "#{pad}end"
|
|
343
347
|
return unless node.aliased?
|
|
@@ -22,6 +22,10 @@ class GraphWeaver::Codegen
|
|
|
22
22
|
# value — both spellings cast, the target is what serializes. It is the
|
|
23
23
|
# whole registration when there is no T::Enum to map onto, and then the
|
|
24
24
|
# generated enum gets one constant for the target and none for the alias.
|
|
25
|
+
#
|
|
26
|
+
# fallback: true is the same type-less form asking for leniency instead:
|
|
27
|
+
# the generated enum gains an Other member and absorbs undeclared wire
|
|
28
|
+
# values into it (see Codegen#enum_values).
|
|
25
29
|
class EnumType
|
|
26
30
|
attr_reader :graphql_name, :type, :fallback, :requires
|
|
27
31
|
|
|
@@ -55,6 +59,10 @@ class GraphWeaver::Codegen
|
|
|
55
59
|
end
|
|
56
60
|
end
|
|
57
61
|
|
|
62
|
+
# register_enum("Species", fallback: true): the generated enum gains an
|
|
63
|
+
# Other member and casts every undeclared wire value to it.
|
|
64
|
+
def generated_fallback? = type.nil? && fallback == true
|
|
65
|
+
|
|
58
66
|
# The wire tables for a mapped enum: [wire value => member, member => the
|
|
59
67
|
# wire value that goes out]. Every spelling casts; an alias's target is the
|
|
60
68
|
# one that serializes.
|
|
@@ -115,19 +123,27 @@ class GraphWeaver::Codegen
|
|
|
115
123
|
|
|
116
124
|
private
|
|
117
125
|
|
|
118
|
-
# Without a T::Enum there is nothing for map:/
|
|
119
|
-
#
|
|
126
|
+
# Without a T::Enum there is nothing for map:/requires: to describe, so
|
|
127
|
+
# alias: and fallback: true are the whole registration.
|
|
120
128
|
def alias_only!(map, fallback, requires)
|
|
121
|
-
if
|
|
129
|
+
if fallback && fallback != true
|
|
130
|
+
raise ArgumentError, "register_enum(#{graphql_name.inspect}, fallback: #{fallback.inspect}): the " \
|
|
131
|
+
"generated enum generates its fallback member too, so say fallback: true. To fall back onto a " \
|
|
132
|
+
"member of your own, pass the T::Enum: " \
|
|
133
|
+
"register_enum(#{graphql_name.inspect}, YourEnum, fallback: YourEnum::Unknown)"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
if @aliases.empty? && !fallback
|
|
122
137
|
raise ArgumentError, "register_enum(#{graphql_name.inspect}) says nothing about #{graphql_name} — " \
|
|
123
|
-
"pass the T::Enum to map it onto,
|
|
138
|
+
"pass the T::Enum to map it onto, alias: { \"old\" => \"NEW\" } to read two wire values as one, " \
|
|
139
|
+
"or fallback: true to absorb values the server adds"
|
|
124
140
|
end
|
|
125
141
|
|
|
126
|
-
extra = { map:,
|
|
142
|
+
extra = { map:, requires: }.compact.keys.first
|
|
127
143
|
if extra
|
|
128
144
|
raise ArgumentError,
|
|
129
|
-
"register_enum(#{graphql_name.inspect}
|
|
130
|
-
"
|
|
145
|
+
"register_enum(#{graphql_name.inspect}) takes no #{extra}: — that describes a T::Enum of your own, " \
|
|
146
|
+
"so pass one: register_enum(#{graphql_name.inspect}, YourEnum, #{extra}: {...})"
|
|
131
147
|
end
|
|
132
148
|
|
|
133
149
|
@requires = []
|
|
@@ -189,8 +205,9 @@ class GraphWeaver::Codegen
|
|
|
189
205
|
|
|
190
206
|
# The enum half of one graph's registrations — see Codegen::Registry.
|
|
191
207
|
class Registry
|
|
192
|
-
# Map a GraphQL enum onto an app-owned T::Enum,
|
|
193
|
-
# spellings into one value
|
|
208
|
+
# Map a GraphQL enum onto an app-owned T::Enum, fold two of its wire
|
|
209
|
+
# spellings into one value, or absorb the ones the server hasn't told you
|
|
210
|
+
# about yet (see EnumType). The one implementation —
|
|
194
211
|
# GraphWeaver.register_enum is a delegate, so the same call reaches it
|
|
195
212
|
# whichever door you came in by.
|
|
196
213
|
#
|
|
@@ -113,11 +113,11 @@ class GraphWeaver::Codegen
|
|
|
113
113
|
|
|
114
114
|
def cast(expr, depth)
|
|
115
115
|
var = "v#{depth}"
|
|
116
|
-
element =
|
|
117
|
-
@of.identity?
|
|
118
|
-
|
|
119
|
-
"#{var}&.then { |v#{depth + 1}| #{@of.cast("v#{depth + 1}", depth + 2)} }"
|
|
120
|
-
|
|
116
|
+
element =
|
|
117
|
+
if @of.identity? then var
|
|
118
|
+
elsif @of.non_null? then @of.cast(var, depth + 1)
|
|
119
|
+
else "#{var}&.then { |v#{depth + 1}| #{@of.cast("v#{depth + 1}", depth + 2)} }"
|
|
120
|
+
end
|
|
121
121
|
|
|
122
122
|
"#{expr}.map { |#{var}| #{element} }"
|
|
123
123
|
end
|
|
@@ -127,11 +127,11 @@ class GraphWeaver::Codegen
|
|
|
127
127
|
|
|
128
128
|
def serialize(expr, depth)
|
|
129
129
|
var = "v#{depth}"
|
|
130
|
-
element =
|
|
131
|
-
@of.serialize_identity?
|
|
132
|
-
|
|
133
|
-
"#{var}&.then { |v#{depth + 1}| #{@of.serialize("v#{depth + 1}", depth + 2)} }"
|
|
134
|
-
|
|
130
|
+
element =
|
|
131
|
+
if @of.serialize_identity? then var
|
|
132
|
+
elsif @of.non_null? then @of.serialize(var, depth + 1)
|
|
133
|
+
else "#{var}&.then { |v#{depth + 1}| #{@of.serialize("v#{depth + 1}", depth + 2)} }"
|
|
134
|
+
end
|
|
135
135
|
|
|
136
136
|
"#{expr}.map { |#{var}| #{element} }"
|
|
137
137
|
end
|
|
@@ -153,11 +153,11 @@ class GraphWeaver::Codegen
|
|
|
153
153
|
|
|
154
154
|
def hash_coerce(expr, depth)
|
|
155
155
|
var = "v#{depth}"
|
|
156
|
-
inner =
|
|
157
|
-
@of.hash_coerce_identity?
|
|
158
|
-
|
|
159
|
-
"#{var}&.then { |v#{depth + 1}| #{@of.hash_coerce("v#{depth + 1}", depth + 2)} }"
|
|
160
|
-
|
|
156
|
+
inner =
|
|
157
|
+
if @of.hash_coerce_identity? then var
|
|
158
|
+
elsif @of.non_null? then @of.hash_coerce(var, depth + 1)
|
|
159
|
+
else "#{var}&.then { |v#{depth + 1}| #{@of.hash_coerce("v#{depth + 1}", depth + 2)} }"
|
|
160
|
+
end
|
|
161
161
|
return "#{expr}.map { |#{var}| #{inner} }" if hash_coerce_identity?
|
|
162
162
|
|
|
163
163
|
# the index is a path segment — `where._and.0._not.species` needs the 0
|
|
@@ -207,10 +207,13 @@ class GraphWeaver::Codegen
|
|
|
207
207
|
class EnumNode < Node
|
|
208
208
|
attr_reader :class_name, :values, :aliases
|
|
209
209
|
|
|
210
|
-
|
|
210
|
+
# fallback: the name of the member unknown wire values cast to (register_enum
|
|
211
|
+
# fallback: true), or nil for a strict enum
|
|
212
|
+
def initialize(class_name, values, aliases = {}, fallback: nil)
|
|
211
213
|
@class_name = class_name
|
|
212
214
|
@values = values
|
|
213
215
|
@aliases = aliases
|
|
216
|
+
@fallback = fallback
|
|
214
217
|
end
|
|
215
218
|
|
|
216
219
|
def bare_type = class_name
|
|
@@ -220,10 +223,19 @@ class GraphWeaver::Codegen
|
|
|
220
223
|
def aliased? = !@aliases.empty?
|
|
221
224
|
def alias_const = "#{GraphWeaver::Inflect.underscore(class_name).upcase}_ALIASES"
|
|
222
225
|
|
|
226
|
+
# register_enum fallback: true — the extra member every value the schema
|
|
227
|
+
# doesn't declare casts to, and the one member a variable can't send
|
|
228
|
+
attr_reader :fallback
|
|
229
|
+
def fallback? = !@fallback.nil?
|
|
230
|
+
def fallback_const = "#{class_name}::#{@fallback}"
|
|
231
|
+
|
|
223
232
|
def cast(expr, _depth)
|
|
224
|
-
"GraphWeaver::Hints.enum(#{class_name}, #{expr}#{
|
|
233
|
+
"GraphWeaver::Hints.enum(#{class_name}, #{expr}#{runtime_args})"
|
|
225
234
|
end
|
|
226
235
|
|
|
236
|
+
# Other serializes to ENUM_FALLBACK_WIRE, which casts back to Other — so a
|
|
237
|
+
# result carrying it still round-trips through #as_json, even though no
|
|
238
|
+
# server would accept that spelling.
|
|
227
239
|
def serialize(expr, _depth)
|
|
228
240
|
"#{expr}.serialize"
|
|
229
241
|
end
|
|
@@ -235,7 +247,11 @@ class GraphWeaver::Codegen
|
|
|
235
247
|
def coerce? = true
|
|
236
248
|
|
|
237
249
|
def coerce(expr)
|
|
238
|
-
"GraphWeaver::InputStruct.enum(#{class_name}, #{expr}#{
|
|
250
|
+
"GraphWeaver::InputStruct.enum(#{class_name}, #{expr}#{runtime_args})"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def runtime_args
|
|
254
|
+
"#{", #{alias_const}" if aliased?}#{", fallback: #{fallback_const}" if fallback?}"
|
|
239
255
|
end
|
|
240
256
|
|
|
241
257
|
def input_type = "T.any(#{class_name}, String)"
|
|
@@ -358,20 +374,22 @@ class GraphWeaver::Codegen
|
|
|
358
374
|
def nested = self
|
|
359
375
|
end
|
|
360
376
|
|
|
361
|
-
# A reference to a
|
|
362
|
-
#
|
|
363
|
-
# <Name
|
|
364
|
-
#
|
|
365
|
-
#
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
377
|
+
# A reference to a type hoisted into the shared types module (a named shared
|
|
378
|
+
# fragment spread as a whole field): the query casts through <Name>.from_h,
|
|
379
|
+
# where <Name> is the alias the query module gives GraphQLTypes::<Name>. The
|
|
380
|
+
# type lives once in the shared module, so the same fragment across queries
|
|
381
|
+
# is one Ruby type — nested is nil, nothing is emitted here.
|
|
382
|
+
class HoistedRefNode < Node
|
|
383
|
+
# graphql_type is carried for refusals alone — an alias path that tries to
|
|
384
|
+
# read into the hoisted struct says which type to register itself on
|
|
385
|
+
attr_reader :class_name, :graphql_type
|
|
386
|
+
|
|
387
|
+
def initialize(class_name, graphql_type = nil)
|
|
371
388
|
@class_name = class_name
|
|
389
|
+
@graphql_type = graphql_type
|
|
372
390
|
end
|
|
373
391
|
|
|
374
|
-
def bare_type =
|
|
392
|
+
def bare_type = class_name
|
|
375
393
|
|
|
376
394
|
def cast(expr, _depth)
|
|
377
395
|
"#{class_name}.from_h(#{expr})"
|
|
@@ -382,6 +400,12 @@ class GraphWeaver::Codegen
|
|
|
382
400
|
end
|
|
383
401
|
end
|
|
384
402
|
|
|
403
|
+
# The same, on an abstract type: the hoisted name is a dispatch module, so
|
|
404
|
+
# the Ruby type is its member union rather than the module itself.
|
|
405
|
+
class UnionRefNode < HoistedRefNode
|
|
406
|
+
def bare_type = "#{class_name}::Type"
|
|
407
|
+
end
|
|
408
|
+
|
|
385
409
|
# An input-object variable: emitted as a module-level T::Struct whose
|
|
386
410
|
# serialize produces the wire hash. Inputs never cast FROM the wire.
|
|
387
411
|
# Joins the coerce protocol so execute kwargs accept plain hashes,
|
|
@@ -454,6 +478,6 @@ class GraphWeaver::Codegen
|
|
|
454
478
|
# The IR is codegen's own vocabulary — every node type is reachable only
|
|
455
479
|
# from inside the walk.
|
|
456
480
|
private_constant :Node, :Scalar, :NonNull, :List, :ObjectNode, :EnumNode,
|
|
457
|
-
:MappedEnum, :NarrowedNode, :UnionNode, :
|
|
458
|
-
:RepresentationNode
|
|
481
|
+
:MappedEnum, :NarrowedNode, :UnionNode, :HoistedRefNode, :UnionRefNode,
|
|
482
|
+
:InputNode, :RepresentationNode
|
|
459
483
|
end
|