graph_weaver 0.7.4 → 0.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +2 -2
  3. data/README.md +1 -0
  4. data/docs/errors.md +12 -5
  5. data/docs/generated_modules.md +134 -17
  6. data/docs/getting_started.md +182 -20
  7. data/docs/logging.md +79 -35
  8. data/docs/migrating.md +126 -0
  9. data/docs/scalars.md +50 -6
  10. data/docs/testing.md +78 -14
  11. data/docs/upgrading.md +44 -2
  12. data/examples/README.md +4 -2
  13. data/examples/github/generate.rb +22 -8
  14. data/examples/github/generated/star_mutation.rb +2 -2
  15. data/examples/github/generated/stargazers_query.rb +2 -2
  16. data/examples/github/generated/starred_query.rb +2 -2
  17. data/examples/github/run.rb +1 -0
  18. data/examples/github/setup.rb +16 -8
  19. data/graph_weaver.gemspec +15 -6
  20. data/lib/generators/graph_weaver/install_generator.rb +49 -2
  21. data/lib/graph_weaver/client.rb +0 -23
  22. data/lib/graph_weaver/codegen/aliases.rb +36 -3
  23. data/lib/graph_weaver/codegen/emit.rb +20 -15
  24. data/lib/graph_weaver/codegen/enum_type.rb +52 -11
  25. data/lib/graph_weaver/codegen/nodes.rb +75 -32
  26. data/lib/graph_weaver/codegen.rb +260 -100
  27. data/lib/graph_weaver/coerce.rb +1 -1
  28. data/lib/graph_weaver/federation.rb +1 -6
  29. data/lib/graph_weaver/graph.rb +55 -5
  30. data/lib/graph_weaver/hints.rb +20 -5
  31. data/lib/graph_weaver/in_process.rb +2 -4
  32. data/lib/graph_weaver/input_struct.rb +50 -10
  33. data/lib/graph_weaver/internal/overrides.rb +126 -14
  34. data/lib/graph_weaver/internal/subgraphs.rb +1 -10
  35. data/lib/graph_weaver/internal/test_clients.rb +29 -7
  36. data/lib/graph_weaver/internal/unused.rb +62 -18
  37. data/lib/graph_weaver/internal/values.rb +24 -7
  38. data/lib/graph_weaver/internal.rb +23 -6
  39. data/lib/graph_weaver/log_subscriber.rb +27 -17
  40. data/lib/graph_weaver/logging.rb +115 -82
  41. data/lib/graph_weaver/parsing.rb +32 -3
  42. data/lib/graph_weaver/query_module.rb +67 -12
  43. data/lib/graph_weaver/railtie.rb +7 -2
  44. data/lib/graph_weaver/rspec.rb +41 -18
  45. data/lib/graph_weaver/schema_diff.rb +24 -5
  46. data/lib/graph_weaver/schema_loader.rb +29 -17
  47. data/lib/graph_weaver/tasks.rb +98 -16
  48. data/lib/graph_weaver/testing/fake_client.rb +28 -31
  49. data/lib/graph_weaver/testing/router.rb +26 -25
  50. data/lib/graph_weaver/testing.rb +27 -8
  51. data/lib/graph_weaver/transport.rb +1 -1
  52. data/lib/graph_weaver/version.rb +1 -1
  53. data/lib/graph_weaver.rb +72 -42
  54. metadata +3 -2
@@ -326,11 +326,14 @@ module GraphWeaver
326
326
 
327
327
  plan = @planner.plan(document, operation_name:)
328
328
  return introspect(query, variables, plan.operation_name) if plan.introspection
329
+ # read once, so every hop runs as the identity the query started
330
+ # with, whatever writes #context= while it is in flight
331
+ context = Internal::Util.context!(@context)
329
332
  # one subgraph answers the whole thing: hand it the document as
330
333
  # written, so nothing is rewritten that didn't have to be
331
- return fetch(plan.entry, query, variables, plan.operation_name) if plan.verbatim
334
+ return fetch(plan.entry, query, variables, plan.operation_name, context) if plan.verbatim
332
335
 
333
- run(plan, variables)
336
+ run(plan, variables, context)
334
337
  end
335
338
 
336
339
  # never leak the context (tokens, current_user) through logs or errors
