graph_weaver 0.7.5 → 0.7.6
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/docs/errors.md +7 -3
- data/docs/generated_modules.md +10 -6
- data/docs/getting_started.md +34 -17
- data/docs/logging.md +79 -35
- data/docs/migrating.md +11 -4
- data/docs/scalars.md +12 -3
- data/docs/testing.md +75 -13
- data/docs/upgrading.md +25 -3
- data/examples/README.md +4 -2
- data/examples/github/generate.rb +22 -8
- data/examples/github/generated/star_mutation.rb +2 -2
- data/examples/github/generated/stargazers_query.rb +2 -2
- data/examples/github/generated/starred_query.rb +2 -2
- data/examples/github/run.rb +1 -0
- data/examples/github/setup.rb +16 -8
- data/graph_weaver.gemspec +15 -6
- data/lib/generators/graph_weaver/install_generator.rb +20 -2
- data/lib/graph_weaver/client.rb +0 -23
- data/lib/graph_weaver/codegen/aliases.rb +15 -3
- data/lib/graph_weaver/codegen/emit.rb +7 -6
- data/lib/graph_weaver/codegen/enum_type.rb +26 -2
- data/lib/graph_weaver/codegen/nodes.rb +21 -2
- data/lib/graph_weaver/codegen.rb +106 -54
- data/lib/graph_weaver/graph.rb +55 -5
- data/lib/graph_weaver/in_process.rb +1 -1
- data/lib/graph_weaver/input_struct.rb +29 -4
- data/lib/graph_weaver/internal/overrides.rb +126 -14
- data/lib/graph_weaver/internal/test_clients.rb +29 -7
- data/lib/graph_weaver/internal/unused.rb +30 -11
- data/lib/graph_weaver/internal/values.rb +17 -5
- data/lib/graph_weaver/internal.rb +6 -6
- data/lib/graph_weaver/log_subscriber.rb +27 -17
- data/lib/graph_weaver/logging.rb +89 -53
- data/lib/graph_weaver/parsing.rb +32 -3
- data/lib/graph_weaver/query_module.rb +48 -8
- data/lib/graph_weaver/rspec.rb +41 -17
- data/lib/graph_weaver/schema_diff.rb +24 -5
- data/lib/graph_weaver/schema_loader.rb +22 -9
- data/lib/graph_weaver/tasks.rb +71 -44
- data/lib/graph_weaver/testing/fake_client.rb +24 -21
- data/lib/graph_weaver/testing.rb +27 -7
- data/lib/graph_weaver/transport.rb +1 -1
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +59 -35
- metadata +2 -2
data/docs/upgrading.md
CHANGED
|
@@ -1,8 +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.
|
|
5
|
-
[0.7.3](#upgrading-from-073), from
|
|
4
|
+
the one section that is yours: from [0.7.5](#upgrading-from-075), from
|
|
5
|
+
[0.7.4](#upgrading-from-074), from [0.7.3](#upgrading-from-073), from
|
|
6
6
|
[0.7.1](#upgrading-from-071), from [0.7.0](#upgrading-from-070) or from
|
|
7
7
|
[0.6.1](#upgrading-from-061). Coming from 0.6.0 or older, the path is that
|
|
8
8
|
version's own upgrade notes — read them at the tag they shipped under
|
|
@@ -24,6 +24,28 @@ That's the reminder working, not a false alarm. Generation is deterministic, so
|
|
|
24
24
|
the diff is exactly what the new version emits differently and nothing else —
|
|
25
25
|
worth reading rather than rubber-stamping.
|
|
26
26
|
|
|
27
|
+
## Upgrading from 0.7.5
|
|
28
|
+
|
|
29
|
+
A patch release. The regenerate is what changes behaviour — every module gains
|
|
30
|
+
the per-call event — and the rest reaches an app only where it already relied
|
|
31
|
+
on the old wording. Read the left column and skip what isn't yours; the
|
|
32
|
+
[changelog](../CHANGELOG.md) says why each one moved.
|
|
33
|
+
|
|
34
|
+
| applies if you… | what changed |
|
|
35
|
+
|---|---|
|
|
36
|
+
| subscribe to `execute.graph_weaver`, or read `GraphWeaver::LogSubscriber`'s line | a second event, [`operation.graph_weaver`](logging.md#instrumentation), covers one `execute` start to typed result — retries, backoff and the cast — and the request events nest inside it. **The info log line is the operation's now**: `failed GraphWeaver::CastError` where it read `ok`, present under a `graphql:` tag, one line per retried call; the per-attempt line writes at debug. A module generated by 0.7.5 emits neither until regenerated |
|
|
37
|
+
| spread a shared fragment on an interface or union that selects no `... on`, or exactly one — `grep -rn 'fragment .* on' app/graphql/fragments` | it [hoists](generated_modules.md#abstract-types) into `GraphQLTypes::<Fragment>` like every other shared fragment; app code naming `YourQuery::Result::Field` moves, and `srb tc` finds each site |
|
|
38
|
+
| run `rake graph_weaver:queries:check` in CI for a graph that names its own `schema` — `rake graph_weaver:graphs` lists them | it re-introspects that graph's server (the dump's url, or the client's url or schema class) where it used to check the committed file, so CI needs the same reach a single-schema app already had. The verdict names what it checked against either way |
|
|
39
|
+
| keep a composed supergraph on a graph that also names a `client` | `schema:refresh` steps over it with the recompose line where 0.7.5 could have overwritten it with the API schema |
|
|
40
|
+
| parse `rake graph_weaver:unused` output, or run it `STRICT=1` | a `before_action`-loaded `@result` is credited again, and the coordinate printed comes from the query, not from any word in the file |
|
|
41
|
+
|
|
42
|
+
Then regenerate, and the gate:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
rake graph_weaver:generate
|
|
46
|
+
rake graph_weaver:verify
|
|
47
|
+
```
|
|
48
|
+
|
|
27
49
|
## Upgrading from 0.7.4
|
|
28
50
|
|
|
29
51
|
Read the left column and skip what isn't yours; the
|
|
@@ -227,7 +249,7 @@ only one here that shows up in production rather than in your code.
|
|
|
227
249
|
Nothing raises: a subscriber comparing it to an Integer just stops matching. **A
|
|
228
250
|
subscriber that branched on a 4xx/5xx reads `:http_status` now**, which is nil
|
|
229
251
|
in-process. The whole payload is a documented contract —
|
|
230
|
-
[logging](logging.md#the-payload).
|
|
252
|
+
[logging](logging.md#the-request-payload).
|
|
231
253
|
- **`respond_to?` on a result struct no longer answers true for a name that doesn't
|
|
232
254
|
exist.** It used to say true for any near miss, which broke the standard
|
|
233
255
|
duck-typing guard. **A branch that read the old answer now takes the other path**,
|
data/examples/README.md
CHANGED
|
@@ -34,5 +34,7 @@ looks like an app:
|
|
|
34
34
|
fellow stargazers.
|
|
35
35
|
|
|
36
36
|
Regeneration introspects GitHub's schema (a few seconds, cached to a gitignored
|
|
37
|
-
`github/schema.json`);
|
|
38
|
-
modules
|
|
37
|
+
`github/schema.json`); once that dump is there the repo's own `bin/generate`
|
|
38
|
+
refreshes these modules along with every other generated file, offline. `run.rb`
|
|
39
|
+
alone never introspects, because the generated modules already carry their
|
|
40
|
+
types.
|
data/examples/github/generate.rb
CHANGED
|
@@ -6,15 +6,29 @@
|
|
|
6
6
|
# the same workflow `rake graph_weaver:generate` runs in an app:
|
|
7
7
|
#
|
|
8
8
|
# examples/github/generate.rb
|
|
9
|
+
#
|
|
10
|
+
# bin/generate calls generate! too, off the cached dump, so these files are
|
|
11
|
+
# regenerated by the build like every other checked-in generated file.
|
|
9
12
|
require_relative "setup"
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
module GithubExample
|
|
15
|
+
# The graph, declared once for both doors: this script, which introspects
|
|
16
|
+
# GitHub, and bin/generate, which passes the dump that introspection cached.
|
|
17
|
+
def self.generate!(schema_source)
|
|
18
|
+
GraphWeaver.graph :github do
|
|
19
|
+
schema schema_source
|
|
20
|
+
queries File.join(__dir__, "queries")
|
|
21
|
+
output File.join(__dir__, "generated")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
GraphWeaver.generate!
|
|
25
|
+
GraphWeaver.changed_files
|
|
26
|
+
end
|
|
15
27
|
end
|
|
16
28
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
changed
|
|
20
|
-
|
|
29
|
+
if $PROGRAM_NAME == __FILE__
|
|
30
|
+
GithubExample.connect!
|
|
31
|
+
changed = GithubExample.generate!(GraphWeaver.client.schema)
|
|
32
|
+
changed.each { |path| puts "wrote #{path}" }
|
|
33
|
+
puts "already up to date" if changed.empty?
|
|
34
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
# Generated by GraphWeaver 0.7.
|
|
4
|
+
# Generated by GraphWeaver 0.7.6 — do not edit.
|
|
5
5
|
|
|
6
6
|
module StarMutation
|
|
7
7
|
extend T::Sig
|
|
@@ -117,7 +117,7 @@ module StarMutation
|
|
|
117
117
|
"id" => GraphWeaver::Coerce.variable("id", OPERATION_NAME, id) { |v| GraphWeaver::Coerce.id(v, "ID") },
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
dispatch(variables, client:) { |raw| from_response(raw) }
|
|
121
121
|
end
|
|
122
122
|
|
|
123
123
|
sig { params(id: String, client: T.untyped).returns(Result).checked(:never) }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
# Generated by GraphWeaver 0.7.
|
|
4
|
+
# Generated by GraphWeaver 0.7.6 — do not edit.
|
|
5
5
|
|
|
6
6
|
require "time"
|
|
7
7
|
|
|
@@ -257,7 +257,7 @@ module StargazersQuery
|
|
|
257
257
|
"first" => GraphWeaver::Coerce.variable("first", OPERATION_NAME, first) { |v| GraphWeaver::Coerce.integer(v, "Int") },
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
|
|
260
|
+
dispatch(variables, client:) { |raw| from_response(raw) }
|
|
261
261
|
end
|
|
262
262
|
|
|
263
263
|
sig { params(owner: String, name: String, first: Integer, client: T.untyped).returns(Result).checked(:never) }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
# Generated by GraphWeaver 0.7.
|
|
4
|
+
# Generated by GraphWeaver 0.7.6 — do not edit.
|
|
5
5
|
|
|
6
6
|
module StarredQuery
|
|
7
7
|
extend T::Sig
|
|
@@ -150,7 +150,7 @@ module StarredQuery
|
|
|
150
150
|
"first" => GraphWeaver::Coerce.variable("first", OPERATION_NAME, first) { |v| GraphWeaver::Coerce.integer(v, "Int") },
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
dispatch(variables, client:) { |raw| from_response(raw) }
|
|
154
154
|
end
|
|
155
155
|
|
|
156
156
|
sig { params(login: String, first: Integer, client: T.untyped).returns(Result).checked(:never) }
|
data/examples/github/run.rb
CHANGED
data/examples/github/setup.rb
CHANGED
|
@@ -4,15 +4,23 @@
|
|
|
4
4
|
# Shared wiring for the GitHub example: auth and the client.
|
|
5
5
|
require_relative "../../lib/graph_weaver"
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
abort "need a token: `gh auth login`, or GITHUB_TOKEN=..." if token.empty?
|
|
9
|
-
|
|
10
|
-
GraphWeaver.client = GraphWeaver.new(
|
|
11
|
-
"https://api.github.com/graphql",
|
|
12
|
-
auth: token,
|
|
7
|
+
module GithubExample
|
|
13
8
|
# used by generate.rb (and any dynamic parse): the first introspection
|
|
14
9
|
# of GitHub's large schema dumps here — gitignored, a few seconds once,
|
|
15
10
|
# instant after. run.rb's checked-in generated modules never introspect,
|
|
16
11
|
# so running it alone won't create this file.
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
SCHEMA_CACHE = File.join(__dir__, "schema.json")
|
|
13
|
+
|
|
14
|
+
# Requiring this file reaches nothing, so bin/generate can regenerate from
|
|
15
|
+
# SCHEMA_CACHE with no token; needing GitHub is this call.
|
|
16
|
+
def self.connect!
|
|
17
|
+
token = ENV["GITHUB_TOKEN"] || `gh auth token 2>/dev/null`.strip
|
|
18
|
+
abort "need a token: `gh auth login`, or GITHUB_TOKEN=..." if token.empty?
|
|
19
|
+
|
|
20
|
+
GraphWeaver.client = GraphWeaver.new(
|
|
21
|
+
"https://api.github.com/graphql",
|
|
22
|
+
auth: token,
|
|
23
|
+
cache: SCHEMA_CACHE,
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
data/graph_weaver.gemspec
CHANGED
|
@@ -5,8 +5,6 @@ Gem::Specification.new do |s|
|
|
|
5
5
|
# the README tagline, verbatim — the two pitches drifted apart once already,
|
|
6
6
|
# so spec/gemspec_spec.rb pins them together
|
|
7
7
|
s.description = "Your .graphql files, compiled into Sorbet types — and the fakes to test them."
|
|
8
|
-
# ".yardopts" explicitly: `git ls-files *` skips dotfiles, and
|
|
9
|
-
# rubydoc.info needs it shipped to render docstrings as markdown
|
|
10
8
|
# CLAUDE.md/PLAN.md/REVIEW.md/NOTES.md/DECISIONS.md are written for whoever
|
|
11
9
|
# works on the gem, not whoever installs it — and REVIEW.md carries examples
|
|
12
10
|
# from before the API it describes was rewritten
|
|
@@ -14,10 +12,21 @@ Gem::Specification.new do |s|
|
|
|
14
12
|
# someone who only has the installed gem, not a checkout
|
|
15
13
|
# CHANGELOG.md doesn't (259 KB, ~16% of the package) — changelog_uri below
|
|
16
14
|
# points at the GitHub copy instead
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
unpackaged = %w[spec sorbet bin design research CLAUDE.md PLAN.md REVIEW.md
|
|
16
|
+
NOTES.md DECISIONS.md CHANGELOG.md Makefile]
|
|
17
|
+
# git knows what is tracked; a checkout that isn't a repo — an unpacked gem,
|
|
18
|
+
# a vendored copy, a shallow CI export — has no such answer, and git's
|
|
19
|
+
# "fatal:" on stderr lands in the output of every subprocess a spec captures.
|
|
20
|
+
# So ask quietly, and walk the tree rather than ship nothing.
|
|
21
|
+
files = `git ls-files * #{unpackaged.map { |path| "':!:#{path}'" }.join(" ")} 2>/dev/null`.split("\n")
|
|
22
|
+
if files.empty?
|
|
23
|
+
files = Dir.glob("**/*", base: __dir__).reject do |path|
|
|
24
|
+
unpackaged.include?(path.split("/").first) || File.directory?(File.join(__dir__, path))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
# ".yardopts" explicitly: neither list carries a dotfile, and rubydoc.info
|
|
28
|
+
# needs it shipped to render docstrings as markdown
|
|
29
|
+
s.files = files + [".yardopts"]
|
|
21
30
|
s.homepage = "https://github.com/dpep/graph_weaver"
|
|
22
31
|
s.license = "MIT"
|
|
23
32
|
s.name = "graph_weaver"
|
|
@@ -128,8 +128,7 @@ module GraphWeaver
|
|
|
128
128
|
# .gitattributes turns no tooling on, so there is no app this surprises.
|
|
129
129
|
def mark_generated_for_github
|
|
130
130
|
body = File.read(gitattributes) if File.exist?(gitattributes)
|
|
131
|
-
|
|
132
|
-
globs = linguist_globs.reject { |glob| body&.include?(glob) }
|
|
131
|
+
globs = linguist_globs.reject { |glob| marked?(body, glob) }
|
|
133
132
|
return if globs.empty?
|
|
134
133
|
|
|
135
134
|
lines = globs.map { |glob| "#{glob} linguist-generated" }.join("\n")
|
|
@@ -246,6 +245,25 @@ module GraphWeaver
|
|
|
246
245
|
# everything beneath dir — spelled as git reads it, not as rubocop does.
|
|
247
246
|
def linguist_globs = generated_outputs.map { |dir| File.join(dir, "**") }
|
|
248
247
|
|
|
248
|
+
# git's own spellings of the one attribute: set, unset (`-`), unspecified
|
|
249
|
+
# (`!`), or valued.
|
|
250
|
+
LINGUIST = /\A[-!]?linguist-generated(?:=|\z)/
|
|
251
|
+
|
|
252
|
+
# Already marked — a re-run, or done by hand. A .gitattributes line is
|
|
253
|
+
# `<pattern> <attribute>…`, with `#` opening a comment, and the same
|
|
254
|
+
# directory is `dir/**`, `dir/*`, `dir/` or `dir`. Searching the body as
|
|
255
|
+
# one string instead let a comment that mentions the path suppress the
|
|
256
|
+
# mark, and saw only the spelling this generator writes.
|
|
257
|
+
def marked?(body, glob)
|
|
258
|
+
dir = glob.delete_suffix("/**")
|
|
259
|
+
body.to_s.lines.any? do |line|
|
|
260
|
+
pattern, *attributes = line.split
|
|
261
|
+
next false if pattern.nil? || pattern.start_with?("#")
|
|
262
|
+
|
|
263
|
+
pattern.chomp("**").chomp("*").chomp("/") == dir && attributes.any? { |a| LINGUIST.match?(a) }
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
249
267
|
# This install run is the one moment the user is guaranteed to be
|
|
250
268
|
# reading, and a composed supergraph changes what the next steps are:
|
|
251
269
|
# the test client is the interesting one, and there's a CI gate to add.
|
data/lib/graph_weaver/client.rb
CHANGED
|
@@ -190,29 +190,6 @@ class GraphWeaver::Client
|
|
|
190
190
|
run(query, **variables).data!
|
|
191
191
|
end
|
|
192
192
|
|
|
193
|
-
# Does this query validate? The string form of GraphWeaver.check_queries,
|
|
194
|
-
# answering with the same JSON-ready hashes — `message`, `line`, `column`,
|
|
195
|
-
# plus `subgraphs` where a supergraph brands them — so an empty array means
|
|
196
|
-
# it validates:
|
|
197
|
-
#
|
|
198
|
-
# client.check_query("query { viewer { login } }") # => []
|
|
199
|
-
# client.check_query("query { viewer { lgoin } }")
|
|
200
|
-
# # => [{ "message" => "Field 'lgoin' doesn't exist on type 'User'",
|
|
201
|
-
# # "line" => 1, "column" => 17 }]
|
|
202
|
-
#
|
|
203
|
-
# Checked against this client's own schema — what `execute` would run
|
|
204
|
-
# against — so a url client introspects on first use as it always does, and
|
|
205
|
-
# nothing re-introspects the way check_queries defaults to. An unparseable
|
|
206
|
-
# source is an entry like any other; nothing here raises for a bad query.
|
|
207
|
-
# Shared fragments are inlined from fragments: the same way every other door
|
|
208
|
-
# inlines them.
|
|
209
|
-
def check_query(source, fragments: GraphWeaver.fragments_paths)
|
|
210
|
-
GraphWeaver::Internal::QueryCheck.errors(
|
|
211
|
-
schema, source, GraphWeaver::Codegen.load_fragments(fragments),
|
|
212
|
-
GraphWeaver::Internal::QueryCheck.routing_table_for(schema_source),
|
|
213
|
-
)
|
|
214
|
-
end
|
|
215
|
-
|
|
216
193
|
private
|
|
217
194
|
|
|
218
195
|
# Anything already speaking the client contract — another Client,
|
|
@@ -34,6 +34,13 @@ class GraphWeaver::Codegen
|
|
|
34
34
|
rescue UnknownSegment => e
|
|
35
35
|
# names nothing in the schema, so no selection would fit — offering
|
|
36
36
|
# optional: as the way out would just hide the typo
|
|
37
|
+
raise e.class, qualify(node, e.message)
|
|
38
|
+
rescue HoistedSegment => e
|
|
39
|
+
# optional: still skips it, but the message doesn't offer it: the two
|
|
40
|
+
# fixes it names are the only ones that end with an accessor, and no
|
|
41
|
+
# change to the fragment would make the path fit
|
|
42
|
+
next nil if spec[:optional]
|
|
43
|
+
|
|
37
44
|
raise e.class, qualify(node, e.message)
|
|
38
45
|
rescue GraphWeaver::Error => e
|
|
39
46
|
# a path that doesn't fit THIS query's selection: optional simply
|
|
@@ -86,7 +93,10 @@ class GraphWeaver::Codegen
|
|
|
86
93
|
# ever satisfy it, so it's a typo (or a wire-cased name), not a path that
|
|
87
94
|
# doesn't fit this query. optional: skips the latter, never this.
|
|
88
95
|
UnknownSegment = Class.new(GraphWeaver::Error)
|
|
89
|
-
|
|
96
|
+
|
|
97
|
+
# A segment inside a struct another module owns — see refuse_hoisted!.
|
|
98
|
+
HoistedSegment = Class.new(GraphWeaver::Error)
|
|
99
|
+
private_constant :ALIAS_RESERVED, :LIST_SELECTORS, :UnknownSegment, :HoistedSegment
|
|
90
100
|
|
|
91
101
|
# Walk a dotted path through this struct's selected shape, building the
|
|
92
102
|
# delegator expression (`meta&.tag`, `_entities.first&.name`) and its return
|
|
@@ -158,11 +168,13 @@ class GraphWeaver::Codegen
|
|
|
158
168
|
|
|
159
169
|
# A hop landing on a hoisted shared fragment: the struct is another
|
|
160
170
|
# 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
|
|
171
|
+
# that query spreads the fragment, so optional: still skips it — but
|
|
172
|
+
# selecting differently inside the fragment never makes the path fit, so
|
|
173
|
+
# the message names only the two fixes that do.
|
|
162
174
|
def refuse_hoisted!(node, name, ref, seg)
|
|
163
175
|
return unless ref
|
|
164
176
|
|
|
165
|
-
raise
|
|
177
|
+
raise HoistedSegment,
|
|
166
178
|
"alias #{name.inspect} on #{node.graphql_type}: '#{seg}' is inside the shared fragment " \
|
|
167
179
|
"#{ref.class_name}, which hoists to #{@types_namespace}::#{ref.class_name} — a path can't " \
|
|
168
180
|
"read into it. Register the alias on #{ref.graphql_type}, or select a field beside the " \
|
|
@@ -521,10 +521,11 @@ class GraphWeaver::Codegen
|
|
|
521
521
|
flag ? "#{var.kwarg}: (#{flag} = true; nil)" : "#{var.kwarg}: nil"
|
|
522
522
|
} + ["client: nil"]
|
|
523
523
|
|
|
524
|
-
# QueryModule#dispatch reads QUERY/OPERATION_NAME/GRAPH off the module
|
|
525
|
-
#
|
|
526
|
-
#
|
|
527
|
-
|
|
524
|
+
# QueryModule#dispatch reads QUERY/OPERATION_NAME/GRAPH off the module
|
|
525
|
+
# and closes the operation event over the cast the block does, so the
|
|
526
|
+
# gem brackets the whole call — request and cast — without a line of it
|
|
527
|
+
# landing in every generated file
|
|
528
|
+
call = "dispatch(variables, client:) { |raw| from_response(raw) }"
|
|
528
529
|
|
|
529
530
|
# execute returns the full envelope; execute! is the strict shortcut for
|
|
530
531
|
# the typed result, or a raised QueryError.
|
|
@@ -534,7 +535,7 @@ class GraphWeaver::Codegen
|
|
|
534
535
|
out << " def self.execute(#{kwargs.join(", ")})"
|
|
535
536
|
emit_variables(out, required, optional, omitted)
|
|
536
537
|
out << ""
|
|
537
|
-
out << "
|
|
538
|
+
out << " #{call}"
|
|
538
539
|
out << " end"
|
|
539
540
|
out << ""
|
|
540
541
|
out << " sig { params(#{sig_params.join(", ")}).returns(Result).checked(:never) }"
|
|
@@ -547,7 +548,7 @@ class GraphWeaver::Codegen
|
|
|
547
548
|
# left out is exactly what a Ruby call can't forward
|
|
548
549
|
emit_variables(out, required, optional, omitted)
|
|
549
550
|
out << ""
|
|
550
|
-
out << "
|
|
551
|
+
out << " #{call}.data!"
|
|
551
552
|
end
|
|
552
553
|
out << " end"
|
|
553
554
|
out << ""
|
|
@@ -83,7 +83,31 @@ class GraphWeaver::Codegen
|
|
|
83
83
|
"add them, pin with map:, or absorb with fallback:"
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
wire = to_wire(from_wire, aliases)
|
|
87
|
+
warn_unsendable(wire)
|
|
88
|
+
[from_wire, wire]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The other direction, which nothing said: a member no wire value maps back
|
|
92
|
+
# from. A warning, not a refusal — one app-wide T::Enum serving two APIs
|
|
93
|
+
# that each expose a subset is a real shape, and the call that carries such
|
|
94
|
+
# a member is refused by name at runtime (InputStruct.enum_wire), so no
|
|
95
|
+
# wrong answer gets through either way. Warned on the channel
|
|
96
|
+
# validate_registrations! already narrates a mismatched registration on.
|
|
97
|
+
# fallback: is exempt: standing for what the schema doesn't declare is
|
|
98
|
+
# exactly what it is for.
|
|
99
|
+
def warn_unsendable(wire)
|
|
100
|
+
unsendable = type.values - wire.keys - [fallback].compact
|
|
101
|
+
return if unsendable.empty?
|
|
102
|
+
|
|
103
|
+
# a T::Enum member inspects as #<Type::Name>
|
|
104
|
+
names = unsendable.map { |member| member.inspect[2..-2] }.join(", ")
|
|
105
|
+
them = unsendable.one? ? "it" : "them"
|
|
106
|
+
GraphWeaver::Internal::Log.log(:warn) do
|
|
107
|
+
"#{graphql_name} has no value for #{type} member(s) #{names} — a kwarg carrying " \
|
|
108
|
+
"#{them} is refused at the call; map onto a matching T::Enum, or name the drift " \
|
|
109
|
+
"member with fallback:"
|
|
110
|
+
end
|
|
87
111
|
end
|
|
88
112
|
|
|
89
113
|
# alias spelling => the value it is read as, checked against what the
|
|
@@ -185,7 +209,7 @@ class GraphWeaver::Codegen
|
|
|
185
209
|
groups = ambiguous.values.map { |pairs| pairs.map(&:first) }
|
|
186
210
|
more = ambiguous.size - 1
|
|
187
211
|
raise GraphWeaver::Error,
|
|
188
|
-
"enum #{graphql_name}: #{groups.first.join(" and ")}
|
|
212
|
+
"enum #{graphql_name}: #{groups.first.join(" and ")} all map onto the #{type} member " \
|
|
189
213
|
"#{member.serialize.to_s.inspect}#{" (and #{more} more)" unless more.zero?} — say which spelling " \
|
|
190
214
|
"goes on the wire:\n #{EnumType.alias_suggestion(graphql_name, groups, type)}"
|
|
191
215
|
end
|
|
@@ -293,7 +293,9 @@ class GraphWeaver::Codegen
|
|
|
293
293
|
# #as_json. Without a fallback the table is total and a miss is a real
|
|
294
294
|
# mistake, so it still raises.
|
|
295
295
|
def serialize(expr, _depth)
|
|
296
|
-
|
|
296
|
+
unless @fallback
|
|
297
|
+
return "GraphWeaver::InputStruct.enum_wire(#{@graphql_name.inspect}, #{const_prefix}_TO_WIRE, #{expr})"
|
|
298
|
+
end
|
|
297
299
|
|
|
298
300
|
"#{const_prefix}_TO_WIRE.fetch(#{expr}) { |member| member.serialize }"
|
|
299
301
|
end
|
|
@@ -406,6 +408,23 @@ class GraphWeaver::Codegen
|
|
|
406
408
|
def bare_type = "#{class_name}::Type"
|
|
407
409
|
end
|
|
408
410
|
|
|
411
|
+
# An abstract fragment narrowing to one member hoists to that member's
|
|
412
|
+
# struct, and narrowing filters — so the match is tested at the reference,
|
|
413
|
+
# the way NarrowedNode does it, rather than inside a T::Struct.from_h that
|
|
414
|
+
# has no way to answer nil. See NarrowedNode for what `typename` being
|
|
415
|
+
# absent means.
|
|
416
|
+
class NarrowedRefNode < HoistedRefNode
|
|
417
|
+
def initialize(class_name, graphql_type = nil, typename: nil)
|
|
418
|
+
super(class_name, graphql_type)
|
|
419
|
+
@typename = typename
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def cast(expr, depth)
|
|
423
|
+
match = @typename ? "#{expr}[\"__typename\"] == #{@typename.inspect}" : "!#{expr}.empty?"
|
|
424
|
+
"(#{match} ? #{super} : nil)"
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
409
428
|
# An input-object variable: emitted as a module-level T::Struct whose
|
|
410
429
|
# serialize produces the wire hash. Inputs never cast FROM the wire.
|
|
411
430
|
# Joins the coerce protocol so execute kwargs accept plain hashes,
|
|
@@ -479,5 +498,5 @@ class GraphWeaver::Codegen
|
|
|
479
498
|
# from inside the walk.
|
|
480
499
|
private_constant :Node, :Scalar, :NonNull, :List, :ObjectNode, :EnumNode,
|
|
481
500
|
:MappedEnum, :NarrowedNode, :UnionNode, :HoistedRefNode, :UnionRefNode,
|
|
482
|
-
:InputNode, :RepresentationNode
|
|
501
|
+
:NarrowedRefNode, :InputNode, :RepresentationNode
|
|
483
502
|
end
|