graph_weaver 0.7.0 → 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.
Files changed (55) 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 +370 -459
  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 +340 -486
  16. data/docs/transports.md +191 -263
  17. data/docs/upgrading.md +188 -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 +22 -1
  33. data/lib/graph_weaver/codegen.rb +5 -1
  34. data/lib/graph_weaver/context_seam.rb +54 -0
  35. data/lib/graph_weaver/errors.rb +23 -15
  36. data/lib/graph_weaver/federation.rb +11 -2
  37. data/lib/graph_weaver/in_process.rb +15 -9
  38. data/lib/graph_weaver/internal/endpoint.rb +7 -5
  39. data/lib/graph_weaver/internal/headers.rb +19 -0
  40. data/lib/graph_weaver/internal.rb +66 -13
  41. data/lib/graph_weaver/log_subscriber.rb +10 -2
  42. data/lib/graph_weaver/logging.rb +33 -13
  43. data/lib/graph_weaver/query_module.rb +8 -0
  44. data/lib/graph_weaver/retry.rb +12 -8
  45. data/lib/graph_weaver/schema_loader.rb +52 -14
  46. data/lib/graph_weaver/testing/cassette.rb +28 -5
  47. data/lib/graph_weaver/testing/endpoint.rb +14 -13
  48. data/lib/graph_weaver/testing/fake_client.rb +33 -3
  49. data/lib/graph_weaver/testing/router.rb +7 -3
  50. data/lib/graph_weaver/transport/http.rb +2 -2
  51. data/lib/graph_weaver/transport.rb +47 -23
  52. data/lib/graph_weaver/version.rb +1 -1
  53. data/lib/graph_weaver.rb +22 -1
  54. metadata +16 -3
  55. data/CHANGELOG.md +0 -3801
@@ -96,7 +96,8 @@ class GraphWeaver::Transport
96
96
  # a raw query string falls back to the name in the document itself.
97
97
  def execute(query, variables: {}, operation_name: nil)
98
98
  operation_name ||= GraphWeaver::Internal::Wire.operation_name(query)
99
- payload = { url: safe_url, operation: operation_name, client: self.class }
99
+ payload = { url: safe_url, operation: operation_name, client: self.class,
100
+ kind: GraphWeaver::Internal::Wire.kind(query) }
100
101
 
101
102
  GraphWeaver::Internal::Log.instrument(GraphWeaver::EXECUTE_EVENT, payload) do
102
103
  perform(query, variables, operation_name, payload)
@@ -132,12 +133,24 @@ class GraphWeaver::Transport
132
133
  post(encoded)
133
134
  end
134
135
  rescue *GraphWeaver.transport_errors.to_a => e
135
- # never got a response — DNS, connection refused/reset, TLS, timeout
136
- raise GraphWeaver::TransportError.new("#{e.class}: #{e.message}", url: safe_url)
136
+ # never got a response — DNS, connection refused/reset, TLS, timeout.
137
+ # The adapter's sentence is its own words, capped like any text we
138
+ # didn't author.
139
+ raise GraphWeaver::TransportError.new(
140
+ "#{e.class}: #{GraphWeaver::Internal::Redact.cap(e.message)}", url: safe_url,
141
+ )
137
142
  end
138
143
 
139
144
  payload[:http_status] = status
140
- GraphWeaver::Internal::Log.log(:debug) { "HTTP #{status} #{tag} from #{safe_url} (#{body.to_s.bytesize} bytes)" }
145
+ # folded once: a third-party subclass's headers may come back in any
146
+ # casing, and a subclass that returns none says nothing
147
+ fields = GraphWeaver::Internal::Headers.wrap(headers || {})
148
+ # the content type, not the body: it is what tells a proxy's HTML page
149
+ # from a router's JSON without quoting bytes a server chose
150
+ GraphWeaver::Internal::Log.log(:debug) do
151
+ type = GraphWeaver::Internal::Redact.tag(fields["content-type"])
152
+ "HTTP #{status} #{tag} from #{safe_url} (#{body.to_s.bytesize} bytes#{", #{type}" if type})"
153
+ end
141
154
 
142
155
  parsed = parse_body(body)
143
156
 
@@ -151,42 +164,54 @@ class GraphWeaver::Transport
151
164
  # `"errors": null` (or []) isn't a structured error response, so the
152
165
  # status stays the signal
153
166
  if parsed.is_a?(Hash) && parsed["errors"].is_a?(Array) && parsed["errors"].any?