@@ -374,7 +377,7 @@ module GraphWeaver
374
377
 
375
378
  # ---- execution ----------------------------------------------------
376
379
 
377
- def run(plan, variables)
380
+ def run(plan, variables, context)
378
381
  errors = []
379
382
  data = {}
380
383
  # An operation's declared defaults are part of the variables, and
@@ -384,7 +387,7 @@ module GraphWeaver
384
387
  .merge(variables.to_h { |name, value| [name.to_s, value] })
385
388
 
386
389
  plan.steps.each do |step|
387
- result = fetch_step(step, plan.operation, given)
390
+ result = fetch_step(step, plan.operation, given, context)
388
391
  Array(result["errors"]).each { |error| errors << rewrite(error, step.subgraph) }
389
392
  payload = result["data"]
390
393
  if payload.nil?
@@ -397,7 +400,7 @@ module GraphWeaver
397
400
  end
398
401
  end
399
402
 
400
- plan.steps.each { |step| stitch(step, [[data, []]], plan.operation, given, errors) }
403
+ plan.steps.each { |step| stitch(step, [[data, []]], plan.operation, given, context, errors) }
401
404
 
402
405
  # A stitched fetch can leave a null where the composed schema says
403
406
  # non-null, and nothing re-applies GraphQL's propagation rules over a
@@ -443,7 +446,7 @@ module GraphWeaver
443
446
  # Everything the plan applies at this level: one _entities fetch per
444
447
  # subgraph the level defers to (all nodes at once — _entities answers
445
448
  # in representation order), then the same again one level down.
446
- def stitch(step, nodes, operation, variables, errors)
449
+ def stitch(step, nodes, operation, variables, context, errors)
447
450
  return if nodes.empty?
448
451
 
449
452
  # An abstract position: the plan holds one branch per concrete type
@@ -454,12 +457,12 @@ module GraphWeaver
454
457
  if step.is_a?(Internal::Planner::Branches)
455
458
  step.steps.each do |type_name, branch|
456
459
  stitch(branch, nodes.select { |(node, _)| node[TYPENAME] == type_name },
457
- operation, variables, errors)
460
+ operation, variables, context, errors)
458
461
  end
459
462
  return
460
463
  end
461
464
 
462
- blocked = prefetch(step, nodes, operation, variables, errors)
465
+ blocked = prefetch(step, nodes, operation, variables, context, errors)
463
466
 
464
467
  # A fetch for a selection the operation excluded is a fetch a real
465
468
  # router never makes, and `trace` is something specs assert on. The
@@ -478,7 +481,8 @@ module GraphWeaver
478
481
 
479
482
  entities = []
480
483
  if fetched.any?
481
- result = entities_fetch(target, step.type_name, deferrals.map(&:node), representations, operation, variables)
484
+ result = entities_fetch(target, step.type_name, deferrals.map(&:node), representations, operation,
485
+ variables, context)
482
486
  entities = result.dig("data", "_entities") || []
483
487
  Array(result["errors"]).each { |error| errors << rewrite(error, target, fetched) }
484
488
  end
@@ -502,12 +506,12 @@ module GraphWeaver
502
506
  deferrals.each do |deferral|
503
507
  next unless deferral.step
504
508
 
505
- stitch(deferral.step, descend(nodes, deferral.response_key), operation, variables, errors)
509
+ stitch(deferral.step, descend(nodes, deferral.response_key), operation, variables, context, errors)
506
510
  end
507
511
  end
508
512
 
509
513
  step.children.each do |key, child|
510
- stitch(child, descend(nodes, key), operation, variables, errors)
514
+ stitch(child, descend(nodes, key), operation, variables, context, errors)
511
515
  end
512
516
 
513
517
  nodes.each { |(node, _)| strip!(node, step) }
@@ -517,7 +521,7 @@ module GraphWeaver
517
521
  # keys before the fetch whose representation carries them. Returns the
518
522
  # nodes the holding subgraph didn't recognize: their required fields
519
523
  # don't exist, so nothing depending on them can resolve.
520
- def prefetch(step, nodes, operation, variables, errors)
524
+ def prefetch(step, nodes, operation, variables, context, errors)
521
525
  blocked = []
522
526
  # the field it feeds was excluded, so this is a fetch a real router
