graph_weaver 0.7.3 → 0.7.5

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +2 -2
  3. data/README.md +1 -0
  4. data/docs/errors.md +5 -2
  5. data/docs/federation.md +3 -2
  6. data/docs/generated_modules.md +176 -22
  7. data/docs/getting_started.md +174 -14
  8. data/docs/i18n.md +4 -4
  9. data/docs/migrating.md +119 -0
  10. data/docs/scalars.md +161 -35
  11. data/docs/testing.md +24 -3
  12. data/docs/upgrading.md +51 -5
  13. data/examples/github/generated/star_mutation.rb +24 -2
  14. data/examples/github/generated/stargazers_query.rb +61 -5
  15. data/examples/github/generated/starred_query.rb +33 -3
  16. data/lib/generators/graph_weaver/install_generator.rb +32 -3
  17. data/lib/graph_weaver/client.rb +23 -0
  18. data/lib/graph_weaver/codegen/aliases.rb +23 -2
  19. data/lib/graph_weaver/codegen/emit.rb +35 -16
  20. data/lib/graph_weaver/codegen/enum_type.rb +149 -19
  21. data/lib/graph_weaver/codegen/nodes.rb +72 -37
  22. data/lib/graph_weaver/codegen/scalar_type.rb +72 -18
  23. data/lib/graph_weaver/codegen/type_helpers.rb +71 -13
  24. data/lib/graph_weaver/codegen.rb +259 -106
  25. data/lib/graph_weaver/coerce.rb +25 -6
  26. data/lib/graph_weaver/federation.rb +1 -6
  27. data/lib/graph_weaver/graph.rb +4 -1
  28. data/lib/graph_weaver/hints.rb +23 -5
  29. data/lib/graph_weaver/in_process.rb +1 -3
  30. data/lib/graph_weaver/input_struct.rb +31 -10
  31. data/lib/graph_weaver/internal/subgraphs.rb +1 -10
  32. data/lib/graph_weaver/internal/unused.rb +32 -7
  33. data/lib/graph_weaver/internal/values.rb +12 -4
  34. data/lib/graph_weaver/internal.rb +84 -0
  35. data/lib/graph_weaver/logging.rb +26 -29
  36. data/lib/graph_weaver/query_module.rb +20 -5
  37. data/lib/graph_weaver/railtie.rb +7 -2
  38. data/lib/graph_weaver/rspec.rb +0 -1
  39. data/lib/graph_weaver/schema_loader.rb +7 -8
  40. data/lib/graph_weaver/tasks.rb +60 -5
  41. data/lib/graph_weaver/testing/fake_client.rb +4 -10
  42. data/lib/graph_weaver/testing/router.rb +26 -25
  43. data/lib/graph_weaver/testing.rb +101 -1
  44. data/lib/graph_weaver/version.rb +1 -1
  45. data/lib/graph_weaver.rb +80 -74
  46. metadata +3 -2
data/lib/graph_weaver.rb CHANGED
@@ -29,7 +29,11 @@ module GraphWeaver
29
29
  # How far into a file to look for it: the header sits under the `typed:` and
30
30
  # `frozen_string_literal:` magic comments, never deeper.
31
31
  HEADER_SCAN_LINES = 10
32
- private_constant :GENERATED_HEADER, :HEADER_SCAN_LINES
32
+
33
+ # Where a graph declares the modules its extend_type blocks mint — an .rbi,
34
+ # so the declaration reaches `srb tc` and nothing else (see helpers_rbi).
35
+ HELPERS_RBI = "type_helpers.rbi"
36
+ private_constant :GENERATED_HEADER, :HEADER_SCAN_LINES, :HELPERS_RBI
33
37
 
34
38
  class << self
35
39
  # A client for one GraphQL server — transport, schema, and scoped
@@ -210,7 +214,7 @@ module GraphWeaver
210
214
  attr_accessor :skip_generated_load
211
215
 
212
216
  # The name of the shared module — the types that live once per schema
213
- # (input types, enums, unions hoisted from shared fragments) and are
217
+ # (input types, enums, the types hoisted from shared fragments) and are
214
218
  # aliased into every query module that touches them. Constant, not derived
215
219
  # from where you put the files: set it globally, or pass types_module: per
216
220
  # generate!. A multi-schema layout names it in the same initializer that