154
- return Envelope.new(parsed, status)
167
+ return Envelope.new(parsed, status, fields.retry_after)
155
168
  end
156
169
 
157
- raise GraphWeaver::ServerError.new(status:, body: body.to_s, headers: headers || {}, url: safe_url)
170
+ refuse!(status, body, headers)
158
171
  end
159
172
 
160
173
  unless parsed.is_a?(Hash)
161
174
  # a 200 that isn't a GraphQL object — an HTML error page from a proxy, a
162
175
  # captive portal, or a bare JSON array/string: the server misbehaved.
163
- # An empty body says so rather than trailing off after the colon, and a
164
- # well-formed @defer stream is named rather than dumped: it isn't
176
+ # A well-formed @defer stream is named rather than lumped in: it isn't
165
177
  # non-GraphQL, it's more than one GraphQL document.
166
- quoted =
167
- if incremental?(headers)
178
+ detail =
179
+ if incremental?(fields)
168
180
  "this response is incremental delivery (@defer/@stream), which this client doesn't read"
169
181
  elsif body.to_s.empty?
170
182
  "empty response body"
171
183
  else
172
- "non-GraphQL response: #{body.to_s[0, 500]}"
184
+ "non-GraphQL response"
173
185
  end
174
- raise GraphWeaver::ServerError.new(status:, body: quoted, headers: headers || {}, url: safe_url)
186
+ refuse!(status, body, headers, detail:)
175
187
  end
176
188
 
177
- Envelope.new(parsed, status)
189
+ Envelope.new(parsed, status, fields.retry_after)
190
+ end
191
+
192
+ # The response wasn't one we can read. A body is never quoted — not into the
193
+ # message, not into a log line: it is text a server chose, and an error page
194
+ # that echoes the request fills it with the variables and the Authorization
195
+ # header we just sent. `detail` is what WE say went wrong; the bytes are on
196
+ # ServerError#body for whoever rescues it.
197
+ private def refuse!(status, body, headers, detail: nil)
198
+ raise GraphWeaver::ServerError.new(status:, body: body.to_s, headers: headers || {}, url: safe_url, detail:)
178
199
  end
179
200
 
180
- # The parsed envelope, plus the HTTP status it came back on — a Hash to
181
- # everything that reads a GraphQL response, and to the one caller that
182
- # needs more. Retry asks: a router answers rate limiting with a 503 AND
183
- # an errors body, so the body alone can't say whether to come back.
201
+ # The parsed envelope, plus what the HTTP response said around it — a Hash
202
+ # to everything that reads a GraphQL response, and more to the one caller
203
+ # that needs it. Retry asks both: a router answers rate limiting with a 503
204
+ # or 429 AND an errors body, so the body alone can't say whether to come
205
+ # back, and Retry-After says when. The seconds, not the headers — that is
206
+ # the whole of what Retry asks, and every other header stays where a
207
+ # ServerError already carries it.
184
208
  class Envelope < Hash
185
- attr_reader :http_status
209
+ attr_reader :http_status, :retry_after
186
210
 
187
- def initialize(parsed, http_status)
211
+ def initialize(parsed, http_status, retry_after = nil)
188
212
  super()
189
213
  @http_status = http_status
214
+ @retry_after = retry_after
190
215
  update(parsed)
191
216
  end
192
217
  end
@@ -201,10 +226,9 @@ class GraphWeaver::Transport
201
226
  private_constant :BOM
202
227
 
203
228
  # A multipart/mixed body is one @defer/@stream response arriving in
204
- # installments. Folded here because a third-party subclass's headers may
205
- # come back in any casing; a subclass that returns none says nothing.
206
- private def incremental?(headers)
207
- GraphWeaver::Internal::Headers.wrap(headers || {})["content-type"].to_s.start_with?("multipart/mixed")
229
+ # installments.
230
+ private def incremental?(fields)
231
+ fields["content-type"].to_s.start_with?("multipart/mixed")
208
232
  end
209
233
 
210
234
  # the parsed body, or nil when it isn't JSON (a caller's connection may
@@ -1,3 +1,3 @@
1
1
  module GraphWeaver
2
- VERSION = "0.7.0"
2
+ VERSION = "0.7.1"
3
3
  end
data/lib/graph_weaver.rb CHANGED
@@ -849,7 +849,9 @@ module GraphWeaver
849
849
  used_unions = []
