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
@@ -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.2"
3
3
  end
data/lib/graph_weaver.rb CHANGED
@@ -65,8 +65,8 @@ module GraphWeaver
65
65
  #
66
66
  # Anything satisfying the execute contract — a Client, a schema class,
67
67
  # a transport, a fake (testing's graphql: tag swaps one in per
68
- # example). Generated modules resolve per call -> per module
69
- # (MyQuery.client=) -> baked constant -> here.
68
+ # example). Generated modules resolve per call -> a test mode's stand-in
69
+ # -> the client their graph names -> here.
70
70
  attr_accessor :client
71
71
 
72
72
  # the default client, when one is required
@@ -389,7 +389,7 @@ module GraphWeaver
389
389
  # (see #changed_files). Generated files the plan no longer produces are deleted
390
390
  # (see #orphaned), so renaming or dropping a .graphql leaves nothing
391
391
  # behind. Pair with a freshness spec (docs/generated_modules.md).
392
- def generate!(schema: nil, queries: nil, output: nil, client: nil, types_module: nil)
392
+ def generate!(schema: nil, queries: nil, output: nil, types_module: nil)
393
393
  @changed_files = []
394
394
  @unmatched_registrations = []
395
395
  @untyped_scalars_by_graph = {}
@@ -399,7 +399,7 @@ module GraphWeaver
399
399
  # tree exactly as it was — the railtie's watch mode regenerates on a
400
400
  # request and promises a failed save changes nothing, and that promise
401
401
  # was true within a graph and false across them.
402
- planned = graphs_for(schema:, queries:, output:, client:, types_module:).map do |graph|
402
+ planned = graphs_for(schema:, queries:, output:, types_module:).map do |graph|
403
403
  if Internal::Util.query_files(graph.queries).empty?
404
404
  # a brand-new app legitimately has none; a mistyped queries_paths looks
405
405
  # exactly the same, and prints nothing either way
@@ -488,11 +488,11 @@ module GraphWeaver
488
488
  # it "generated queries are current" do
489
489
  # GraphWeaver.verify_generated!
490
490
  # end
491
- def verify_generated!(schema: nil, queries: nil, output: nil, client: nil, types_module: nil)
491
+ def verify_generated!(schema: nil, queries: nil, output: nil, types_module: nil)
492
492
  @unmatched_registrations = []
493
493
  @untyped_scalars_by_graph = {}
494
494
  seen = new_seen
495
- graphs = graphs_for(schema:, queries:, output:, client:, types_module:)
495
+ graphs = graphs_for(schema:, queries:, output:, types_module:)
496
496
 
497
497
  # The dump is checked in too, and everything below reads it — so a
498
498
  # stale one is answered before staleness downstream of it, because
@@ -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)
@@ -858,7 +860,6 @@ module GraphWeaver
858
860
  schema:,
859
861
  query: Codegen.inline_fragments(source, shared, path),
860
862
  name:,
861
- client: graph.client,
862
863
  graph_name: graph.name,
863
864
  types_namespace: graph.types_module,
864
865
  hoistable_unions: Codegen.shared_fragment_spreads(source, shared, path),
@@ -870,7 +871,13 @@ module GraphWeaver
870
871
  found.concat(codegen.untyped_scalars).uniq!
871
872
  used_unions |= codegen.used_union_names
872
873
  [filename, out]
874
+ rescue GraphWeaver::Error => e
875
+ # collected, not raised: nothing is written either way, and an adopter
876
+ # aiming generate! at an existing query directory wants the list
877
+ refusals << e
878
+ nil
873
879
  end
880
+ refuse_all!(refusals, paths.size)
874
881
 
875
882
  if used_unions.any? || used.values.any?(&:any?)
876
883
  refuse_duplicate_types!(seen, graph)
@@ -890,6 +897,19 @@ module GraphWeaver
890
897
  end
891
898
  private :generation_plan
892
899
 
900
+ # Every query that refused, in one error. One refusal is re-raised as
901
+ # itself, so a single bad file reads exactly as it always has — class,
902
+ # message and all; several become one list, because clearing them a file
903
+ # per run is the slowest way there is to adopt this.
904
+ def refuse_all!(refusals, considered)
905
+ return if refusals.empty?
906
+ raise refusals.first if refusals.one?
907
+
908
+ raise GraphWeaver::Error, "#{refusals.size} of #{considered} queries refused:\n" +
909
+ refusals.map { |refusal| refusal.message.gsub(/^/, " ") }.join("\n")
910
+ end
911
+ private :refuse_all!
912
+
893
913
  # Two query files landing on one constant, or on one output file. Within a
894
914
  # graph the fix is a rename, as it has always been; across two graphs it is
895
915
  # `namespace:` for the constant and `output:` for the file — so the message
@@ -1061,8 +1081,10 @@ module GraphWeaver
1061
1081
  # name derived from the file name and the operation — see #module_name) or
1062
1082
  # a raw query string (name derived from the operation name, falling back to
1063
1083
  # "Query" for anonymous operations — collisions are impossible since each
1064
- # parse gets its own container). Pass name: to override, client: to bake
1065
- # the module's default client/transport.
1084
+ # parse gets its own container). Pass name: to override, client: to say
1085
+ # what the module runs against — a parsed module generates no file, so it
1086
+ # has no graph to read one off, and this is the only time one is bound
1087
+ # (a per-call `client:` still wins).
1066
1088
  #
1067
1089
  # graph: names the graph this module belongs to, which is what a test mode
1068
1090
  # runs it against in an app with more than one — the same thing generation
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.2
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.2/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