@@ -484,12 +488,13 @@ module GraphWeaver
484
488
  end
485
489
  private :orphaned
486
490
 
487
- # Every .rb under output that GraphWeaver wrote, identified by the header
491
+ # Every file under output that GraphWeaver wrote, identified by the header
488
492
  # it emits. The header — not a *_query.rb glob — is what makes pruning
489
493
  # safe: this is a real directory, and a hand-written file in it must
490
- # survive regeneration.
494
+ # survive regeneration. .rbi too: a stale type-helper declaration would
495
+ # keep an app's srb tc green over an include that is gone.
491
496
  def generated_files(output)
492
- Dir[File.join(Internal::Util.resolve(output), "**/*.rb")].sort.select do |path|
497
+ Dir[File.join(Internal::Util.resolve(output), "**/*.{rb,rbi}")].sort.select do |path|
493
498
  File.foreach(path).first(HEADER_SCAN_LINES).any? { |line| line.start_with?(GENERATED_HEADER) }
494
499
  end
495
500
  end
@@ -618,6 +623,9 @@ module GraphWeaver
618
623
  # (products, reviews)", plus a "subgraphs" key — since knowing whose
619
624
  # code to look at is half the answer. A plain schema is unaffected.
620
625
  #
626
+ # One query you have as a *string* is Client#check_query — the same
627
+ # entries, against that client's own schema.
628
+ #
621
629
  # A different question from verify_generated!, which asks whether the
622
630
  # committed Ruby matches the committed schema. `rake
623
631
  # graph_weaver:queries:check` prints this and exits non-zero.
@@ -628,7 +636,7 @@ module GraphWeaver
628
636
  checked = checked_schema(graph)
629
637
  table = checked_routing_table(graph)
630
638
  Internal::Util.query_files(graph.queries).each do |path|
631
- errors = validation_errors(checked, File.read(path), shared, table)
639
+ errors = Internal::QueryCheck.errors(checked, File.read(path), shared, table)
632
640
  next if errors.empty?
633
641
 
634
642
  # keyed by file, as it has always been — and two graphs may share a
@@ -647,20 +655,11 @@ module GraphWeaver
647
655
  def checked_schema(graph) = graph.named_schema? ? graph.schema : refreshed_schema
648
656
  private :checked_schema
649
657
 
650
- # The routing table behind the schema check_queries is about to use,
651
- # when there is one: a composed supergraph dump says who resolves what,
652
- # so a validation error can name the subgraph whose code to look at. nil
653
- # for every other source — a plain schema is entirely unaffected — and
654
- # nil when a live schema class is what gets checked, since the dump then
655
- # isn't what the errors came from.
658
+ # The routing table behind the schema check_queries is about to use, when
659
+ # there is one — nil when a live schema class is what gets checked, since
660
+ # the dump then isn't what the errors came from.
656
661
  def checked_routing_table(graph)
657
- return if graph.live_schema
658
-
659
- path = graph.dump_path
660
- return unless path&.end_with?(".graphql", ".gql")
661
-
662
- sdl = File.read(path)
663
- SchemaLoader.routing_table(sdl) if SchemaLoader.federation_sdl?(sdl)
662
+ Internal::QueryCheck.routing_table_for(graph.dump_path) unless graph.live_schema
664
663
  end
665
664
  private :checked_routing_table
666
665
 
@@ -686,53 +685,6 @@ module GraphWeaver
686
685
  end
687
686
  private :refreshed_schema
688
687
 