523
527
  # never makes — and a test double that runs a resolver production
@@ -533,7 +537,7 @@ module GraphWeaver
533
537
  selections = Internal::Planner.injected_selections(group.flat_map(&:paths).uniq)
534
538
  roots = selections.map(&:alias)
535
539
 
536
- result = entities_fetch(subgraph, step.type_name, selections, representations, operation, variables)
540
+ result = entities_fetch(subgraph, step.type_name, selections, representations, operation, variables, context)
537
541
  entities = result.dig("data", "_entities") || []
538
542
  Array(result["errors"]).each { |error| errors << rewrite(error, subgraph, nodes) }
539
543
 
@@ -621,16 +625,16 @@ module GraphWeaver
621
625
  Array(path).map { |segment| segment.is_a?(String) ? segment.delete_prefix(PREFIX) : segment }
622
626
  end
623
627
 
624
- def fetch_step(step, operation, variables)
628
+ def fetch_step(step, operation, variables, context)
625
629
  document = GraphQL::Language::Nodes::OperationDefinition.new(
626
630
  operation_type: operation.operation_type || "query",
627
631
  variables: used_variables(step.selections, operation),
628
632
  selections: step.selections,
629
633
  )
630
- run_subgraph(step.subgraph, document, variables)
634
+ run_subgraph(step.subgraph, document, variables, context)
631
635
  end
632
636
 
