graph_weaver 0.7.0 → 0.7.2

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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +4 -4
  3. data/README.md +40 -88
  4. data/docs/alternatives.md +1 -7
  5. data/docs/cassettes.md +54 -59
  6. data/docs/editors.md +32 -47
  7. data/docs/errors.md +261 -369
  8. data/docs/federation.md +650 -837
  9. data/docs/generated_modules.md +380 -463
  10. data/docs/getting_started.md +211 -428
  11. data/docs/i18n.md +114 -177
  12. data/docs/logging.md +127 -116
  13. data/docs/real_world.md +26 -39
  14. data/docs/scalars.md +277 -310
  15. data/docs/testing.md +343 -486
  16. data/docs/transports.md +203 -268
  17. data/docs/upgrading.md +211 -560
  18. data/examples/README.md +38 -0
  19. data/examples/countries.rb +39 -0
  20. data/examples/federation.rb +62 -0
  21. data/examples/github/generate.rb +20 -0
  22. data/examples/github/generated/star_mutation.rb +126 -0
  23. data/examples/github/generated/stargazers_query.rb +232 -0
  24. data/examples/github/generated/starred_query.rb +151 -0
  25. data/examples/github/queries/star.graphql +8 -0
  26. data/examples/github/queries/stargazers.graphql +22 -0
  27. data/examples/github/queries/starred.graphql +11 -0
  28. data/examples/github/run.rb +43 -0
  29. data/examples/github/setup.rb +18 -0
  30. data/examples/rick_and_morty.rb +57 -0
  31. data/graph_weaver.gemspec +12 -3
  32. data/lib/graph_weaver/client.rb +30 -1
  33. data/lib/graph_weaver/codegen/emit.rb +5 -11
  34. data/lib/graph_weaver/codegen.rb +23 -55
  35. data/lib/graph_weaver/context_seam.rb +54 -0
  36. data/lib/graph_weaver/errors.rb +23 -15
  37. data/lib/graph_weaver/federation.rb +11 -2
  38. data/lib/graph_weaver/graph.rb +39 -29
  39. data/lib/graph_weaver/in_process.rb +15 -9
  40. data/lib/graph_weaver/internal/endpoint.rb +7 -5
  41. data/lib/graph_weaver/internal/headers.rb +19 -0
  42. data/lib/graph_weaver/internal/test_clients.rb +7 -11
  43. data/lib/graph_weaver/internal.rb +81 -13
  44. data/lib/graph_weaver/log_subscriber.rb +10 -2
  45. data/lib/graph_weaver/logging.rb +33 -13
  46. data/lib/graph_weaver/query_module.rb +44 -23
  47. data/lib/graph_weaver/retry.rb +12 -8
  48. data/lib/graph_weaver/rspec.rb +13 -24
  49. data/lib/graph_weaver/schema_loader.rb +52 -14
  50. data/lib/graph_weaver/tasks.rb +10 -2
  51. data/lib/graph_weaver/testing/cassette.rb +28 -5
  52. data/lib/graph_weaver/testing/endpoint.rb +14 -13
  53. data/lib/graph_weaver/testing/fake_client.rb +33 -3
  54. data/lib/graph_weaver/testing/router.rb +7 -3
  55. data/lib/graph_weaver/testing.rb +12 -4
  56. data/lib/graph_weaver/transport/http.rb +2 -2
  57. data/lib/graph_weaver/transport.rb +47 -23
  58. data/lib/graph_weaver/version.rb +1 -1
  59. data/lib/graph_weaver.rb +32 -10
  60. metadata +16 -3
  61. data/CHANGELOG.md +0 -3801
@@ -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.2 — 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 — see GraphWeaver::QueryModule
83
+ extend GraphWeaver::QueryModule
84
+
85
+ # the graph this module was generated from — whose client it runs
86
+ # against, and what a test mode builds its stand-in 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.2 — 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 — see GraphWeaver::QueryModule
187
+ extend GraphWeaver::QueryModule
188
+
189
+ # the graph this module was generated from — whose client it runs
190
+ # against, and what a test mode builds its stand-in 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.2 — 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 — see GraphWeaver::QueryModule
107
+ extend GraphWeaver::QueryModule
108
+
109
+ # the graph this module was generated from — whose client it runs
110
+ # against, and what a test mode builds its stand-in 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
@@ -0,0 +1,8 @@
1
+ mutation($id: ID!) {
2
+ addStar(input: { starrableId: $id }) {
3
+ starrable {
4
+ stargazerCount
5
+ viewerHasStarred
6
+ }
7
+ }
8
+ }