689
- # One query's schema-validation errors as JSON-ready hashes, with the
690
- # source position graphql-ruby reports. Unparseable counts as an error
691
- # too — it doesn't validate either, and inline_fragments (which parses
692
- # first) has already branded it with its position.
693
- def validation_errors(schema, source, shared, table = nil)
694
- # path omitted: the caller keys the report by file, so branding the
695
- # message with it too would just print the path twice
696
- schema.validate(Codegen.inline_fragments(source, shared)).map do |error|
697
- detail = error.to_h
698
- location = detail["locations"]&.first || {}
699
- subgraphs = table ? attribute(table, detail["extensions"]) : []
700
- entry = {
701
- "message" => subgraphs.empty? ? error.message : "#{error.message} (#{subgraphs.join(", ")})",
702
- "line" => location["line"],
703
- "column" => location["column"],
704
- }
705
- subgraphs.empty? ? entry : entry.merge("subgraphs" => subgraphs)
706
- end
707
- rescue GraphWeaver::QueryValidationError => e
708
- # an unparseable query: codegen folds the position (and the file) into
709
- # the message, and this report keeps them separate — same splitter the
710
- # rendered error uses, so the two can't drift apart
711
- e.errors.map do |detail|
712
- _path, _position, message = QueryValidationError.split(detail)
713
- detail.transform_keys(&:to_s).merge("message" => message)
714
- end
715
- end
716
- private :validation_errors
717
-
718
- # Which subgraphs a validation error is about, on a federated schema:
719
- # "Field 'weight' doesn't exist on type 'Product'" is much less useful
720
- # than the same line plus "(products)" — whose code to look at, whose
721
- # team to talk to. graphql-ruby reports the coordinate structurally, so
722
- # this is a lookup rather than message parsing. Both halves of the
723
- # coordinate are required: an argument error reports typeName "Field"
724
- # (the AST node kind, not a type), and looking that up would attribute
725
- # confidently and wrongly.
726
- def attribute(table, extensions)
727
- return [] unless extensions
728
-
729
- type_name, field_name = extensions.values_at("typeName", "fieldName")
730
- return [] unless type_name && field_name
731
-
732
- table.responsible(type_name, field_name)
733
- end
734
- private :attribute
735
-
736
688
  # Load the generated modules — one line in an initializer or spec
737
689
  # helper (loading happens only when you call this; skip it and
738
690
  # require files yourself if you'd rather):
@@ -843,7 +795,7 @@ module GraphWeaver
843
795
 
844
796
  # (filename, source) per artifact. Types a schema shares across queries —
845
797
  # input types, schema enums, and each named shared fragment spread as a
846
- # whole-union field — are emitted once into the shared module, with query
798
+ # whole field — are emitted once into the shared module, with query
847
799
  # modules aliasing what they use. That's the difference between hundreds of
848
800
  # duplicated bool_exp structs (or one Ruby class per query for the same
849
801
  # schema enum) and one copy per schema. (Single-query parse inlines
@@ -862,7 +814,8 @@ module GraphWeaver
862
814
  @unmatched_registrations |= registry.unmatched_registrations(schema)
863
815
 
864
816
  used = { inputs: [], enums: [], mapped: [] }
865
- used_unions = []
817
+ hoisted = []
818
+ helpers = []
866
819
  shared = Codegen.load_fragments(fragments)
867
820
 
868
821
  refusals = []
@@ -878,14 +831,15 @@ module GraphWeaver
878
831
  name:,
879
832
  graph_name: graph.name,
880
833
  types_namespace: graph.types_module,
881
- hoistable_unions: Codegen.shared_fragment_spreads(source, shared, path),
834
+ hoistable_fragments: Codegen.shared_fragment_spreads(source, shared, path),
882
835
  path:,
883
836
  registry:,
884
837
  )
885
838
  out = codegen.generate
886
839
  codegen.variable_type_names.each { |kind, names| used[kind] |= names }
887
840
  found.concat(codegen.untyped_scalars).uniq!
888
- used_unions |= codegen.used_union_names
841
+ hoisted |= codegen.used_fragment_names
842
+ helpers |= codegen.block_helpers
889
843
  [filename, out]
890
844
  rescue GraphWeaver::Error => e
891
845
  # collected, not raised: nothing is written either way, and an adopter
@@ -895,24 +849,60 @@ module GraphWeaver
895
849
  end
896
850
  refuse_all!(refusals, paths.size)
897
851
 
898
- if used_unions.any? || used.values.any?(&:any?)
852
+ if hoisted.any? || used.values.any?(&:any?)
899
853
  refuse_duplicate_types!(seen, graph)
900
854
  codegen = Codegen.new(schema:, query: "", name: graph.types_module, registry:)
901
855
  types = codegen.generate_types(
902
856
  inputs: used[:inputs], enums: used[:enums] + used[:mapped],
903
- unions: used_unions, fragments: shared,
857
+ hoisted:, fragments: shared,
904
858
  )
905
859
  found.concat(codegen.untyped_scalars).uniq!
860
+ helpers |= codegen.block_helpers
906
861
  # these land in the graph's output like any other file, so they collide