850
850
  shared = Codegen.load_fragments(fragments)
851
851
 
852
- plan = Internal::Util.query_files(graph.queries).map do |path|
852
+ refusals = []
853
+ paths = Internal::Util.query_files(graph.queries)
854
+ plan = paths.filter_map do |path|
853
855
  source = File.read(path)
854
856
  name, filename = graph.generated_names(path, source)
855
857
  refuse_duplicate!(seen, name, filename, graph, path)
@@ -870,7 +872,13 @@ module GraphWeaver
870
872
  found.concat(codegen.untyped_scalars).uniq!
871
873
  used_unions |= codegen.used_union_names
872
874
  [filename, out]
875
+ rescue GraphWeaver::Error => e
876
+ # collected, not raised: nothing is written either way, and an adopter
877
+ # aiming generate! at an existing query directory wants the list
878
+ refusals << e
879
+ nil
873
880
  end
881
+ refuse_all!(refusals, paths.size)
874
882
 
875
883
  if used_unions.any? || used.values.any?(&:any?)
876
884
  refuse_duplicate_types!(seen, graph)
@@ -890,6 +898,19 @@ module GraphWeaver
890
898
  end
891
899
  private :generation_plan
892
900
 
901
+ # Every query that refused, in one error. One refusal is re-raised as
902
+ # itself, so a single bad file reads exactly as it always has — class,
903
+ # message and all; several become one list, because clearing them a file
904
+ # per run is the slowest way there is to adopt this.
905
+ def refuse_all!(refusals, considered)
906
+ return if refusals.empty?
907
+ raise refusals.first if refusals.one?
908
+
909
+ raise GraphWeaver::Error, "#{refusals.size} of #{considered} queries refused:\n" +
910
+ refusals.map { |refusal| refusal.message.gsub(/^/, " ") }.join("\n")
911
+ end
912
+ private :refuse_all!
913
+
893
914
  # Two query files landing on one constant, or on one output file. Within a
894
915
  # graph the fix is a rename, as it has always been; across two graphs it is
895
916
  # `namespace:` for the constant and `output:` for the file — so the message
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graph_weaver
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Pepper
@@ -268,7 +268,6 @@ extensions: []
268
268
  extra_rdoc_files: []
269
269
  files:
270
270
  - ".yardopts"
271
- - CHANGELOG.md
272
271
  - Gemfile
273
272
  - Gemfile.lock
274
273
  - LICENSE.txt
@@ -287,6 +286,19 @@ files:
287
286
  - docs/testing.md
288
287
  - docs/transports.md
289
288
  - docs/upgrading.md
289
+ - examples/README.md
290
+ - examples/countries.rb
291
+ - examples/federation.rb
292
+ - examples/github/generate.rb
293
+ - examples/github/generated/star_mutation.rb
294
+ - examples/github/generated/stargazers_query.rb
295
+ - examples/github/generated/starred_query.rb
296
+ - examples/github/queries/star.graphql
297
+ - examples/github/queries/stargazers.graphql
298
+ - examples/github/queries/starred.graphql
299
+ - examples/github/run.rb
300
+ - examples/github/setup.rb
301
+ - examples/rick_and_morty.rb
290
302
  - graph_weaver.gemspec
291
303
  - lib/generators/graph_weaver/install_generator.rb
292
304
  - lib/graph_weaver.rb
@@ -300,6 +312,7 @@ files:
300
312
  - lib/graph_weaver/codegen/scalar_type.rb
301
313
  - lib/graph_weaver/codegen/type_helpers.rb
302
314
  - lib/graph_weaver/coerce.rb
315
+ - lib/graph_weaver/context_seam.rb
303
316
  - lib/graph_weaver/errors.rb
304
317
  - lib/graph_weaver/federation.rb
305
318
  - lib/graph_weaver/graph.rb
@@ -350,7 +363,7 @@ licenses:
350
363
  - MIT
351
364
  metadata:
352
365
  bug_tracker_uri: https://github.com/dpep/graph_weaver/issues
353
- changelog_uri: https://github.com/dpep/graph_weaver/blob/main/CHANGELOG.md
366
+ changelog_uri: https://github.com/dpep/graph_weaver/blob/v0.7.1/CHANGELOG.md
354
367
  documentation_uri: https://github.com/dpep/graph_weaver/tree/main/docs
355
368
  rubygems_mfa_required: 'true'
356
369
  source_code_uri: https://github.com/dpep/graph_weaver