graph_weaver 0.1.0 → 0.2.0
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/.yardopts +6 -0
- data/CHANGELOG.md +213 -0
- data/Gemfile.lock +32 -2
- data/Makefile +7 -2
- data/NOTES.md +1 -1
- data/PLAN.md +34 -1
- data/README.md +56 -41
- data/docs/cassettes.md +76 -0
- data/docs/errors.md +37 -9
- data/docs/generated_modules.md +139 -29
- data/docs/getting_started.md +134 -0
- data/docs/logging.md +33 -0
- data/docs/real_world.md +27 -20
- data/docs/scalars.md +122 -8
- data/docs/testing.md +44 -34
- data/docs/transports.md +124 -0
- data/graph_weaver.gemspec +7 -1
- data/lib/graph_weaver/client.rb +200 -0
- data/lib/graph_weaver/codegen/emit.rb +88 -39
- data/lib/graph_weaver/codegen/enum_type.rb +149 -0
- data/lib/graph_weaver/codegen/nodes.rb +109 -9
- data/lib/graph_weaver/codegen/scalar_type.rb +53 -35
- data/lib/graph_weaver/codegen.rb +301 -67
- data/lib/graph_weaver/errors.rb +37 -25
- data/lib/graph_weaver/hints.rb +63 -0
- data/lib/graph_weaver/inflect.rb +2 -1
- data/lib/graph_weaver/logging.rb +41 -0
- data/lib/graph_weaver/railtie.rb +29 -0
- data/lib/graph_weaver/retry.rb +97 -0
- data/lib/graph_weaver/rspec.rb +19 -14
- data/lib/graph_weaver/schema_loader.rb +156 -20
- data/lib/graph_weaver/selection.rb +3 -3
- data/lib/graph_weaver/tasks.rb +71 -0
- data/lib/graph_weaver/testing/cassette.rb +42 -21
- data/lib/graph_weaver/testing/failure.rb +22 -22
- data/lib/graph_weaver/testing/{fake_executor.rb → fake_client.rb} +13 -13
- data/lib/graph_weaver/testing/values.rb +2 -2
- data/lib/graph_weaver/testing.rb +32 -16
- data/lib/graph_weaver/transport/faraday.rb +56 -0
- data/lib/graph_weaver/transport/http.rb +87 -0
- data/lib/graph_weaver/transport.rb +120 -0
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +208 -38
- metadata +73 -5
- data/lib/graph_weaver/faraday_executor.rb +0 -61
- data/lib/graph_weaver/http_executor.rb +0 -44
- data/lib/graph_weaver/testing/rspec.rb +0 -5
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "sorbet-runtime"
|
|
6
|
+
|
|
7
|
+
require_relative "errors"
|
|
8
|
+
|
|
9
|
+
# Base class for the bundled network transports — Transport::HTTP
|
|
10
|
+
# (zero-dependency net/http, loaded by default) and Transport::Faraday
|
|
11
|
+
# (opt-in). A transport speaks GraphQL-over-HTTP and satisfies the
|
|
12
|
+
# same execute(query, variables:) => {"data" => ..., "errors" => ...}
|
|
13
|
+
# contract as a schema class or a fake — anything in a client slot.
|
|
14
|
+
#
|
|
15
|
+
# The base class owns the shared flow — encode the request, reclassify
|
|
16
|
+
# network-level failures as TransportError, raise ServerError on a
|
|
17
|
+
# non-2xx status, parse the body — so a subclass only implements post:
|
|
18
|
+
# take the request body, return [status, body].
|
|
19
|
+
class GraphWeaver::Transport
|
|
20
|
+
extend T::Sig
|
|
21
|
+
extend T::Helpers
|
|
22
|
+
abstract!
|
|
23
|
+
|
|
24
|
+
# the endpoint this transport talks to — recorded into cached schema
|
|
25
|
+
# dumps as provenance (see SchemaLoader.introspect)
|
|
26
|
+
attr_reader :url
|
|
27
|
+
|
|
28
|
+
def execute(query, variables: {})
|
|
29
|
+
# tag pairs this request's log lines (threads interleave), and names
|
|
30
|
+
# the operation so the log says WHICH query, not just the url
|
|
31
|
+
tag = GraphWeaver.logger && GraphWeaver::Transport.log_tag(query)
|
|
32
|
+
|
|
33
|
+
# full query + variables at debug only — they can carry PII
|
|
34
|
+
GraphWeaver.log(:debug) do
|
|
35
|
+
"POST #{url} #{tag} variables=#{JSON.generate(variables)}\n#{GraphWeaver::Transport.truncate_for_log(query)}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
encoded = begin
|
|
39
|
+
JSON.generate(query:, variables:)
|
|
40
|
+
rescue JSON::GeneratorError => e
|
|
41
|
+
# a value with no JSON form (NaN, Infinity, binary) — the caller's
|
|
42
|
+
# bug, surfaced under the umbrella instead of a raw JSON:: error
|
|
43
|
+
raise GraphWeaver::Error, "variables are not JSON-serializable: #{e.message}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
status, body = begin
|
|
47
|
+
GraphWeaver.log_timed(:debug, "POST #{url} #{tag} completed") do
|
|
48
|
+
post(encoded)
|
|
49
|
+
end
|
|
50
|
+
rescue *GraphWeaver.transport_errors.to_a => e
|
|
51
|
+
# never got a response — DNS, connection refused/reset, TLS, timeout
|
|
52
|
+
raise GraphWeaver::TransportError, "#{e.class}: #{e.message}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
GraphWeaver.log(:debug) { "HTTP #{status} #{tag} from #{url} (#{body.to_s.bytesize} bytes)" }
|
|
56
|
+
|
|
57
|
+
parsed = parse_body(body)
|
|
58
|
+
|
|
59
|
+
# reached the server, but it returned a non-2xx status. Per
|
|
60
|
+
# graphql-over-http, routers (Apollo Server/Router) send request
|
|
61
|
+
# errors as 4xx WITH a GraphQL errors body — those flow into the
|
|
62
|
+
# envelope so QueryError machinery sees the structured errors; only
|
|
63
|
+
# a body that isn't GraphQL (proxy pages, HTML 500s) is a ServerError.
|
|
64
|
+
unless (200..299).cover?(status)
|
|
65
|
+
return parsed if parsed.is_a?(Hash) && parsed.key?("errors")
|
|
66
|
+
|
|
67
|
+
raise GraphWeaver::ServerError.new(status:, body: body.to_s)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
unless parsed
|
|
71
|
+
# a 200 that isn't GraphQL — an HTML error page from a proxy, a
|
|
72
|
+
# captive portal: the server misbehaved, classify it that way
|
|
73
|
+
raise GraphWeaver::ServerError.new(status:, body: "non-JSON response: #{body.to_s[0, 500]}")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
parsed
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# the parsed body, or nil when it isn't JSON (a caller's connection may
|
|
80
|
+
# already parse via middleware — pass that through)
|
|
81
|
+
private def parse_body(body)
|
|
82
|
+
return body unless body.is_a?(String)
|
|
83
|
+
|
|
84
|
+
JSON.parse(body)
|
|
85
|
+
rescue JSON::ParserError
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# never leak Authorization headers through logs/exceptions — a
|
|
90
|
+
# transport inspects as its class + endpoint, nothing more
|
|
91
|
+
def inspect
|
|
92
|
+
"#<#{self.class.name} url=#{url.inspect}>"
|
|
93
|
+
end
|
|
94
|
+
alias to_s inspect
|
|
95
|
+
|
|
96
|
+
# "[req 3 FilteredPokemon]" — a per-process request id plus the
|
|
97
|
+
# operation name (when the document declares one)
|
|
98
|
+
REQUEST_MUTEX = Mutex.new
|
|
99
|
+
|
|
100
|
+
def self.log_tag(query)
|
|
101
|
+
id = REQUEST_MUTEX.synchronize { @request_count = (@request_count || 0) + 1 }
|
|
102
|
+
name = query[/\A\s*(?:query|mutation|subscription)\s+([A-Za-z_]\w*)/, 1]
|
|
103
|
+
"[req #{id}#{" #{name}" if name}]"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# keep debug readable: a 100-line introspection query would drown the
|
|
107
|
+
# log — the INFO introspection line already carries the timing
|
|
108
|
+
LOG_QUERY_LIMIT = 600
|
|
109
|
+
def self.truncate_for_log(query)
|
|
110
|
+
return query if query.length <= LOG_QUERY_LIMIT
|
|
111
|
+
|
|
112
|
+
"#{query[0, LOG_QUERY_LIMIT]}... (truncated, #{query.bytesize} bytes total)"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
# POST the JSON body to the endpoint; return [status code, raw body].
|
|
118
|
+
sig { abstract.params(body: String).returns([Integer, T.untyped]) }
|
|
119
|
+
def post(body); end
|
|
120
|
+
end
|
data/lib/graph_weaver/version.rb
CHANGED
data/lib/graph_weaver.rb
CHANGED
|
@@ -1,35 +1,167 @@
|
|
|
1
1
|
require "graphql"
|
|
2
2
|
require "sorbet-runtime"
|
|
3
3
|
|
|
4
|
+
require_relative "graph_weaver/logging"
|
|
4
5
|
require_relative "graph_weaver/errors"
|
|
6
|
+
require_relative "graph_weaver/hints"
|
|
5
7
|
require_relative "graph_weaver/response"
|
|
6
8
|
require_relative "graph_weaver/inflect"
|
|
7
9
|
require_relative "graph_weaver/codegen"
|
|
8
|
-
require_relative "graph_weaver/
|
|
10
|
+
require_relative "graph_weaver/client"
|
|
11
|
+
require_relative "graph_weaver/transport/http"
|
|
12
|
+
require_relative "graph_weaver/retry"
|
|
9
13
|
require_relative "graph_weaver/schema_loader"
|
|
10
14
|
require_relative "graph_weaver/version"
|
|
15
|
+
require_relative "graph_weaver/railtie" if defined?(::Rails::Railtie)
|
|
11
16
|
|
|
12
17
|
# opt-in extras:
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
18
|
+
# require "graph_weaver/transport/faraday" # Faraday transport
|
|
19
|
+
# require "graph_weaver/directive_defaults_patch" # fix graphql-ruby
|
|
20
|
+
# dropping directive argument defaults when loading SDL (needed for
|
|
21
|
+
# Apollo supergraph SDL until rmosolgo/graphql-ruby#5659 ships)
|
|
17
22
|
module GraphWeaver
|
|
18
23
|
class << self
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
|
|
24
|
+
# A client for one GraphQL server — transport, schema, and scoped
|
|
25
|
+
# scalars in one object (see Client):
|
|
26
|
+
#
|
|
27
|
+
# github = GraphWeaver.new("https://api.github.com/graphql", auth: token, cache: true)
|
|
28
|
+
# RepoQuery = github.parse("queries/repo.graphql")
|
|
29
|
+
#
|
|
30
|
+
# The first argument is a url or any schema source (a live schema
|
|
31
|
+
# class, or a path/SDL/introspection dump).
|
|
32
|
+
def new(source, **options, &middleware)
|
|
33
|
+
Client.new(source, **options, &middleware)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The app's default client — how generated modules find their server:
|
|
37
|
+
#
|
|
38
|
+
# GraphWeaver.client = GraphWeaver.new(url, auth: token)
|
|
39
|
+
#
|
|
40
|
+
# Accepts a Client or anything satisfying the execute contract (a
|
|
41
|
+
# schema class, a fake — testing's auto_fake swaps one in per
|
|
42
|
+
# example). Generated modules resolve per call -> per module
|
|
43
|
+
# (MyQuery.client=) -> baked constant -> here.
|
|
44
|
+
attr_accessor :client
|
|
45
|
+
|
|
46
|
+
# the default client, when one is required
|
|
47
|
+
def client!
|
|
48
|
+
@client or raise Error, "no client configured — set GraphWeaver.client= or pass a client"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The transport behind a client-or-transport value: a Client resolves
|
|
52
|
+
# to its own transport, anything else already speaks execute.
|
|
53
|
+
# Generated modules call this on every execute, so any slot in the
|
|
54
|
+
# resolution chain can hold either kind.
|
|
55
|
+
def resolve_transport(target)
|
|
56
|
+
target.is_a?(Client) ? target.transport! : target
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Conventional locations, factory_bot-style: used as defaults by
|
|
60
|
+
# generate!, verify_generated!, load_generated!, and the rake tasks;
|
|
61
|
+
# override the accessors or pass paths.
|
|
62
|
+
attr_writer :queries_path, :generated_path, :schema_path
|
|
63
|
+
|
|
64
|
+
def queries_path = @queries_path || "app/graphql/queries"
|
|
65
|
+
def generated_path = @generated_path || "app/graphql/generated"
|
|
66
|
+
def schema_path = @schema_path || "app/graphql/schema.json"
|
|
23
67
|
|
|
24
|
-
|
|
25
|
-
|
|
68
|
+
# Generate every .graphql query in a directory into checked-in Ruby
|
|
69
|
+
# files. Paths default to the conventions above; schema: defaults to
|
|
70
|
+
# the dump at schema_path (any supported extension):
|
|
71
|
+
#
|
|
72
|
+
# GraphWeaver.generate! # queries_path -> generated_path
|
|
73
|
+
#
|
|
74
|
+
# person.graphql => person_query.rb defining PersonQuery. Returns the
|
|
75
|
+
# written paths. Pair with a freshness spec (docs/generated_modules.md).
|
|
76
|
+
def generate!(schema: nil, queries: queries_path, output: generated_path, client: nil)
|
|
77
|
+
schema ||= locate_schema!
|
|
78
|
+
FileUtils.mkdir_p(output)
|
|
79
|
+
|
|
80
|
+
each_query(queries, schema:, client:).map do |base, source|
|
|
81
|
+
target = File.join(output, "#{base}_query.rb")
|
|
82
|
+
File.write(target, source)
|
|
83
|
+
log(:info) { "generated #{target}" }
|
|
84
|
+
target
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The freshness guard: raise unless every generated file matches what
|
|
89
|
+
# the current schema + queries + scalar registrations would produce.
|
|
90
|
+
# One line in a spec, or `rake graph_weaver:verify` in CI:
|
|
91
|
+
#
|
|
92
|
+
# it "generated queries are current" do
|
|
93
|
+
# GraphWeaver.verify_generated!
|
|
94
|
+
# end
|
|
95
|
+
def verify_generated!(schema: nil, queries: queries_path, output: generated_path, client: nil)
|
|
96
|
+
schema ||= locate_schema!
|
|
97
|
+
stale = each_query(queries, schema:, client:).filter_map do |base, source|
|
|
98
|
+
target = File.join(output, "#{base}_query.rb")
|
|
99
|
+
target unless File.exist?(target) && File.read(target) == source
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
unless stale.empty?
|
|
103
|
+
raise Error, "stale generated queries — regenerate (rake graph_weaver:generate): #{stale.join(", ")}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
true
|
|
26
107
|
end
|
|
27
108
|
|
|
109
|
+
# Load the generated modules — one line in an initializer or spec
|
|
110
|
+
# helper (loading happens only when you call this; skip it and
|
|
111
|
+
# require files yourself if you'd rather):
|
|
112
|
+
#
|
|
113
|
+
# GraphWeaver.load_generated!
|
|
114
|
+
#
|
|
115
|
+
# In Rails, prefer this over autoloading: Zeitwerk would expect
|
|
116
|
+
# Generated::PersonQuery from generated/person_query.rb, and
|
|
117
|
+
# generated code only changes on regeneration anyway (restart, like
|
|
118
|
+
# a schema migration).
|
|
119
|
+
def load_generated!(path = generated_path)
|
|
120
|
+
files = Dir[File.join(path, "**/*.rb")].sort
|
|
121
|
+
files.each { |file| require File.expand_path(file) }
|
|
122
|
+
log(:info) { "loaded #{files.size} generated module(s) from #{path}" }
|
|
123
|
+
files
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# the conventional schema dump, required
|
|
127
|
+
def locate_schema!
|
|
128
|
+
SchemaLoader.locate or raise Error,
|
|
129
|
+
"no schema dump at #{schema_path} (.json/.graphql/.gql) — pass schema:, or cache one: GraphWeaver.new(url, cache: true).schema"
|
|
130
|
+
end
|
|
131
|
+
private :locate_schema!
|
|
132
|
+
|
|
133
|
+
# (base, generated_source) per .graphql file in a directory
|
|
134
|
+
def each_query(queries, schema:, client:)
|
|
135
|
+
Dir[File.join(queries, "*.graphql")].sort.map do |path|
|
|
136
|
+
base = File.basename(path, ".graphql")
|
|
137
|
+
source = Codegen.generate(
|
|
138
|
+
schema:,
|
|
139
|
+
query: File.read(path),
|
|
140
|
+
module_name: "#{Inflect.camelize(base)}Query",
|
|
141
|
+
client:,
|
|
142
|
+
)
|
|
143
|
+
[base, source]
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
private :each_query
|
|
147
|
+
|
|
148
|
+
# Default input coercion for scalars that don't say coerce: themselves,
|
|
149
|
+
# resolved lazily at generation time (so set it any time before you
|
|
150
|
+
# generate — no reset_scalars! ordering dance):
|
|
151
|
+
#
|
|
152
|
+
# GraphWeaver.auto_coerce = true
|
|
153
|
+
#
|
|
154
|
+
# Convertible built-ins take their conversion (Int accepts 5/"5"),
|
|
155
|
+
# and any scalar with a full cast/serialize pair (Date, your Money)
|
|
156
|
+
# accepts its raw wire form. An explicit coerce: true/false/Symbol on
|
|
157
|
+
# a registration always wins.
|
|
158
|
+
attr_accessor :auto_coerce
|
|
159
|
+
|
|
28
160
|
# Teach the generator how a GraphQL custom scalar deserializes into a
|
|
29
161
|
# rich Ruby object (and serializes back onto the wire when used as a
|
|
30
162
|
# variable):
|
|
31
163
|
#
|
|
32
|
-
#
|
|
164
|
+
# GraphWeaver.register_scalar("Money", Money, requires: "bigdecimal")
|
|
33
165
|
#
|
|
34
166
|
# A field typed `Money` then generates `const :price, T.nilable(Money)`
|
|
35
167
|
# and casts with `Money.parse(...)` in from_h. Pass a real class as
|
|
@@ -44,16 +176,54 @@ module GraphWeaver
|
|
|
44
176
|
# serializing — it raises on bad input, so some safety survives. Built-in
|
|
45
177
|
# scalars are pre-registered the same way, so this also overrides them.
|
|
46
178
|
# Call before generating.
|
|
47
|
-
def register_scalar(graphql_name, type
|
|
48
|
-
Codegen.register_scalar(graphql_name, type
|
|
179
|
+
def register_scalar(graphql_name, type, cast: nil, serialize: nil, requires: nil, coerce: nil)
|
|
180
|
+
Codegen.register_scalar(graphql_name, type, cast:, serialize:, requires:, coerce:)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Map a GraphQL enum onto an app-owned T::Enum, so generated code
|
|
184
|
+
# speaks YOUR enum — casting wire values in, serializing members out:
|
|
185
|
+
#
|
|
186
|
+
# GraphWeaver.register_enum("Species", PetKind)
|
|
187
|
+
#
|
|
188
|
+
# The mapping is inferred by name ("CAT" <-> PetKind::Cat); map: pins
|
|
189
|
+
# renames, fallback: absorbs unknown wire values on cast (inputs stay
|
|
190
|
+
# strict), requires: names files the generated code should require.
|
|
191
|
+
# Generation fails naming any schema value that doesn't resolve —
|
|
192
|
+
# exhaustiveness checked ahead of runtime. Global; client.register_enum
|
|
193
|
+
# scopes to one client.
|
|
194
|
+
def register_enum(graphql_name, type, map: nil, fallback: nil, requires: nil)
|
|
195
|
+
Codegen.register_enum(graphql_name, type, map:, fallback:, requires:)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Bulk, inference-only form: register_enums("Species" => PetKind, ...)
|
|
199
|
+
def register_enums(mappings)
|
|
200
|
+
Codegen.register_enums(mappings)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Include app-owned helper modules into every struct generated from a
|
|
204
|
+
# GraphQL type — derived values live as methods next to the honest
|
|
205
|
+
# wire data, and srb tc checks them against each query's selection:
|
|
206
|
+
#
|
|
207
|
+
# GraphWeaver.register_type("Pet", PetHelpers)
|
|
208
|
+
#
|
|
209
|
+
# Or build the mixin inline with a block (module_eval'd into an
|
|
210
|
+
# auto-named module — quick, but invisible to srb tc):
|
|
211
|
+
#
|
|
212
|
+
# GraphWeaver.register_type("Pet") do
|
|
213
|
+
# def display_name = "#{name} the pet"
|
|
214
|
+
# end
|
|
215
|
+
#
|
|
216
|
+
# Additive (repeated and client-scoped registrations stack). Global;
|
|
217
|
+
# client.register_type scopes to one client.
|
|
218
|
+
def register_type(graphql_name, *mixins, requires: nil, &block)
|
|
219
|
+
Codegen.register_type(graphql_name, *mixins, requires:, &block)
|
|
49
220
|
end
|
|
50
221
|
|
|
51
222
|
# Restore the built-in scalars, dropping every custom registration —
|
|
52
|
-
# the clean slate to reach for between tests or to undo overrides.
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
Codegen.reset_scalars!(coerce:)
|
|
223
|
+
# the clean slate to reach for between tests or to undo overrides.
|
|
224
|
+
# (Coercible built-ins are auto_coerce's job, not a reset flavor.)
|
|
225
|
+
def reset_scalars!
|
|
226
|
+
Codegen.reset_scalars!
|
|
57
227
|
end
|
|
58
228
|
|
|
59
229
|
# Empty the scalar registry entirely, built-ins included (see
|
|
@@ -64,42 +234,42 @@ module GraphWeaver
|
|
|
64
234
|
|
|
65
235
|
# Parse a query into a typed query module:
|
|
66
236
|
#
|
|
67
|
-
#
|
|
237
|
+
# PersonQuery = GraphWeaver.parse(schema:, query: "queries/person.graphql")
|
|
68
238
|
#
|
|
69
239
|
# query is a .graphql/.gql path (module name derived from the file
|
|
70
240
|
# name) or a raw query string (name derived from the operation name,
|
|
71
241
|
# falling back to "Query" for anonymous operations — collisions are
|
|
72
242
|
# impossible since each parse gets its own container). Pass name: to
|
|
73
|
-
# override,
|
|
74
|
-
def parse(schema:, query:, name: nil,
|
|
243
|
+
# override, client: to bake the module's default client/transport.
|
|
244
|
+
def parse(schema:, query:, name: nil, client: nil, scalars: nil, enums: nil, types: nil)
|
|
75
245
|
if query.end_with?(".graphql", ".gql")
|
|
76
246
|
name ||= "#{Inflect.camelize(File.basename(query, ".*"))}Query"
|
|
77
247
|
query = File.read(query)
|
|
78
248
|
end
|
|
79
249
|
|
|
80
|
-
Codegen.parse(schema:, query:, module_name: name,
|
|
250
|
+
Codegen.parse(schema:, query:, module_name: name, client:, scalars:, enums:, types:)
|
|
81
251
|
end
|
|
82
252
|
|
|
83
|
-
# One-shot dynamic execution —
|
|
253
|
+
# One-shot dynamic execution — a throwaway client, no build step:
|
|
84
254
|
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
255
|
+
# GraphWeaver.execute(schema, "query($id: ID!) { ... }", id: "1") # => Response
|
|
256
|
+
# GraphWeaver.execute!(url, "query { viewer { login } }") # => Result (or raise)
|
|
87
257
|
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
258
|
+
# The first argument is a url or schema source, exactly as
|
|
259
|
+
# GraphWeaver.new; this is Client#execute on a client you don't keep.
|
|
260
|
+
# (A url source introspects the schema on every call — keep a client
|
|
261
|
+
# for more than one query.) Variables are plain kwargs, as on a
|
|
262
|
+
# generated module (nothing reserved). execute returns the
|
|
263
|
+
# Response envelope, execute! the typed result, raising QueryError on
|
|
264
|
+
# top-level errors.
|
|
265
|
+
def execute(source, query, **variables)
|
|
266
|
+
client = source.is_a?(Client) ? source : Client.new(source)
|
|
267
|
+
client.execute(query, **variables)
|
|
98
268
|
end
|
|
99
269
|
|
|
100
270
|
# execute + data! — the typed result, or a raised QueryError. See execute.
|
|
101
|
-
def execute!(
|
|
102
|
-
execute(
|
|
271
|
+
def execute!(source, query, **variables)
|
|
272
|
+
execute(source, query, **variables).data!
|
|
103
273
|
end
|
|
104
274
|
end
|
|
105
275
|
end
|
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.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Pepper
|
|
@@ -37,6 +37,20 @@ dependencies:
|
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: apollo-federation
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
40
54
|
- !ruby/object:Gem::Dependency
|
|
41
55
|
name: bigdecimal
|
|
42
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -93,6 +107,34 @@ dependencies:
|
|
|
93
107
|
- - ">="
|
|
94
108
|
- !ruby/object:Gem::Version
|
|
95
109
|
version: '0'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: rake
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0'
|
|
117
|
+
type: :development
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: redcarpet
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - ">="
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '0'
|
|
131
|
+
type: :development
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - ">="
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '0'
|
|
96
138
|
- !ruby/object:Gem::Dependency
|
|
97
139
|
name: rspec
|
|
98
140
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -163,12 +205,27 @@ dependencies:
|
|
|
163
205
|
- - ">="
|
|
164
206
|
- !ruby/object:Gem::Version
|
|
165
207
|
version: '0'
|
|
208
|
+
- !ruby/object:Gem::Dependency
|
|
209
|
+
name: yard
|
|
210
|
+
requirement: !ruby/object:Gem::Requirement
|
|
211
|
+
requirements:
|
|
212
|
+
- - ">="
|
|
213
|
+
- !ruby/object:Gem::Version
|
|
214
|
+
version: '0'
|
|
215
|
+
type: :development
|
|
216
|
+
prerelease: false
|
|
217
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
218
|
+
requirements:
|
|
219
|
+
- - ">="
|
|
220
|
+
- !ruby/object:Gem::Version
|
|
221
|
+
version: '0'
|
|
166
222
|
description: A typed GraphQL client for Ruby — generate Sorbet T::Structs from queries,
|
|
167
223
|
with federation, extensibility, and testing in mind
|
|
168
224
|
executables: []
|
|
169
225
|
extensions: []
|
|
170
226
|
extra_rdoc_files: []
|
|
171
227
|
files:
|
|
228
|
+
- ".yardopts"
|
|
172
229
|
- CHANGELOG.md
|
|
173
230
|
- Gemfile
|
|
174
231
|
- Gemfile.lock
|
|
@@ -177,32 +234,43 @@ files:
|
|
|
177
234
|
- NOTES.md
|
|
178
235
|
- PLAN.md
|
|
179
236
|
- README.md
|
|
237
|
+
- docs/cassettes.md
|
|
180
238
|
- docs/errors.md
|
|
181
239
|
- docs/generated_modules.md
|
|
240
|
+
- docs/getting_started.md
|
|
241
|
+
- docs/logging.md
|
|
182
242
|
- docs/real_world.md
|
|
183
243
|
- docs/scalars.md
|
|
184
244
|
- docs/testing.md
|
|
245
|
+
- docs/transports.md
|
|
185
246
|
- graph_weaver.gemspec
|
|
186
247
|
- lib/graph_weaver.rb
|
|
248
|
+
- lib/graph_weaver/client.rb
|
|
187
249
|
- lib/graph_weaver/codegen.rb
|
|
188
250
|
- lib/graph_weaver/codegen/emit.rb
|
|
251
|
+
- lib/graph_weaver/codegen/enum_type.rb
|
|
189
252
|
- lib/graph_weaver/codegen/nodes.rb
|
|
190
253
|
- lib/graph_weaver/codegen/scalar_type.rb
|
|
191
254
|
- lib/graph_weaver/directive_defaults_patch.rb
|
|
192
255
|
- lib/graph_weaver/errors.rb
|
|
193
|
-
- lib/graph_weaver/
|
|
194
|
-
- lib/graph_weaver/http_executor.rb
|
|
256
|
+
- lib/graph_weaver/hints.rb
|
|
195
257
|
- lib/graph_weaver/inflect.rb
|
|
258
|
+
- lib/graph_weaver/logging.rb
|
|
259
|
+
- lib/graph_weaver/railtie.rb
|
|
196
260
|
- lib/graph_weaver/response.rb
|
|
261
|
+
- lib/graph_weaver/retry.rb
|
|
197
262
|
- lib/graph_weaver/rspec.rb
|
|
198
263
|
- lib/graph_weaver/schema_loader.rb
|
|
199
264
|
- lib/graph_weaver/selection.rb
|
|
265
|
+
- lib/graph_weaver/tasks.rb
|
|
200
266
|
- lib/graph_weaver/testing.rb
|
|
201
267
|
- lib/graph_weaver/testing/cassette.rb
|
|
202
268
|
- lib/graph_weaver/testing/failure.rb
|
|
203
|
-
- lib/graph_weaver/testing/
|
|
204
|
-
- lib/graph_weaver/testing/rspec.rb
|
|
269
|
+
- lib/graph_weaver/testing/fake_client.rb
|
|
205
270
|
- lib/graph_weaver/testing/values.rb
|
|
271
|
+
- lib/graph_weaver/transport.rb
|
|
272
|
+
- lib/graph_weaver/transport/faraday.rb
|
|
273
|
+
- lib/graph_weaver/transport/http.rb
|
|
206
274
|
- lib/graph_weaver/version.rb
|
|
207
275
|
homepage: https://github.com/dpep/graph_weaver
|
|
208
276
|
licenses:
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
# typed: true
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
require "faraday"
|
|
5
|
-
require "json"
|
|
6
|
-
|
|
7
|
-
require_relative "errors"
|
|
8
|
-
|
|
9
|
-
# Faraday-backed transport. Opt-in (faraday is not a hard dependency):
|
|
10
|
-
#
|
|
11
|
-
# require "graph_weaver/faraday_executor"
|
|
12
|
-
#
|
|
13
|
-
# # simplest: build a default connection from a url
|
|
14
|
-
# GraphWeaver::FaradayExecutor.new("https://api.example.com/graphql")
|
|
15
|
-
#
|
|
16
|
-
# # customize middleware while building
|
|
17
|
-
# GraphWeaver::FaradayExecutor.new(url) do |conn|
|
|
18
|
-
# conn.request :authorization, "Bearer", -> { Tokens.fetch }
|
|
19
|
-
# conn.response :logger
|
|
20
|
-
# end
|
|
21
|
-
#
|
|
22
|
-
# # or bring a fully configured connection
|
|
23
|
-
# GraphWeaver::FaradayExecutor.new(Faraday.new(url:) { |conn| ... })
|
|
24
|
-
class GraphWeaver::FaradayExecutor
|
|
25
|
-
# Faraday's network-level failures — added to the shared, extensible
|
|
26
|
-
# transport-error set.
|
|
27
|
-
GraphWeaver.register_transport_error(
|
|
28
|
-
Faraday::ConnectionFailed, Faraday::TimeoutError, Faraday::SSLError
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
def initialize(url_or_connection, headers: {}, &block)
|
|
32
|
-
@connection = case url_or_connection
|
|
33
|
-
when Faraday::Connection
|
|
34
|
-
url_or_connection
|
|
35
|
-
else
|
|
36
|
-
# Faraday appends the default adapter when the block doesn't set one
|
|
37
|
-
Faraday.new(url: url_or_connection, headers:, &block)
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def execute(query, variables: {})
|
|
42
|
-
response = begin
|
|
43
|
-
@connection.post do |request|
|
|
44
|
-
request.headers["Content-Type"] = "application/json"
|
|
45
|
-
request.body = JSON.generate(query:, variables:)
|
|
46
|
-
end
|
|
47
|
-
rescue *GraphWeaver.transport_errors.to_a => e
|
|
48
|
-
# never got a response — connection refused/reset, TLS, timeout
|
|
49
|
-
raise GraphWeaver::TransportError, "#{e.class}: #{e.message}"
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# reached the server, but it returned a non-2xx status
|
|
53
|
-
unless response.success?
|
|
54
|
-
raise GraphWeaver::ServerError.new(status: response.status, body: response.body.to_s)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
body = response.body
|
|
58
|
-
# a caller's connection may already parse json via middleware
|
|
59
|
-
body.is_a?(String) ? JSON.parse(body) : body
|
|
60
|
-
end
|
|
61
|
-
end
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# typed: true
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
require "json"
|
|
5
|
-
require "sorbet-runtime"
|
|
6
|
-
require "net/http"
|
|
7
|
-
require "openssl"
|
|
8
|
-
require "uri"
|
|
9
|
-
|
|
10
|
-
require_relative "errors"
|
|
11
|
-
|
|
12
|
-
# Minimal HTTP transport satisfying the generated modules' executor
|
|
13
|
-
# interface: execute(query, variables:) => {"data" => ..., "errors" => ...}
|
|
14
|
-
class GraphWeaver::HttpExecutor
|
|
15
|
-
# net/http's own network-level failures (Errno/SocketError/IOError are
|
|
16
|
-
# already seeded) — added to the shared, extensible transport-error set.
|
|
17
|
-
GraphWeaver.register_transport_error(Timeout::Error, OpenSSL::SSL::SSLError)
|
|
18
|
-
|
|
19
|
-
def initialize(url, headers: {})
|
|
20
|
-
@uri = URI(url)
|
|
21
|
-
@headers = headers
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def execute(query, variables: {})
|
|
25
|
-
request = Net::HTTP::Post.new(@uri, { "Content-Type" => "application/json" }.merge(@headers))
|
|
26
|
-
request.body = JSON.generate(query:, variables:)
|
|
27
|
-
|
|
28
|
-
response = begin
|
|
29
|
-
Net::HTTP.start(@uri.hostname, @uri.port, use_ssl: @uri.scheme == "https") do |http|
|
|
30
|
-
http.request(request)
|
|
31
|
-
end
|
|
32
|
-
rescue *GraphWeaver.transport_errors.to_a => e
|
|
33
|
-
# never got a response — DNS, connection refused/reset, TLS, timeout
|
|
34
|
-
raise GraphWeaver::TransportError, "#{e.class}: #{e.message}"
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# reached the server, but it returned a non-2xx status
|
|
38
|
-
unless response.is_a?(Net::HTTPSuccess)
|
|
39
|
-
raise GraphWeaver::ServerError.new(status: response.code.to_i, body: response.body)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
JSON.parse(T.must(response.body))
|
|
43
|
-
end
|
|
44
|
-
end
|