633
- def entities_fetch(subgraph, type_name, nodes, representations, operation, variables)
637
+ def entities_fetch(subgraph, type_name, nodes, representations, operation, variables, context)
634
638
  entities = GraphQL::Language::Nodes::Field.new(
635
639
  name: "_entities",
636
640
  arguments: [GraphQL::Language::Nodes::Argument.new(
@@ -647,7 +651,7 @@ module GraphWeaver
647
651
  variables: [REPRESENTATIONS_DEFINITION] + used_variables(nodes, operation),
648
652
  selections: [entities],
649
653
  )
650
- run_subgraph(subgraph, document, variables.merge(REPRESENTATIONS => representations))
654
+ run_subgraph(subgraph, document, variables.merge(REPRESENTATIONS => representations), context)
651
655
  end
652
656
 
653
657
  REPRESENTATIONS = "representations"
@@ -682,12 +686,12 @@ module GraphWeaver
682
686
  end
683
687
  end
684
688
 
685
- def run_subgraph(subgraph, document, variables)
689
+ def run_subgraph(subgraph, document, variables, context)
686
690
  declared = document.variables.map(&:name)
687
- fetch(subgraph, document.to_query_string, variables.slice(*declared), nil)
691
+ fetch(subgraph, document.to_query_string, variables.slice(*declared), nil, context)
688
692
  end
689
693
 
690
- def fetch(name, query, variables, operation_name)
694
+ def fetch(name, query, variables, operation_name, context)
691
695
  faked = @faked.include?(name)
692
696
  entry = { subgraph: name, query:, variables: variables.to_h }
693
697
  entry[:faked] = true if faked
@@ -701,14 +705,12 @@ module GraphWeaver
701
705
  end
702
706
 
703
707
  GraphWeaver::Internal::Log.log(:debug) do
704
- "router -> #{name} #{tag} variables=#{JSON.generate(GraphWeaver::Internal::Log.filter_variables(variables))}
705
- " \
708
+ "router -> #{name} #{tag} variables=#{JSON.generate(GraphWeaver::Internal::Log.filter_variables(variables))}\n" \
706
709
  "#{GraphWeaver::Internal::Wire.truncate_for_log(query)}"
707
710
  end
708
711
 
709
712
  GraphWeaver::Internal::Log.log_timed(:debug, "router -> #{name} #{tag} completed") do
710
- @subgraphs.fetch(name).execute(query, variables:, operation_name:,
711
- context: Internal::Util.context!(@context)).to_h
713
+ @subgraphs.fetch(name).execute(query, variables:, operation_name:, context:).to_h
712
714
  end
713
715
  end
714
716
 
@@ -783,7 +785,6 @@ module GraphWeaver
783
785
  end
784
786
  ordered
785
787
  end
786
-
787
788
  end
788
789
  end
789
790
  end
@@ -340,10 +340,14 @@ module GraphWeaver
340
340
  "Declare it with the class: GraphWeaver.graph(#{graph.name.inspect}) " \
341
341
  "{ schema -> { MySchema } }."
342
342
  else
343
+ # the helper needs no tag, which is what makes it reachable from
344
+ # BOTH paths into here: a tagged example is already past its own
345
+ # before hook, so "name it in the example" only works if the tag goes
343
346
  "GraphWeaver.client isn't running one in-process to borrow. Name it in the example — " \
344
- "graphql_in_process(MySchema) — or set GraphWeaver::Testing.config.schema = MySchema " \
345
- "for the whole suite. A federated app names the subgraph it means, per example; " \
346
- "graphql: :router runs the graph stitched."
347
+ "graphql_in_process(MySchema) — instead of the graphql: :in_process tag, which " \
348
+ "builds this client in a `before` hook of its own and so is already past. Or set " \
349
+ "GraphWeaver::Testing.config.schema = MySchema for the whole suite. A federated app " \
350
+ "names the subgraph it means, per example; graphql: :router runs the graph stitched."
347
351
  end
348
352
  end
349
353
 
@@ -352,7 +356,6 @@ module GraphWeaver
352
356
  def runnable(schema)
353
357
  schema if schema.is_a?(Class) && schema <= GraphQL::Schema
354
358
  end
355
-
356
359
  end
357
360
 
358
361
  class << self
@@ -412,7 +415,7 @@ module GraphWeaver
412
415
  # the precision case gets exercised at all.
413
416
  def check_scalars!(schema)
414
417
  registry = Internal::Util.registry_for(schema)
415
- values = Internal::Values.new(seed: 0, schema:, registry:)
418
+ values = Internal::Values.new(seed: 0, schema:, registry:, pin_advice: method(:pin_for_check))
416
419
  context = GraphQL::Query.new(schema, "{ __typename }").context
417
420
 
418
421
  disagreed = schema.types.values.sort_by(&:graphql_name).filter_map do |type|
@@ -432,6 +435,14 @@ module GraphWeaver
432
435
 
433
436
  private
434
437
 
438
+ # check_scalars! takes the schema and nothing else, and runs outside
439
+ # every fake — so of the doors onto a pin only the suite-wide one is a
440
+ # door here. graphql_fake is a no-op, and there is no second argument.
441
+ def pin_for_check(name)
442
+ "Pin the form this server sends: GraphWeaver::Testing.config.overrides = " \
443
+ "{ #{name.inspect} => ... }."
444
+ end
445
+
435
446
  # One scalar's verdict, or nil when the two halves agree. Each step is
436
447
  # a different mistake, so each says which.
437
448
  def disagreement(scalar, type, values, context)
@@ -449,8 +460,8 @@ module GraphWeaver
449
460
  begin
450
461
  sample = cast.call(wire)
451
462
  rescue StandardError => e
452
- return "#{name}: cast: can't read #{wire.inspect}, the value fabricated for it (#{e.message}) " \
453
- "— pin the form this server sends: overrides: { #{name.inspect} => ... }"
463
+ return "#{name}: cast: can't read #{wire.inspect}, the value fabricated for it " \
464
+ "(#{e.message}). #{pin_for_check(name)}"
454
465
  end
455
466
 
456
467
  if scalar.serialize? && !scalar.serialize_value?
@@ -476,7 +487,15 @@ module GraphWeaver
476
487
  end
477
488
  return if back == sample
478
489
 
479
- "#{name}: round-trips lossily — sent #{sample.inspect}, got back #{back.inspect}"
490
+ sent = Internal::Redact.spell(sample)
491
+ got = Internal::Redact.spell(back)
492
+ return "#{name}: round-trips lossily — sent #{sent}, got back #{got}" unless sent == got
493
+
494
+ # two identical spellings for two unequal values: the class inherits
495
+ # Object#==, which is identity, so this can't tell a lossy round trip
496
+ # from a faithful one and must not claim either
497
+ "#{name}: #{sample.class} defines no ==, so a round trip can't be checked — sent " \
498
+ "#{sent} and got back #{got}, and nothing here can tell those apart"
480
499
  end
481
500
 
482
501
  # The registration's `cast:`, RUN rather than emitted. A cast builds
@@ -99,7 +99,7 @@ class GraphWeaver::Transport
99
99
  payload = { url: safe_url, operation: operation_name, client: self.class,
100
100
  kind: GraphWeaver::Internal::Wire.kind(query) }
101
101
 
102
- GraphWeaver::Internal::Log.instrument(GraphWeaver::EXECUTE_EVENT, payload) do
102
+ GraphWeaver::Internal::Log.instrument_request(payload) do
103
103
  perform(query, variables, operation_name, payload)
104
104
  end
105
105
  end
@@ -1,3 +1,3 @@
1
1
  module GraphWeaver
2
- VERSION = "0.7.4"
2
+ VERSION = "0.7.6"
3
3
  end
data/lib/graph_weaver.rb CHANGED
@@ -214,7 +214,7 @@ module GraphWeaver
214
214
  attr_accessor :skip_generated_load
215
215
 
216
216
  # The name of the shared module — the types that live once per schema
217
- # (input types, enums, unions hoisted from shared fragments) and are
217
+ # (input types, enums, the types hoisted from shared fragments) and are
218
218
  # aliased into every query module that touches them. Constant, not derived
219
219
  # from where you put the files: set it globally, or pass types_module: per
220
220
  # generate!. A multi-schema layout names it in the same initializer that
@@ -614,8 +614,8 @@ module GraphWeaver
614
614
  # # "line" => 4, "column" => 5 }] }
615
615
  #
616
616
  # Empty means every query validates. schema: defaults to the server as
617
- # it is now — a FRESH introspection of the url the dump records, or the
618
- # live schema class when the app default runs in-process — and the dump
617
+ # it is now — a FRESH introspection of the source behind each graph's
618
+ # dump, or the live schema class where that source IS one — and the dump
619
619
  # is left alone; pass schema: and nothing touches the network.
620
620
  #
621
621
  # When that dump is a composed supergraph, an error naming a type is
@@ -623,17 +623,25 @@ module GraphWeaver
623
623
  # (products, reviews)", plus a "subgraphs" key — since knowing whose
624
624
  # code to look at is half the answer. A plain schema is unaffected.
625
625
  #
626
- # One query you have as a *string* is Client#check_query the same
627
- # entries, against that client's own schema.
626
+ # One query you have as a *string* is #check_query, which every client
627
+ # holding a schema answers — the same entries, against that schema.
628
628
  #
629
629
  # A different question from verify_generated!, which asks whether the
630
630
  # committed Ruby matches the committed schema. `rake
631
631
  # graph_weaver:queries:check` prints this and exits non-zero.
632
632
  def check_queries(schema: nil, queries: nil, fragments: fragments_paths)
633
633
  shared = Codegen.load_fragments(fragments)
634
-
635
- graphs_for(schema:, queries:).each_with_object({}) do |graph, failures|
636
- checked = checked_schema(graph)
634
+ # one graph's server being down is that graph's verdict: every graph is
635
+ # attempted, so a run names every server it couldn't reach rather than
636
+ # ending at the first
637
+ unreachable = []
638
+
639
+ failures = graphs_for(schema:, queries:).each_with_object({}) do |graph, failures|
640
+ begin
641
+ checked = checked_schema(graph)
642
+ rescue Error => e
643
+ next unreachable << unreachable_source(graph, e)
644
+ end
637
645
  table = checked_routing_table(graph)
638
646
  Internal::Util.query_files(graph.queries).each do |path|
639
647
  errors = Internal::QueryCheck.errors(checked, File.read(path), shared, table)
@@ -646,13 +654,39 @@ module GraphWeaver
646
654
  failures[key] = failures.key?(key) ? failures[key] | errors : errors
647
655
  end
648
656
  end
657
+
658
+ raise Error, unreachable.join("\n") unless unreachable.empty?
659
+
660
+ failures
649
661
  end
650
662
 
651
- # What this graph is checked against. A graph that names its schema is
652
- # checked against exactly that, so nothing touches the network; the default
653
- # graph names none, so its dump is re-introspected first (see
654
- # refreshed_schema).
655
- def checked_schema(graph) = graph.named_schema? ? graph.schema : refreshed_schema
663
+ # Which graph couldn't be checked, and against what. The bare socket error
664
+ # named neither in a multi-graph app it was the entire output, and the
665
+ # reader could not tell which of their servers was down.
666
+ def unreachable_source(graph, error)
667
+ whose = graph.name ? "graph #{graph.name.inspect}" : "this app"
668
+ dump = graph.dump_path || graph.named_dump_path
669
+ subject = dump ? Internal::Util.relative(dump) : "these queries"
670
+ "#{whose}: couldn't reach the schema behind #{subject} — #{error.message}"
671
+ end
672
+ private :unreachable_source
673
+
674
+ # What this graph is checked against: Graph#source, the rule `schema:refresh`
675
+ # and `schema:diff` follow too. A live class is asked directly — for an app
676
+ # that IS the server, a dump is a snapshot of its own code, and checking
677
+ # against it reports phantom errors about a field just added. A url is
678
+ # re-introspected, so no refresh step (and no rewritten dump) is needed
679
+ # first. A graph with neither — hand-written SDL nothing serves, a composed
680
+ # supergraph — has nothing to re-read, so it is checked as committed.
681
+ def checked_schema(graph)
682
+ source = graph.source
683
+ return source if source.is_a?(Module)
684
+ # Internal::Util.locate_schema! raises the conventional "no schema dump"
685
+ # message when the default graph has no dump either
686
+ return graph.schema unless source
687
+
688
+ SchemaLoader.introspect(graph.source_transport)
689
+ end
656
690
  private :checked_schema
657
691
 
658
692
  # The routing table behind the schema check_queries is about to use, when
@@ -663,28 +697,6 @@ module GraphWeaver
663
697
  end
664
698
  private :checked_routing_table
665
699
 
666
- # The schema check_queries defaults to: the server as it is now. Over a
667
- # socket that's a fresh introspection of the url the local dump recorded,
668
- # so no refresh step (and no rewritten dump) is needed first. In-process
669
- # it's the live schema class — for an app that IS the server, a dump is a
670
- # snapshot of its own code, and checking against it reports phantom
671
- # errors about a field you just added. Dumps with no url and no live
672
- # class — hand-written SDL, a composed supergraph — have nothing to
673
- # re-read, so they're checked as they are.
674
- def refreshed_schema
675
- live = Internal::Util.live_schema
676
- return live if live
677
-
678
- # Internal::Util.locate_schema! raises the conventional "no schema dump" message
679
- path = SchemaLoader.locate_path or Internal::Util.locate_schema!
680
- return SchemaLoader.load(path) unless SchemaLoader.provenance(path)&.key?("url")
681
-
682
- # source_transport rather than one built here: it reads the auth ENV var
683
- # the dump named, so `--auth MY_TOKEN` reaches this path too
684
- SchemaLoader.introspect(SchemaLoader.source_transport(path))
685
- end
686
- private :refreshed_schema
687
-
688
700
  # Load the generated modules — one line in an initializer or spec
689
701
  # helper (loading happens only when you call this; skip it and
690
702
  # require files yourself if you'd rather):
@@ -795,7 +807,7 @@ module GraphWeaver
795
807
 
796
808
  # (filename, source) per artifact. Types a schema shares across queries —
797
809
  # input types, schema enums, and each named shared fragment spread as a
798
- # whole-union field — are emitted once into the shared module, with query
810
+ # whole field — are emitted once into the shared module, with query
799
811
  # modules aliasing what they use. That's the difference between hundreds of
800
812
  # duplicated bool_exp structs (or one Ruby class per query for the same
801
813
  # schema enum) and one copy per schema. (Single-query parse inlines
@@ -814,7 +826,7 @@ module GraphWeaver
814
826
  @unmatched_registrations |= registry.unmatched_registrations(schema)
815
827
 
816
828
  used = { inputs: [], enums: [], mapped: [] }
817
- used_unions = []
829
+ hoisted = []
818
830
  helpers = []
819
831
  shared = Codegen.load_fragments(fragments)
820
832
 
@@ -831,30 +843,30 @@ module GraphWeaver
831
843
  name:,
832
844
  graph_name: graph.name,
833
845
  types_namespace: graph.types_module,
834
- hoistable_unions: Codegen.shared_fragment_spreads(source, shared, path),
846
+ hoistable_fragments: Codegen.shared_fragment_spreads(source, shared, path),
835
847
  path:,
836
848
  registry:,
837
849
  )
838
850
  out = codegen.generate
839
851
  codegen.variable_type_names.each { |kind, names| used[kind] |= names }
840
852
  found.concat(codegen.untyped_scalars).uniq!
841
- used_unions |= codegen.used_union_names
853
+ hoisted |= codegen.used_fragment_names
842
854
  helpers |= codegen.block_helpers
843
855
  [filename, out]
844
856
  rescue GraphWeaver::Error => e
845
857
  # collected, not raised: nothing is written either way, and an adopter
846
858
  # aiming generate! at an existing query directory wants the list
847
- refusals << e
859
+ refusals << name_query_file(e, path)
848
860
  nil
849
861
  end
850
862
  refuse_all!(refusals, paths.size)
851
863
 
852
- if used_unions.any? || used.values.any?(&:any?)
864
+ if hoisted.any? || used.values.any?(&:any?)
853
865
  refuse_duplicate_types!(seen, graph)
854
866
  codegen = Codegen.new(schema:, query: "", name: graph.types_module, registry:)
855
867
  types = codegen.generate_types(
856
868
  inputs: used[:inputs], enums: used[:enums] + used[:mapped],
857
- unions: used_unions, fragments: shared,
869
+ hoisted:, fragments: shared,
858
870
  )
859
871
  found.concat(codegen.untyped_scalars).uniq!
860
872
  helpers |= codegen.block_helpers
@@ -907,6 +919,18 @@ module GraphWeaver
907
919
  # itself, so a single bad file reads exactly as it always has — class,
908
920
  # message and all; several become one list, because clearing them a file
909
921
  # per run is the slowest way there is to adopt this.
922
+ # Almost every codegen refusal is about the query in front of it and never
923
+ # says which file that was — so a list of them named nothing, which is the
924
+ # one thing a list is for. This rescue is the only place holding both.
925
+ # #exception keeps the class and its fields and swaps only the message.
926
+ def name_query_file(error, path)
927
+ reported = Internal::Util.relative(path)
928
+ return error if error.message.include?(reported)
929
+
930
+ error.exception("#{reported}: #{error.message}")
931
+ end
932
+ private :name_query_file
933
+
910
934
  def refuse_all!(refusals, considered)
911
935
  return if refusals.empty?
912
936
  raise refusals.first if refusals.one?
@@ -1051,6 +1075,12 @@ module GraphWeaver
1051
1075
  # mid-rename needs:
1052
1076
  #
1053
1077
  # GraphWeaver.register_enum("Status", alias: { "legacy_mode" => "LEGACY_MODE" })
1078
+ #
1079
+ # fallback: true is the type-less form of forward-compat: the generated
1080
+ # enum gains an Other member and casts every value the schema doesn't
1081
+ # declare to it, so a server adding one doesn't take the client down.
1082
+ #
1083
+ # GraphWeaver.register_enum("Species", fallback: true)
1054
1084
  def register_enum(graphql_name, type = nil, positional_map = nil, map: nil, fallback: nil, requires: nil,
1055
1085
  alias: nil)
1056
1086
  # `alias` is a Ruby keyword, so the parameter is only readable through binding
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.4
4
+ version: 0.7.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Pepper
@@ -281,6 +281,7 @@ files:
281
281
  - docs/getting_started.md
282
282
  - docs/i18n.md
283
283
  - docs/logging.md
284
+ - docs/migrating.md
284
285
  - docs/real_world.md
285
286
  - docs/scalars.md
286
287
  - docs/testing.md
@@ -363,7 +364,7 @@ licenses:
363
364
  - MIT
364
365
  metadata:
365
366
  bug_tracker_uri: https://github.com/dpep/graph_weaver/issues
366
- changelog_uri: https://github.com/dpep/graph_weaver/blob/v0.7.4/CHANGELOG.md
367
+ changelog_uri: https://github.com/dpep/graph_weaver/blob/v0.7.6/CHANGELOG.md
367
368
  documentation_uri: https://github.com/dpep/graph_weaver/tree/main/docs
368
369
  rubygems_mfa_required: 'true'
369
370
  source_code_uri: https://github.com/dpep/graph_weaver