907
862
  # with another graph's the same way
908
863
  types.each_key { |filename| refuse_duplicate_file!(seen, filename, graph, graph.types_module) }
909
864
  plan = types.to_a + plan
910
865
  end
911
866
 
867
+ if helpers.any?
868
+ refuse_duplicate_file!(seen, HELPERS_RBI, graph, "the extend_type blocks")
869
+ plan = [[HELPERS_RBI, helpers_rbi(helpers)]] + plan
870
+ end
871
+
912
872
  plan
913
873
  end
914
874
  private :generation_plan
915
875
 
876
+ # One rule: generation declares every constant it includes. A block-form
877
+ # extend_type mints its mixin at registration, so no source file declares
878
+ # GraphWeaver::TypeHelpers::Pet — and an app's `srb tc` failed on every
879
+ # generated include of one ("Unable to resolve constant ...").
880
+ #
881
+ # An .rbi rather than Ruby, because Ruby never loads one: the include stays
882
+ # the only thing that resolves the constant at runtime, which keeps a
883
+ # dropped registration loud (see load_generated!) instead of silently
884
+ # handing the struct an empty module.
885
+ def helpers_rbi(names)
886
+ # `module A::B` does not define A, so each outer segment is opened first
887
+ declared = names.flat_map { |name|
888
+ segments = name.split("::")
889
+ (1...segments.size).map { |i| segments.first(i + 1).join("::") }
890
+ }.uniq.sort
891
+ # assembled line by line, not from a heredoc (as Emit does): a `# typed:`
892
+ # sigil at the start of a line is the sigil srb reads for THIS file
893
+ lines = [
894
+ "# typed: strict",
895
+ "",
896
+ "#{GENERATED_HEADER} #{VERSION} — do not edit. The modules this graph's",
897
+ "# extend_type blocks mint, declared so `srb tc` can resolve the includes",
898
+ "# in the generated code. Ruby never loads an .rbi; the registrations do",
899
+ "# the real work.",
900
+ "",
901
+ ]
902
+ (lines + declared.map { |mod| "module #{mod}; end" }).join("\n") + "\n"
903
+ end
904
+ private :helpers_rbi
905
+
916
906
  # Every query that refused, in one error. One refusal is re-raised as
917
907
  # itself, so a single bad file reads exactly as it always has — class,
918
908
  # message and all; several become one list, because clearing them a file
@@ -1054,8 +1044,24 @@ module GraphWeaver
1054
1044
  # strict), requires: names files the generated code should require.
1055
1045
  # Generation fails naming any schema value that doesn't resolve —
1056
1046
  # exhaustiveness checked ahead of runtime.
1057
- def register_enum(graphql_name, type, positional_map = nil, map: nil, fallback: nil, requires: nil)
1058
- Codegen.register_enum(graphql_name, type, positional_map, map:, fallback:, requires:)
1047
+ #
1048
+ # alias: says two of the schema's wire values are one value — both cast,
1049
+ # the target is what goes back on the wire. It is the whole registration
1050
+ # when there is no enum of your own to map onto, which is what a schema
1051
+ # mid-rename needs:
1052
+ #
1053
+ # GraphWeaver.register_enum("Status", alias: { "legacy_mode" => "LEGACY_MODE" })
1054
+ #
1055
+ # fallback: true is the type-less form of forward-compat: the generated
1056
+ # enum gains an Other member and casts every value the schema doesn't
1057
+ # declare to it, so a server adding one doesn't take the client down.
1058
+ #
1059
+ # GraphWeaver.register_enum("Species", fallback: true)
1060
+ def register_enum(graphql_name, type = nil, positional_map = nil, map: nil, fallback: nil, requires: nil,
1061
+ alias: nil)
1062
+ # `alias` is a Ruby keyword, so the parameter is only readable through binding
1063
+ Codegen.register_enum(graphql_name, type, positional_map, map:, fallback:, requires:,
1064
+ alias: binding.local_variable_get(:alias))
1059
1065
  end
1060
1066
 
1061
1067
  # Include app-owned helper modules into every struct generated from a
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.3
4
+ version: 0.7.5
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.3/CHANGELOG.md
367
+ changelog_uri: https://github.com/dpep/graph_weaver/blob/v0.7.5/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