graph_weaver 0.6.1 → 0.7.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.
- checksums.yaml +4 -4
- data/Gemfile +8 -0
- data/Gemfile.lock +153 -4
- data/README.md +45 -79
- data/docs/alternatives.md +195 -0
- data/docs/cassettes.md +61 -50
- data/docs/editors.md +32 -47
- data/docs/errors.md +360 -103
- data/docs/federation.md +692 -473
- data/docs/generated_modules.md +441 -314
- data/docs/getting_started.md +370 -194
- data/docs/i18n.md +171 -0
- data/docs/logging.md +197 -50
- data/docs/real_world.md +42 -27
- data/docs/scalars.md +307 -176
- data/docs/testing.md +473 -220
- data/docs/transports.md +224 -151
- data/docs/upgrading.md +258 -305
- data/examples/README.md +38 -0
- data/examples/countries.rb +39 -0
- data/examples/federation.rb +62 -0
- data/examples/github/generate.rb +20 -0
- data/examples/github/generated/star_mutation.rb +126 -0
- data/examples/github/generated/stargazers_query.rb +232 -0
- data/examples/github/generated/starred_query.rb +151 -0
- data/examples/github/queries/star.graphql +8 -0
- data/examples/github/queries/stargazers.graphql +22 -0
- data/examples/github/queries/starred.graphql +11 -0
- data/examples/github/run.rb +43 -0
- data/examples/github/setup.rb +18 -0
- data/examples/rick_and_morty.rb +57 -0
- data/graph_weaver.gemspec +19 -3
- data/lib/generators/graph_weaver/install_generator.rb +138 -4
- data/lib/graph_weaver/client.rb +69 -11
- data/lib/graph_weaver/codegen/aliases.rb +7 -5
- data/lib/graph_weaver/codegen/emit.rb +98 -29
- data/lib/graph_weaver/codegen/enum_type.rb +2 -1
- data/lib/graph_weaver/codegen/nodes.rb +39 -6
- data/lib/graph_weaver/codegen/registry.rb +175 -0
- data/lib/graph_weaver/codegen/scalar_type.rb +123 -30
- data/lib/graph_weaver/codegen/type_helpers.rb +56 -11
- data/lib/graph_weaver/codegen.rb +406 -195
- data/lib/graph_weaver/coerce.rb +155 -26
- data/lib/graph_weaver/context_seam.rb +54 -0
- data/lib/graph_weaver/errors.rb +284 -46
- data/lib/graph_weaver/federation.rb +129 -27
- data/lib/graph_weaver/graph.rb +315 -0
- data/lib/graph_weaver/hints.rb +100 -24
- data/lib/graph_weaver/in_process.rb +27 -15
- data/lib/graph_weaver/input_struct.rb +119 -32
- data/lib/graph_weaver/internal/endpoint.rb +80 -0
- data/lib/graph_weaver/internal/headers.rb +70 -0
- data/lib/graph_weaver/internal/overrides.rb +67 -5
- data/lib/graph_weaver/internal/planner.rb +45 -15
- data/lib/graph_weaver/internal/refusal.rb +49 -0
- data/lib/graph_weaver/internal/schemas.rb +23 -9
- data/lib/graph_weaver/internal/selection.rb +34 -0
- data/lib/graph_weaver/internal/server_input.rb +251 -0
- data/lib/graph_weaver/internal/test_clients.rb +276 -0
- data/lib/graph_weaver/internal/unused.rb +287 -0
- data/lib/graph_weaver/internal/values.rb +40 -4
- data/lib/graph_weaver/internal.rb +249 -14
- data/lib/graph_weaver/log_subscriber.rb +74 -0
- data/lib/graph_weaver/logging.rb +163 -19
- data/lib/graph_weaver/query_module.rb +44 -3
- data/lib/graph_weaver/railtie.rb +237 -17
- data/lib/graph_weaver/representation.rb +55 -17
- data/lib/graph_weaver/result_struct.rb +90 -0
- data/lib/graph_weaver/retry.rb +45 -13
- data/lib/graph_weaver/rspec.rb +404 -93
- data/lib/graph_weaver/schema_loader.rb +266 -56
- data/lib/graph_weaver/tasks.rb +380 -89
- data/lib/graph_weaver/testing/cassette.rb +34 -10
- data/lib/graph_weaver/testing/endpoint.rb +107 -0
- data/lib/graph_weaver/testing/failure.rb +69 -12
- data/lib/graph_weaver/testing/fake_client.rb +164 -45
- data/lib/graph_weaver/testing/router.rb +64 -13
- data/lib/graph_weaver/testing.rb +200 -58
- data/lib/graph_weaver/transport/faraday.rb +41 -8
- data/lib/graph_weaver/transport/http.rb +48 -6
- data/lib/graph_weaver/transport.rb +134 -27
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +495 -106
- metadata +71 -3
- data/CHANGELOG.md +0 -2355
data/examples/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
Four runnable scripts, smallest first. Each is the smallest thing that shows
|
|
4
|
+
its idea; run them straight from a checkout.
|
|
5
|
+
|
|
6
|
+
| | shows | needs |
|
|
7
|
+
|---|---|---|
|
|
8
|
+
| [`countries.rb`](countries.rb) | the whole loop in 30 lines: a client, `parse`, a typed result, a one-shot `run!` | network |
|
|
9
|
+
| [`rick_and_morty.rb`](rick_and_morty.rb) | filtering, pagination, an aliased field, a block-built type helper | network |
|
|
10
|
+
| [`federation.rb`](federation.rb) | a federated graph planned and stitched in-process, with the fetch trace and a refusal | nothing |
|
|
11
|
+
| [`github/`](github) | the production path: auth, a custom scalar, checked-in generated modules | a token |
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
bundle exec examples/countries.rb JP BR
|
|
15
|
+
bundle exec examples/rick_and_morty.rb morty
|
|
16
|
+
bundle exec examples/federation.rb
|
|
17
|
+
bundle exec examples/github/run.rb # gh auth login, or GITHUB_TOKEN=...
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**`federation.rb` is the one with no network at all** — three real subgraphs, a
|
|
21
|
+
boundary-crossing query through a generated module, the trace of the fetches it
|
|
22
|
+
took, and a refusal it declines to plan. `spec/examples_spec.rb` runs it on every
|
|
23
|
+
build.
|
|
24
|
+
|
|
25
|
+
**`github/` is the only one with committed generated code**, so it's the one that
|
|
26
|
+
looks like an app:
|
|
27
|
+
|
|
28
|
+
- [`setup.rb`](github/setup.rb) — the shared wiring an initializer would hold:
|
|
29
|
+
auth and the client.
|
|
30
|
+
- [`queries/`](github/queries) → [`generate.rb`](github/generate.rb) →
|
|
31
|
+
[`generated/`](github/generated) — the build loop `rake graph_weaver:generate`
|
|
32
|
+
runs in a Rails app.
|
|
33
|
+
- [`run.rb`](github/run.rb) — stars this repo ⭐ and introduces you to your
|
|
34
|
+
fellow stargazers.
|
|
35
|
+
|
|
36
|
+
Regeneration introspects GitHub's schema (a few seconds, cached to a gitignored
|
|
37
|
+
`github/schema.json`); `run.rb` alone never introspects, because the generated
|
|
38
|
+
modules already carry their types.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# typed: false
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# The simplest possible GraphWeaver session: a public API, no auth, no
|
|
6
|
+
# build step — everything dynamic and in memory.
|
|
7
|
+
#
|
|
8
|
+
# examples/countries.rb [CODE ...]
|
|
9
|
+
# examples/countries.rb JP BR
|
|
10
|
+
#
|
|
11
|
+
# (https://countries.trevorblades.com — a free public GraphQL API)
|
|
12
|
+
require_relative "../lib/graph_weaver"
|
|
13
|
+
|
|
14
|
+
api = GraphWeaver.new("https://countries.trevorblades.com/")
|
|
15
|
+
|
|
16
|
+
# parse once: a typed module bound to the client's schema + transport
|
|
17
|
+
CountryQuery = api.parse(<<~GRAPHQL)
|
|
18
|
+
query($code: ID!) {
|
|
19
|
+
country(code: $code) {
|
|
20
|
+
name
|
|
21
|
+
emoji
|
|
22
|
+
capital
|
|
23
|
+
continent { name }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
GRAPHQL
|
|
27
|
+
|
|
28
|
+
codes = ARGV.empty? ? %w[US JP] : ARGV
|
|
29
|
+
codes.each do |code|
|
|
30
|
+
country = CountryQuery.execute!(code: code.upcase).country
|
|
31
|
+
abort "unknown country code: #{code}" unless country
|
|
32
|
+
|
|
33
|
+
puts "#{country.emoji} #{country.name} — capital #{country.capital}, #{country.continent.name}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# or skip the module entirely — a one-shot with variables as kwargs
|
|
37
|
+
continents = api.run!("query { continents { name countries { code } } }").continents
|
|
38
|
+
biggest = continents.max_by { |c| c.countries.size }
|
|
39
|
+
puts "\n#{continents.size} continents; #{biggest.name} has the most countries (#{biggest.countries.size})"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# typed: ignore — the subgraph schema classes it borrows are themselves ignored
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# The only example that needs no network: a federated graph running entirely
|
|
6
|
+
# in this process. `Testing::Router` takes the composed supergraph, plans a
|
|
7
|
+
# query across the subgraphs, and stitches the answer — so a generated module
|
|
8
|
+
# runs against your real resolvers with no gateway, no node, no sockets.
|
|
9
|
+
#
|
|
10
|
+
# bundle exec examples/federation.rb
|
|
11
|
+
#
|
|
12
|
+
# The subgraphs are the suite's own (spec/support/federation_router_graph.rb):
|
|
13
|
+
# `accounts` owns User, `products` owns Product, and `reviews` owns Review
|
|
14
|
+
# while extending both — the Apollo demo graph, as three real
|
|
15
|
+
# apollo-federation schemas. supergraph.graphql beside them is Apollo's own
|
|
16
|
+
# composition of the three, recomposed and diffed by the suite, so what runs
|
|
17
|
+
# here is the real thing.
|
|
18
|
+
#
|
|
19
|
+
# Everything the router does beyond this — @requires, a partly-local
|
|
20
|
+
# supergraph, the `:fake` opt-in, every refusal — is spec/router_spec.rb.
|
|
21
|
+
require_relative "../lib/graph_weaver"
|
|
22
|
+
require_relative "../lib/graph_weaver/testing"
|
|
23
|
+
require_relative "../spec/support/federation_router_graph"
|
|
24
|
+
|
|
25
|
+
# Only the supergraph is needed: which Ruby schema serves each subgraph is
|
|
26
|
+
# derived from what each loaded schema defines.
|
|
27
|
+
router = GraphWeaver::Testing::Router.new(supergraph: RouterGraph::SUPERGRAPH)
|
|
28
|
+
puts router.inspect
|
|
29
|
+
|
|
30
|
+
# me → accounts, reviews → reviews, product → products. The router holds the
|
|
31
|
+
# supergraph, so it parses against it — and the module runs on the router.
|
|
32
|
+
DashboardQuery = router.parse(<<~GRAPHQL, name: "DashboardQuery")
|
|
33
|
+
query Dashboard {
|
|
34
|
+
me {
|
|
35
|
+
username
|
|
36
|
+
reviews { body product { name price } }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
GRAPHQL
|
|
40
|
+
|
|
41
|
+
me = DashboardQuery.execute!.me
|
|
42
|
+
puts "\n#{me.username} reviewed #{me.reviews.size} products:"
|
|
43
|
+
me.reviews.each { |review| puts " #{review.product.name} ($#{review.product.price}) — #{review.body}" }
|
|
44
|
+
|
|
45
|
+
# The trace is the mechanism in one read: a root fetch, then one `_entities`
|
|
46
|
+
# call per subgraph per level — every node at a level in ONE call, so two
|
|
47
|
+
# products are one fetch, not two.
|
|
48
|
+
puts "\nfetches:"
|
|
49
|
+
router.trace.each do |fetch|
|
|
50
|
+
reps = fetch[:variables]["representations"]
|
|
51
|
+
puts " → #{fetch[:subgraph].ljust(9)} #{reps ? "_entities × #{reps.size} #{reps.first["__typename"]}" : "root fields"}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Everything it can't plan *faithfully* raises at plan time, before any
|
|
55
|
+
# subgraph runs. Here the alias collides with the @key the planner injects to
|
|
56
|
+
# cross the boundary, and Apollo's router and a spec-conformant server answer
|
|
57
|
+
# that differently — so there is no one answer to agree with.
|
|
58
|
+
begin
|
|
59
|
+
router.execute("{ me { id: username reviews { body } } }")
|
|
60
|
+
rescue GraphWeaver::Testing::Unplannable => e
|
|
61
|
+
puts "\nrefused (#{e.label}):\n #{e.message}"
|
|
62
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# typed: false
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# Regenerate the checked-in typed modules from queries/*.graphql —
|
|
6
|
+
# the same workflow `rake graph_weaver:generate` runs in an app:
|
|
7
|
+
#
|
|
8
|
+
# examples/github/generate.rb
|
|
9
|
+
require_relative "setup"
|
|
10
|
+
|
|
11
|
+
GraphWeaver.graph :github do
|
|
12
|
+
schema GraphWeaver.client.schema
|
|
13
|
+
queries File.join(__dir__, "queries")
|
|
14
|
+
output File.join(__dir__, "generated")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
GraphWeaver.generate!
|
|
18
|
+
changed = GraphWeaver.changed_files
|
|
19
|
+
changed.each { |path| puts "wrote #{path}" }
|
|
20
|
+
puts "already up to date" if changed.empty?
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Generated by GraphWeaver 0.7.0 — do not edit.
|
|
5
|
+
|
|
6
|
+
module StarMutation
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
QUERY = T.let(<<~'GRAPHQL', String)
|
|
10
|
+
mutation StarMutation($id: ID!) {
|
|
11
|
+
addStar(input: { starrableId: $id }) {
|
|
12
|
+
starrable {
|
|
13
|
+
stargazerCount
|
|
14
|
+
viewerHasStarred
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
GRAPHQL
|
|
19
|
+
|
|
20
|
+
# sent as the request's operationName — what an APM keys traces on
|
|
21
|
+
OPERATION_NAME = T.let("StarMutation", T.nilable(String))
|
|
22
|
+
|
|
23
|
+
class Result < T::Struct
|
|
24
|
+
extend T::Sig
|
|
25
|
+
include GraphWeaver::Hints
|
|
26
|
+
include GraphWeaver::ResultStruct
|
|
27
|
+
|
|
28
|
+
class AddStar < T::Struct
|
|
29
|
+
extend T::Sig
|
|
30
|
+
include GraphWeaver::Hints
|
|
31
|
+
include GraphWeaver::ResultStruct
|
|
32
|
+
|
|
33
|
+
class Starrable < T::Struct
|
|
34
|
+
extend T::Sig
|
|
35
|
+
include GraphWeaver::Hints
|
|
36
|
+
include GraphWeaver::ResultStruct
|
|
37
|
+
|
|
38
|
+
const :stargazer_count, Integer
|
|
39
|
+
const :viewer_has_starred, T::Boolean
|
|
40
|
+
|
|
41
|
+
sig { params(data: T::Hash[String, T.untyped]).returns(Starrable) }
|
|
42
|
+
def self.from_h(data)
|
|
43
|
+
new(
|
|
44
|
+
stargazer_count: data.fetch("stargazerCount"),
|
|
45
|
+
viewer_has_starred: data.fetch("viewerHasStarred"),
|
|
46
|
+
)
|
|
47
|
+
rescue GraphWeaver::Error
|
|
48
|
+
raise # already branded by a nested struct or leaf — keep the innermost context
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
raise GraphWeaver::CastError.new(struct: self, message: GraphWeaver::Hints.cast_message(self, data, e))
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
const :starrable, T.nilable(Starrable)
|
|
55
|
+
|
|
56
|
+
sig { params(data: T::Hash[String, T.untyped]).returns(AddStar) }
|
|
57
|
+
def self.from_h(data)
|
|
58
|
+
new(
|
|
59
|
+
starrable: data["starrable"]&.then { |v1| Starrable.from_h(v1) },
|
|
60
|
+
)
|
|
61
|
+
rescue GraphWeaver::Error
|
|
62
|
+
raise # already branded by a nested struct or leaf — keep the innermost context
|
|
63
|
+
rescue StandardError => e
|
|
64
|
+
raise GraphWeaver::CastError.new(struct: self, message: GraphWeaver::Hints.cast_message(self, data, e))
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
const :add_star, T.nilable(AddStar)
|
|
69
|
+
|
|
70
|
+
sig { params(data: T::Hash[String, T.untyped]).returns(Result) }
|
|
71
|
+
def self.from_h(data)
|
|
72
|
+
new(
|
|
73
|
+
add_star: data["addStar"]&.then { |v1| AddStar.from_h(v1) },
|
|
74
|
+
)
|
|
75
|
+
rescue GraphWeaver::Error
|
|
76
|
+
raise # already branded by a nested struct or leaf — keep the innermost context
|
|
77
|
+
rescue StandardError => e
|
|
78
|
+
raise GraphWeaver::CastError.new(struct: self, message: GraphWeaver::Hints.cast_message(self, data, e))
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# client / client= — see GraphWeaver::QueryModule
|
|
83
|
+
extend GraphWeaver::QueryModule
|
|
84
|
+
|
|
85
|
+
# the graph this module was generated from — what a test mode builds
|
|
86
|
+
# its stand-in client from
|
|
87
|
+
GRAPH = T.let(:github, Symbol)
|
|
88
|
+
private_constant :GRAPH
|
|
89
|
+
|
|
90
|
+
# .checked(:never): an untyped value (a Rails param) reaches the coercion
|
|
91
|
+
# below instead of being rejected by sorbet-runtime's argument check.
|
|
92
|
+
sig { params(id: String, client: T.untyped).returns(GraphWeaver::Response[Result]).checked(:never) }
|
|
93
|
+
def self.execute(id:, client: nil)
|
|
94
|
+
variables = {
|
|
95
|
+
"id" => GraphWeaver::Coerce.variable("id", OPERATION_NAME, id) { |v| GraphWeaver::Coerce.id(v) },
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
from_response(dispatch(variables, client:))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
sig { params(id: String, client: T.untyped).returns(Result).checked(:never) }
|
|
102
|
+
def self.execute!(id:, client: nil)
|
|
103
|
+
execute(id:, client:).data!
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Deserialize a raw GraphQL response into the typed envelope — the
|
|
107
|
+
# network-free half of execute, for responses fetched by any client.
|
|
108
|
+
# Takes the response hash (or anything with #to_h): {"data" => ...,
|
|
109
|
+
# "errors" => ..., "extensions" => ...} with wire-cased string keys.
|
|
110
|
+
sig { params(response: T.untyped).returns(GraphWeaver::Response[Result]) }
|
|
111
|
+
def self.from_response(response)
|
|
112
|
+
raw = GraphWeaver.check_envelope!(response, Result)
|
|
113
|
+
errors = (raw["errors"] || []).map { |e| GraphWeaver::GraphQLError.from_h(e) }
|
|
114
|
+
GraphWeaver::Response[Result].new(
|
|
115
|
+
data: (GraphWeaver.cast_data(Result, raw["data"], errors) if raw["data"]),
|
|
116
|
+
errors:,
|
|
117
|
+
extensions: raw["extensions"] || {},
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# from_response + data! — the typed result, or a raised QueryError.
|
|
122
|
+
sig { params(response: T.untyped).returns(Result) }
|
|
123
|
+
def self.from_response!(response)
|
|
124
|
+
from_response(response).data!
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Generated by GraphWeaver 0.7.0 — do not edit.
|
|
5
|
+
|
|
6
|
+
require "time"
|
|
7
|
+
|
|
8
|
+
module StargazersQuery
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
QUERY = T.let(<<~'GRAPHQL', String)
|
|
12
|
+
query StargazersQuery($owner: String!, $name: String!, $first: Int!) {
|
|
13
|
+
repository(owner: $owner, name: $name) {
|
|
14
|
+
id
|
|
15
|
+
nameWithOwner
|
|
16
|
+
stargazerCount
|
|
17
|
+
stargazers(first: $first, orderBy: { field: STARRED_AT, direction: DESC }) {
|
|
18
|
+
edges {
|
|
19
|
+
starredAt
|
|
20
|
+
node {
|
|
21
|
+
login
|
|
22
|
+
name
|
|
23
|
+
repositories(first: 2, orderBy: { field: STARGAZERS, direction: DESC }) {
|
|
24
|
+
nodes {
|
|
25
|
+
nameWithOwner
|
|
26
|
+
stargazerCount
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
GRAPHQL
|
|
35
|
+
|
|
36
|
+
# sent as the request's operationName — what an APM keys traces on
|
|
37
|
+
OPERATION_NAME = T.let("StargazersQuery", T.nilable(String))
|
|
38
|
+
|
|
39
|
+
class Result < T::Struct
|
|
40
|
+
extend T::Sig
|
|
41
|
+
include GraphWeaver::Hints
|
|
42
|
+
include GraphWeaver::ResultStruct
|
|
43
|
+
|
|
44
|
+
class Repository < T::Struct
|
|
45
|
+
extend T::Sig
|
|
46
|
+
include GraphWeaver::Hints
|
|
47
|
+
include GraphWeaver::ResultStruct
|
|
48
|
+
|
|
49
|
+
class Stargazers < T::Struct
|
|
50
|
+
extend T::Sig
|
|
51
|
+
include GraphWeaver::Hints
|
|
52
|
+
include GraphWeaver::ResultStruct
|
|
53
|
+
|
|
54
|
+
class Edges < T::Struct
|
|
55
|
+
extend T::Sig
|
|
56
|
+
include GraphWeaver::Hints
|
|
57
|
+
include GraphWeaver::ResultStruct
|
|
58
|
+
|
|
59
|
+
class Node < T::Struct
|
|
60
|
+
extend T::Sig
|
|
61
|
+
include GraphWeaver::Hints
|
|
62
|
+
include GraphWeaver::ResultStruct
|
|
63
|
+
|
|
64
|
+
class Repositories < T::Struct
|
|
65
|
+
extend T::Sig
|
|
66
|
+
include GraphWeaver::Hints
|
|
67
|
+
include GraphWeaver::ResultStruct
|
|
68
|
+
|
|
69
|
+
class Nodes < T::Struct
|
|
70
|
+
extend T::Sig
|
|
71
|
+
include GraphWeaver::Hints
|
|
72
|
+
include GraphWeaver::ResultStruct
|
|
73
|
+
|
|
74
|
+
const :name_with_owner, String
|
|
75
|
+
const :stargazer_count, Integer
|
|
76
|
+
|
|
77
|
+
sig { params(data: T::Hash[String, T.untyped]).returns(Nodes) }
|
|
78
|
+
def self.from_h(data)
|
|
79
|
+
new(
|
|
80
|
+
name_with_owner: data.fetch("nameWithOwner"),
|
|
81
|
+
stargazer_count: data.fetch("stargazerCount"),
|
|
82
|
+
)
|
|
83
|
+
rescue GraphWeaver::Error
|
|
84
|
+
raise # already branded by a nested struct or leaf — keep the innermost context
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
raise GraphWeaver::CastError.new(struct: self, message: GraphWeaver::Hints.cast_message(self, data, e))
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
const :nodes, T.nilable(T::Array[T.nilable(Nodes)])
|
|
91
|
+
|
|
92
|
+
sig { params(data: T::Hash[String, T.untyped]).returns(Repositories) }
|
|
93
|
+
def self.from_h(data)
|
|
94
|
+
new(
|
|
95
|
+
nodes: data["nodes"]&.then { |v1| v1.map { |v2| v2&.then { |v3| Nodes.from_h(v3) } } },
|
|
96
|
+
)
|
|
97
|
+
rescue GraphWeaver::Error
|
|
98
|
+
raise # already branded by a nested struct or leaf — keep the innermost context
|
|
99
|
+
rescue StandardError => e
|
|
100
|
+
raise GraphWeaver::CastError.new(struct: self, message: GraphWeaver::Hints.cast_message(self, data, e))
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
const :login, String
|
|
105
|
+
const :name, T.nilable(String)
|
|
106
|
+
const :repositories, Repositories
|
|
107
|
+
|
|
108
|
+
sig { params(data: T::Hash[String, T.untyped]).returns(Node) }
|
|
109
|
+
def self.from_h(data)
|
|
110
|
+
new(
|
|
111
|
+
login: data.fetch("login"),
|
|
112
|
+
name: data["name"],
|
|
113
|
+
repositories: Repositories.from_h(data.fetch("repositories")),
|
|
114
|
+
)
|
|
115
|
+
rescue GraphWeaver::Error
|
|
116
|
+
raise # already branded by a nested struct or leaf — keep the innermost context
|
|
117
|
+
rescue StandardError => e
|
|
118
|
+
raise GraphWeaver::CastError.new(struct: self, message: GraphWeaver::Hints.cast_message(self, data, e))
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
const :starred_at, Time
|
|
123
|
+
const :node, Node
|
|
124
|
+
|
|
125
|
+
sig { params(data: T::Hash[String, T.untyped]).returns(Edges) }
|
|
126
|
+
def self.from_h(data)
|
|
127
|
+
new(
|
|
128
|
+
starred_at: GraphWeaver::Hints.field(self, "starredAt") { Time.parse(data.fetch("starredAt")) },
|
|
129
|
+
node: Node.from_h(data.fetch("node")),
|
|
130
|
+
)
|
|
131
|
+
rescue GraphWeaver::Error
|
|
132
|
+
raise # already branded by a nested struct or leaf — keep the innermost context
|
|
133
|
+
rescue StandardError => e
|
|
134
|
+
raise GraphWeaver::CastError.new(struct: self, message: GraphWeaver::Hints.cast_message(self, data, e))
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
const :edges, T.nilable(T::Array[T.nilable(Edges)])
|
|
139
|
+
|
|
140
|
+
sig { params(data: T::Hash[String, T.untyped]).returns(Stargazers) }
|
|
141
|
+
def self.from_h(data)
|
|
142
|
+
new(
|
|
143
|
+
edges: data["edges"]&.then { |v1| v1.map { |v2| v2&.then { |v3| Edges.from_h(v3) } } },
|
|
144
|
+
)
|
|
145
|
+
rescue GraphWeaver::Error
|
|
146
|
+
raise # already branded by a nested struct or leaf — keep the innermost context
|
|
147
|
+
rescue StandardError => e
|
|
148
|
+
raise GraphWeaver::CastError.new(struct: self, message: GraphWeaver::Hints.cast_message(self, data, e))
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
const :id, String
|
|
153
|
+
const :name_with_owner, String
|
|
154
|
+
const :stargazer_count, Integer
|
|
155
|
+
const :stargazers, Stargazers
|
|
156
|
+
|
|
157
|
+
sig { params(data: T::Hash[String, T.untyped]).returns(Repository) }
|
|
158
|
+
def self.from_h(data)
|
|
159
|
+
new(
|
|
160
|
+
id: data.fetch("id"),
|
|
161
|
+
name_with_owner: data.fetch("nameWithOwner"),
|
|
162
|
+
stargazer_count: data.fetch("stargazerCount"),
|
|
163
|
+
stargazers: Stargazers.from_h(data.fetch("stargazers")),
|
|
164
|
+
)
|
|
165
|
+
rescue GraphWeaver::Error
|
|
166
|
+
raise # already branded by a nested struct or leaf — keep the innermost context
|
|
167
|
+
rescue StandardError => e
|
|
168
|
+
raise GraphWeaver::CastError.new(struct: self, message: GraphWeaver::Hints.cast_message(self, data, e))
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
const :repository, T.nilable(Repository)
|
|
173
|
+
|
|
174
|
+
sig { params(data: T::Hash[String, T.untyped]).returns(Result) }
|
|
175
|
+
def self.from_h(data)
|
|
176
|
+
new(
|
|
177
|
+
repository: data["repository"]&.then { |v1| Repository.from_h(v1) },
|
|
178
|
+
)
|
|
179
|
+
rescue GraphWeaver::Error
|
|
180
|
+
raise # already branded by a nested struct or leaf — keep the innermost context
|
|
181
|
+
rescue StandardError => e
|
|
182
|
+
raise GraphWeaver::CastError.new(struct: self, message: GraphWeaver::Hints.cast_message(self, data, e))
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# client / client= — see GraphWeaver::QueryModule
|
|
187
|
+
extend GraphWeaver::QueryModule
|
|
188
|
+
|
|
189
|
+
# the graph this module was generated from — what a test mode builds
|
|
190
|
+
# its stand-in client from
|
|
191
|
+
GRAPH = T.let(:github, Symbol)
|
|
192
|
+
private_constant :GRAPH
|
|
193
|
+
|
|
194
|
+
# .checked(:never): an untyped value (a Rails param) reaches the coercion
|
|
195
|
+
# below instead of being rejected by sorbet-runtime's argument check.
|
|
196
|
+
sig { params(owner: String, name: String, first: Integer, client: T.untyped).returns(GraphWeaver::Response[Result]).checked(:never) }
|
|
197
|
+
def self.execute(owner:, name:, first:, client: nil)
|
|
198
|
+
variables = {
|
|
199
|
+
"owner" => GraphWeaver::Coerce.variable("owner", OPERATION_NAME, owner) { |v| GraphWeaver::Coerce.string(v) },
|
|
200
|
+
"name" => GraphWeaver::Coerce.variable("name", OPERATION_NAME, name) { |v| GraphWeaver::Coerce.string(v) },
|
|
201
|
+
"first" => GraphWeaver::Coerce.variable("first", OPERATION_NAME, first) { |v| GraphWeaver::Coerce.integer(v) },
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
from_response(dispatch(variables, client:))
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
sig { params(owner: String, name: String, first: Integer, client: T.untyped).returns(Result).checked(:never) }
|
|
208
|
+
def self.execute!(owner:, name:, first:, client: nil)
|
|
209
|
+
execute(owner:, name:, first:, client:).data!
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Deserialize a raw GraphQL response into the typed envelope — the
|
|
213
|
+
# network-free half of execute, for responses fetched by any client.
|
|
214
|
+
# Takes the response hash (or anything with #to_h): {"data" => ...,
|
|
215
|
+
# "errors" => ..., "extensions" => ...} with wire-cased string keys.
|
|
216
|
+
sig { params(response: T.untyped).returns(GraphWeaver::Response[Result]) }
|
|
217
|
+
def self.from_response(response)
|
|
218
|
+
raw = GraphWeaver.check_envelope!(response, Result)
|
|
219
|
+
errors = (raw["errors"] || []).map { |e| GraphWeaver::GraphQLError.from_h(e) }
|
|
220
|
+
GraphWeaver::Response[Result].new(
|
|
221
|
+
data: (GraphWeaver.cast_data(Result, raw["data"], errors) if raw["data"]),
|
|
222
|
+
errors:,
|
|
223
|
+
extensions: raw["extensions"] || {},
|
|
224
|
+
)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# from_response + data! — the typed result, or a raised QueryError.
|
|
228
|
+
sig { params(response: T.untyped).returns(Result) }
|
|
229
|
+
def self.from_response!(response)
|
|
230
|
+
from_response(response).data!
|
|
231
|
+
end
|
|
232
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Generated by GraphWeaver 0.7.0 — do not edit.
|
|
5
|
+
|
|
6
|
+
module StarredQuery
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
QUERY = T.let(<<~'GRAPHQL', String)
|
|
10
|
+
query StarredQuery($login: String!, $first: Int!) {
|
|
11
|
+
user(login: $login) {
|
|
12
|
+
starredRepositories(first: $first, orderBy: { field: STARRED_AT, direction: DESC }) {
|
|
13
|
+
totalCount
|
|
14
|
+
nodes {
|
|
15
|
+
nameWithOwner
|
|
16
|
+
stargazerCount
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
GRAPHQL
|
|
22
|
+
|
|
23
|
+
# sent as the request's operationName — what an APM keys traces on
|
|
24
|
+
OPERATION_NAME = T.let("StarredQuery", T.nilable(String))
|
|
25
|
+
|
|
26
|
+
class Result < T::Struct
|
|
27
|
+
extend T::Sig
|
|
28
|
+
include GraphWeaver::Hints
|
|
29
|
+
include GraphWeaver::ResultStruct
|
|
30
|
+
|
|
31
|
+
class User < T::Struct
|
|
32
|
+
extend T::Sig
|
|
33
|
+
include GraphWeaver::Hints
|
|
34
|
+
include GraphWeaver::ResultStruct
|
|
35
|
+
|
|
36
|
+
class StarredRepositories < T::Struct
|
|
37
|
+
extend T::Sig
|
|
38
|
+
include GraphWeaver::Hints
|
|
39
|
+
include GraphWeaver::ResultStruct
|
|
40
|
+
|
|
41
|
+
class Nodes < T::Struct
|
|
42
|
+
extend T::Sig
|
|
43
|
+
include GraphWeaver::Hints
|
|
44
|
+
include GraphWeaver::ResultStruct
|
|
45
|
+
|
|
46
|
+
const :name_with_owner, String
|
|
47
|
+
const :stargazer_count, Integer
|
|
48
|
+
|
|
49
|
+
sig { params(data: T::Hash[String, T.untyped]).returns(Nodes) }
|
|
50
|
+
def self.from_h(data)
|
|
51
|
+
new(
|
|
52
|
+
name_with_owner: data.fetch("nameWithOwner"),
|
|
53
|
+
stargazer_count: data.fetch("stargazerCount"),
|
|
54
|
+
)
|
|
55
|
+
rescue GraphWeaver::Error
|
|
56
|
+
raise # already branded by a nested struct or leaf — keep the innermost context
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
raise GraphWeaver::CastError.new(struct: self, message: GraphWeaver::Hints.cast_message(self, data, e))
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
const :total_count, Integer
|
|
63
|
+
const :nodes, T.nilable(T::Array[T.nilable(Nodes)])
|
|
64
|
+
|
|
65
|
+
sig { params(data: T::Hash[String, T.untyped]).returns(StarredRepositories) }
|
|
66
|
+
def self.from_h(data)
|
|
67
|
+
new(
|
|
68
|
+
total_count: data.fetch("totalCount"),
|
|
69
|
+
nodes: data["nodes"]&.then { |v1| v1.map { |v2| v2&.then { |v3| Nodes.from_h(v3) } } },
|
|
70
|
+
)
|
|
71
|
+
rescue GraphWeaver::Error
|
|
72
|
+
raise # already branded by a nested struct or leaf — keep the innermost context
|
|
73
|
+
rescue StandardError => e
|
|
74
|
+
raise GraphWeaver::CastError.new(struct: self, message: GraphWeaver::Hints.cast_message(self, data, e))
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
const :starred_repositories, StarredRepositories
|
|
79
|
+
|
|
80
|
+
sig { params(data: T::Hash[String, T.untyped]).returns(User) }
|
|
81
|
+
def self.from_h(data)
|
|
82
|
+
new(
|
|
83
|
+
starred_repositories: StarredRepositories.from_h(data.fetch("starredRepositories")),
|
|
84
|
+
)
|
|
85
|
+
rescue GraphWeaver::Error
|
|
86
|
+
raise # already branded by a nested struct or leaf — keep the innermost context
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
raise GraphWeaver::CastError.new(struct: self, message: GraphWeaver::Hints.cast_message(self, data, e))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
const :user, T.nilable(User)
|
|
93
|
+
|
|
94
|
+
sig { params(data: T::Hash[String, T.untyped]).returns(Result) }
|
|
95
|
+
def self.from_h(data)
|
|
96
|
+
new(
|
|
97
|
+
user: data["user"]&.then { |v1| User.from_h(v1) },
|
|
98
|
+
)
|
|
99
|
+
rescue GraphWeaver::Error
|
|
100
|
+
raise # already branded by a nested struct or leaf — keep the innermost context
|
|
101
|
+
rescue StandardError => e
|
|
102
|
+
raise GraphWeaver::CastError.new(struct: self, message: GraphWeaver::Hints.cast_message(self, data, e))
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# client / client= — see GraphWeaver::QueryModule
|
|
107
|
+
extend GraphWeaver::QueryModule
|
|
108
|
+
|
|
109
|
+
# the graph this module was generated from — what a test mode builds
|
|
110
|
+
# its stand-in client from
|
|
111
|
+
GRAPH = T.let(:github, Symbol)
|
|
112
|
+
private_constant :GRAPH
|
|
113
|
+
|
|
114
|
+
# .checked(:never): an untyped value (a Rails param) reaches the coercion
|
|
115
|
+
# below instead of being rejected by sorbet-runtime's argument check.
|
|
116
|
+
sig { params(login: String, first: Integer, client: T.untyped).returns(GraphWeaver::Response[Result]).checked(:never) }
|
|
117
|
+
def self.execute(login:, first:, client: nil)
|
|
118
|
+
variables = {
|
|
119
|
+
"login" => GraphWeaver::Coerce.variable("login", OPERATION_NAME, login) { |v| GraphWeaver::Coerce.string(v) },
|
|
120
|
+
"first" => GraphWeaver::Coerce.variable("first", OPERATION_NAME, first) { |v| GraphWeaver::Coerce.integer(v) },
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
from_response(dispatch(variables, client:))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
sig { params(login: String, first: Integer, client: T.untyped).returns(Result).checked(:never) }
|
|
127
|
+
def self.execute!(login:, first:, client: nil)
|
|
128
|
+
execute(login:, first:, client:).data!
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Deserialize a raw GraphQL response into the typed envelope — the
|
|
132
|
+
# network-free half of execute, for responses fetched by any client.
|
|
133
|
+
# Takes the response hash (or anything with #to_h): {"data" => ...,
|
|
134
|
+
# "errors" => ..., "extensions" => ...} with wire-cased string keys.
|
|
135
|
+
sig { params(response: T.untyped).returns(GraphWeaver::Response[Result]) }
|
|
136
|
+
def self.from_response(response)
|
|
137
|
+
raw = GraphWeaver.check_envelope!(response, Result)
|
|
138
|
+
errors = (raw["errors"] || []).map { |e| GraphWeaver::GraphQLError.from_h(e) }
|
|
139
|
+
GraphWeaver::Response[Result].new(
|
|
140
|
+
data: (GraphWeaver.cast_data(Result, raw["data"], errors) if raw["data"]),
|
|
141
|
+
errors:,
|
|
142
|
+
extensions: raw["extensions"] || {},
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# from_response + data! — the typed result, or a raised QueryError.
|
|
147
|
+
sig { params(response: T.untyped).returns(Result) }
|
|
148
|
+
def self.from_response!(response)
|
|
149
|
+
from_response(response).data!
|
|
150
|
+
end
|
|
151
|
+
end
|