graph_weaver 0.7.2 → 0.7.4
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.
- checksums.yaml +4 -4
- data/Gemfile.lock +19 -19
- data/docs/federation.md +3 -2
- data/docs/generated_modules.md +52 -10
- data/docs/getting_started.md +18 -2
- data/docs/i18n.md +4 -4
- data/docs/scalars.md +123 -32
- data/docs/testing.md +21 -2
- data/docs/upgrading.md +31 -5
- data/examples/github/generated/star_mutation.rb +24 -2
- data/examples/github/generated/stargazers_query.rb +61 -5
- data/examples/github/generated/starred_query.rb +33 -3
- data/lib/graph_weaver/client.rb +23 -0
- data/lib/graph_weaver/codegen/emit.rb +22 -7
- data/lib/graph_weaver/codegen/enum_type.rb +132 -19
- data/lib/graph_weaver/codegen/nodes.rb +20 -9
- data/lib/graph_weaver/codegen/scalar_type.rb +72 -18
- data/lib/graph_weaver/codegen/type_helpers.rb +71 -13
- data/lib/graph_weaver/codegen.rb +61 -16
- data/lib/graph_weaver/coerce.rb +24 -5
- data/lib/graph_weaver/graph.rb +4 -1
- data/lib/graph_weaver/hints.rb +4 -1
- data/lib/graph_weaver/input_struct.rb +13 -7
- data/lib/graph_weaver/internal/values.rb +5 -2
- data/lib/graph_weaver/internal.rb +67 -0
- data/lib/graph_weaver/railtie.rb +202 -147
- data/lib/graph_weaver/testing.rb +101 -0
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +83 -67
- metadata +2 -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
|
-
|
|
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
|
|
@@ -195,6 +199,7 @@ module GraphWeaver
|
|
|
195
199
|
|
|
196
200
|
def generated_paths=(paths)
|
|
197
201
|
@generated_paths = paths && Array(paths)
|
|
202
|
+
hide_from_autoloading(generated_paths)
|
|
198
203
|
end
|
|
199
204
|
|
|
200
205
|
def fragments_paths=(paths)
|
|
@@ -287,9 +292,24 @@ module GraphWeaver
|
|
|
287
292
|
# generate! reports in however many times the initializer has re-run
|
|
288
293
|
existing = @graphs.index { |candidate| candidate.name == name }
|
|
289
294
|
existing ? @graphs[existing] = graph : @graphs << graph
|
|
295
|
+
hide_from_autoloading([graph.output])
|
|
290
296
|
graph
|
|
291
297
|
end
|
|
292
298
|
|
|
299
|
+
# Rails only: where a graph writes is hidden from Zeitwerk the moment the
|
|
300
|
+
# graph says it. Knowing it any later needed an initializer edge waiting on
|
|
301
|
+
# config/initializers, and such an edge reorders the whole app's boot (see
|
|
302
|
+
# the Railtie). Registered first, so a refusal can name the graph.
|
|
303
|
+
def hide_from_autoloading(paths)
|
|
304
|
+
# by name, not spelled: the railtie is Rails-only and `typed: ignore`, so
|
|
305
|
+
# sorbet has no constant to resolve
|
|
306
|
+
return unless const_defined?(:Railtie, false)
|
|
307
|
+
|
|
308
|
+
railtie = const_get(:Railtie, false)
|
|
309
|
+
paths.each { |path| railtie.ignore_output!(path) }
|
|
310
|
+
end
|
|
311
|
+
private :hide_from_autoloading
|
|
312
|
+
|
|
293
313
|
# Every graph an entry point walks: the declared ones, or the single graph
|
|
294
314
|
# the top-level settings describe. Never empty.
|
|
295
315
|
def graphs = @graphs&.dup || [default_graph]
|
|
@@ -468,12 +488,13 @@ module GraphWeaver
|
|
|
468
488
|
end
|
|
469
489
|
private :orphaned
|
|
470
490
|
|
|
471
|
-
# Every
|
|
491
|
+
# Every file under output that GraphWeaver wrote, identified by the header
|
|
472
492
|
# it emits. The header — not a *_query.rb glob — is what makes pruning
|
|
473
493
|
# safe: this is a real directory, and a hand-written file in it must
|
|
474
|
-
# 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.
|
|
475
496
|
def generated_files(output)
|
|
476
|
-
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|
|
|
477
498
|
File.foreach(path).first(HEADER_SCAN_LINES).any? { |line| line.start_with?(GENERATED_HEADER) }
|
|
478
499
|
end
|
|
479
500
|
end
|
|
@@ -602,6 +623,9 @@ module GraphWeaver
|
|
|
602
623
|
# (products, reviews)", plus a "subgraphs" key — since knowing whose
|
|
603
624
|
# code to look at is half the answer. A plain schema is unaffected.
|
|
604
625
|
#
|
|
626
|
+
# One query you have as a *string* is Client#check_query — the same
|
|
627
|
+
# entries, against that client's own schema.
|
|
628
|
+
#
|
|
605
629
|
# A different question from verify_generated!, which asks whether the
|
|
606
630
|
# committed Ruby matches the committed schema. `rake
|
|
607
631
|
# graph_weaver:queries:check` prints this and exits non-zero.
|
|
@@ -612,7 +636,7 @@ module GraphWeaver
|
|
|
612
636
|
checked = checked_schema(graph)
|
|
613
637
|
table = checked_routing_table(graph)
|
|
614
638
|
Internal::Util.query_files(graph.queries).each do |path|
|
|
615
|
-
errors =
|
|
639
|
+
errors = Internal::QueryCheck.errors(checked, File.read(path), shared, table)
|
|
616
640
|
next if errors.empty?
|
|
617
641
|
|
|
618
642
|
# keyed by file, as it has always been — and two graphs may share a
|
|
@@ -631,20 +655,11 @@ module GraphWeaver
|
|
|
631
655
|
def checked_schema(graph) = graph.named_schema? ? graph.schema : refreshed_schema
|
|
632
656
|
private :checked_schema
|
|
633
657
|
|
|
634
|
-
# The routing table behind the schema check_queries is about to use,
|
|
635
|
-
#
|
|
636
|
-
#
|
|
637
|
-
# for every other source — a plain schema is entirely unaffected — and
|
|
638
|
-
# nil when a live schema class is what gets checked, since the dump then
|
|
639
|
-
# 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.
|
|
640
661
|
def checked_routing_table(graph)
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
path = graph.dump_path
|
|
644
|
-
return unless path&.end_with?(".graphql", ".gql")
|
|
645
|
-
|
|
646
|
-
sdl = File.read(path)
|
|
647
|
-
SchemaLoader.routing_table(sdl) if SchemaLoader.federation_sdl?(sdl)
|
|
662
|
+
Internal::QueryCheck.routing_table_for(graph.dump_path) unless graph.live_schema
|
|
648
663
|
end
|
|
649
664
|
private :checked_routing_table
|
|
650
665
|
|
|
@@ -670,53 +685,6 @@ module GraphWeaver
|
|
|
670
685
|
end
|
|
671
686
|
private :refreshed_schema
|
|
672
687
|
|
|
673
|
-
# One query's schema-validation errors as JSON-ready hashes, with the
|
|
674
|
-
# source position graphql-ruby reports. Unparseable counts as an error
|
|
675
|
-
# too — it doesn't validate either, and inline_fragments (which parses
|
|
676
|
-
# first) has already branded it with its position.
|
|
677
|
-
def validation_errors(schema, source, shared, table = nil)
|
|
678
|
-
# path omitted: the caller keys the report by file, so branding the
|
|
679
|
-
# message with it too would just print the path twice
|
|
680
|
-
schema.validate(Codegen.inline_fragments(source, shared)).map do |error|
|
|
681
|
-
detail = error.to_h
|
|
682
|
-
location = detail["locations"]&.first || {}
|
|
683
|
-
subgraphs = table ? attribute(table, detail["extensions"]) : []
|
|
684
|
-
entry = {
|
|
685
|
-
"message" => subgraphs.empty? ? error.message : "#{error.message} (#{subgraphs.join(", ")})",
|
|
686
|
-
"line" => location["line"],
|
|
687
|
-
"column" => location["column"],
|
|
688
|
-
}
|
|
689
|
-
subgraphs.empty? ? entry : entry.merge("subgraphs" => subgraphs)
|
|
690
|
-
end
|
|
691
|
-
rescue GraphWeaver::QueryValidationError => e
|
|
692
|
-
# an unparseable query: codegen folds the position (and the file) into
|
|
693
|
-
# the message, and this report keeps them separate — same splitter the
|
|
694
|
-
# rendered error uses, so the two can't drift apart
|
|
695
|
-
e.errors.map do |detail|
|
|
696
|
-
_path, _position, message = QueryValidationError.split(detail)
|
|
697
|
-
detail.transform_keys(&:to_s).merge("message" => message)
|
|
698
|
-
end
|
|
699
|
-
end
|
|
700
|
-
private :validation_errors
|
|
701
|
-
|
|
702
|
-
# Which subgraphs a validation error is about, on a federated schema:
|
|
703
|
-
# "Field 'weight' doesn't exist on type 'Product'" is much less useful
|
|
704
|
-
# than the same line plus "(products)" — whose code to look at, whose
|
|
705
|
-
# team to talk to. graphql-ruby reports the coordinate structurally, so
|
|
706
|
-
# this is a lookup rather than message parsing. Both halves of the
|
|
707
|
-
# coordinate are required: an argument error reports typeName "Field"
|
|
708
|
-
# (the AST node kind, not a type), and looking that up would attribute
|
|
709
|
-
# confidently and wrongly.
|
|
710
|
-
def attribute(table, extensions)
|
|
711
|
-
return [] unless extensions
|
|
712
|
-
|
|
713
|
-
type_name, field_name = extensions.values_at("typeName", "fieldName")
|
|
714
|
-
return [] unless type_name && field_name
|
|
715
|
-
|
|
716
|
-
table.responsible(type_name, field_name)
|
|
717
|
-
end
|
|
718
|
-
private :attribute
|
|
719
|
-
|
|
720
688
|
# Load the generated modules — one line in an initializer or spec
|
|
721
689
|
# helper (loading happens only when you call this; skip it and
|
|
722
690
|
# require files yourself if you'd rather):
|
|
@@ -847,6 +815,7 @@ module GraphWeaver
|
|
|
847
815
|
|
|
848
816
|
used = { inputs: [], enums: [], mapped: [] }
|
|
849
817
|
used_unions = []
|
|
818
|
+
helpers = []
|
|
850
819
|
shared = Codegen.load_fragments(fragments)
|
|
851
820
|
|
|
852
821
|
refusals = []
|
|
@@ -870,6 +839,7 @@ module GraphWeaver
|
|
|
870
839
|
codegen.variable_type_names.each { |kind, names| used[kind] |= names }
|
|
871
840
|
found.concat(codegen.untyped_scalars).uniq!
|
|
872
841
|
used_unions |= codegen.used_union_names
|
|
842
|
+
helpers |= codegen.block_helpers
|
|
873
843
|
[filename, out]
|
|
874
844
|
rescue GraphWeaver::Error => e
|
|
875
845
|
# collected, not raised: nothing is written either way, and an adopter
|
|
@@ -887,16 +857,52 @@ module GraphWeaver
|
|
|
887
857
|
unions: used_unions, fragments: shared,
|
|
888
858
|
)
|
|
889
859
|
found.concat(codegen.untyped_scalars).uniq!
|
|
860
|
+
helpers |= codegen.block_helpers
|
|
890
861
|
# these land in the graph's output like any other file, so they collide
|
|
891
862
|
# with another graph's the same way
|
|
892
863
|
types.each_key { |filename| refuse_duplicate_file!(seen, filename, graph, graph.types_module) }
|
|
893
864
|
plan = types.to_a + plan
|
|
894
865
|
end
|
|
895
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
|
+
|
|
896
872
|
plan
|
|
897
873
|
end
|
|
898
874
|
private :generation_plan
|
|
899
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
|
+
|
|
900
906
|
# Every query that refused, in one error. One refusal is re-raised as
|
|
901
907
|
# itself, so a single bad file reads exactly as it always has — class,
|
|
902
908
|
# message and all; several become one list, because clearing them a file
|
|
@@ -1038,8 +1044,18 @@ module GraphWeaver
|
|
|
1038
1044
|
# strict), requires: names files the generated code should require.
|
|
1039
1045
|
# Generation fails naming any schema value that doesn't resolve —
|
|
1040
1046
|
# exhaustiveness checked ahead of runtime.
|
|
1041
|
-
|
|
1042
|
-
|
|
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
|
+
def register_enum(graphql_name, type = nil, positional_map = nil, map: nil, fallback: nil, requires: nil,
|
|
1055
|
+
alias: nil)
|
|
1056
|
+
# `alias` is a Ruby keyword, so the parameter is only readable through binding
|
|
1057
|
+
Codegen.register_enum(graphql_name, type, positional_map, map:, fallback:, requires:,
|
|
1058
|
+
alias: binding.local_variable_get(:alias))
|
|
1043
1059
|
end
|
|
1044
1060
|
|
|
1045
1061
|
# 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.
|
|
4
|
+
version: 0.7.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Pepper
|
|
@@ -363,7 +363,7 @@ licenses:
|
|
|
363
363
|
- MIT
|
|
364
364
|
metadata:
|
|
365
365
|
bug_tracker_uri: https://github.com/dpep/graph_weaver/issues
|
|
366
|
-
changelog_uri: https://github.com/dpep/graph_weaver/blob/v0.7.
|
|
366
|
+
changelog_uri: https://github.com/dpep/graph_weaver/blob/v0.7.4/CHANGELOG.md
|
|
367
367
|
documentation_uri: https://github.com/dpep/graph_weaver/tree/main/docs
|
|
368
368
|
rubygems_mfa_required: 'true'
|
|
369
369
|
source_code_uri: https://github.com/dpep/graph_weaver
|