graph_weaver 0.1.0 → 0.2.1
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 +256 -0
- data/Gemfile.lock +32 -2
- data/Makefile +7 -2
- data/NOTES.md +1 -1
- data/PLAN.md +41 -1
- data/README.md +56 -41
- data/docs/cassettes.md +76 -0
- data/docs/errors.md +37 -9
- data/docs/generated_modules.md +178 -29
- data/docs/getting_started.md +136 -0
- data/docs/logging.md +33 -0
- data/docs/real_world.md +27 -20
- data/docs/scalars.md +122 -8
- data/docs/testing.md +55 -38
- data/docs/transports.md +124 -0
- data/graph_weaver.gemspec +7 -1
- data/lib/graph_weaver/client.rb +201 -0
- data/lib/graph_weaver/codegen/emit.rb +315 -76
- data/lib/graph_weaver/codegen/enum_type.rb +149 -0
- data/lib/graph_weaver/codegen/nodes.rb +123 -68
- data/lib/graph_weaver/codegen/scalar_type.rb +53 -35
- data/lib/graph_weaver/codegen.rb +272 -101
- 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/input_struct.rb +59 -0
- data/lib/graph_weaver/logging.rb +41 -0
- data/lib/graph_weaver/railtie.rb +30 -0
- data/lib/graph_weaver/retry.rb +97 -0
- data/lib/graph_weaver/rspec.rb +17 -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 +31 -15
- 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 +289 -38
- metadata +74 -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
data/lib/graph_weaver.rb
CHANGED
|
@@ -1,35 +1,248 @@
|
|
|
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"
|
|
7
|
+
require_relative "graph_weaver/input_struct"
|
|
5
8
|
require_relative "graph_weaver/response"
|
|
6
9
|
require_relative "graph_weaver/inflect"
|
|
7
10
|
require_relative "graph_weaver/codegen"
|
|
8
|
-
require_relative "graph_weaver/
|
|
11
|
+
require_relative "graph_weaver/client"
|
|
12
|
+
require_relative "graph_weaver/transport/http"
|
|
13
|
+
require_relative "graph_weaver/retry"
|
|
9
14
|
require_relative "graph_weaver/schema_loader"
|
|
10
15
|
require_relative "graph_weaver/version"
|
|
16
|
+
require_relative "graph_weaver/railtie" if defined?(::Rails::Railtie)
|
|
11
17
|
|
|
12
18
|
# opt-in extras:
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
19
|
+
# require "graph_weaver/transport/faraday" # Faraday transport
|
|
20
|
+
# require "graph_weaver/directive_defaults_patch" # fix graphql-ruby
|
|
21
|
+
# dropping directive argument defaults when loading SDL (needed for
|
|
22
|
+
# Apollo supergraph SDL until rmosolgo/graphql-ruby#5659 ships)
|
|
17
23
|
module GraphWeaver
|
|
18
24
|
class << self
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
|
|
25
|
+
# A client for one GraphQL server — transport, schema, and scoped
|
|
26
|
+
# scalars in one object (see Client):
|
|
27
|
+
#
|
|
28
|
+
# github = GraphWeaver.new("https://api.github.com/graphql", auth: token, cache: true)
|
|
29
|
+
# RepoQuery = github.parse("queries/repo.graphql")
|
|
30
|
+
#
|
|
31
|
+
# The first argument is a url or any schema source (a live schema
|
|
32
|
+
# class, or a path/SDL/introspection dump).
|
|
33
|
+
def new(source, **options, &middleware)
|
|
34
|
+
Client.new(source, **options, &middleware)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The app's default client — how generated modules find their server:
|
|
38
|
+
#
|
|
39
|
+
# GraphWeaver.client = GraphWeaver.new(url, auth: token)
|
|
40
|
+
#
|
|
41
|
+
# Accepts a Client or anything satisfying the execute contract (a
|
|
42
|
+
# schema class, a fake — testing's auto_fake swaps one in per
|
|
43
|
+
# example). Generated modules resolve per call -> per module
|
|
44
|
+
# (MyQuery.client=) -> baked constant -> here.
|
|
45
|
+
attr_accessor :client
|
|
46
|
+
|
|
47
|
+
# the default client, when one is required
|
|
48
|
+
def client!
|
|
49
|
+
@client or raise Error, "no client configured — set GraphWeaver.client= or pass a client"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The transport behind a client-or-transport value: a Client resolves
|
|
53
|
+
# to its own transport, anything else already speaks execute.
|
|
54
|
+
# Generated modules call this on every execute, so any slot in the
|
|
55
|
+
# resolution chain can hold either kind.
|
|
56
|
+
def resolve_transport(target)
|
|
57
|
+
target.is_a?(Client) ? target.transport! : target
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Conventional locations, factory_bot-style — LISTS, so extra
|
|
61
|
+
# locations (a test-only dir, an engine's) can be appended and every
|
|
62
|
+
# loader walks them all:
|
|
63
|
+
#
|
|
64
|
+
# # e.g. in spec/support/graph_weaver.rb
|
|
65
|
+
# GraphWeaver.generated_paths << "spec/support/graphql/generated"
|
|
66
|
+
# GraphWeaver.queries_paths << "spec/support/graphql/queries"
|
|
67
|
+
#
|
|
68
|
+
# The singular accessors read the first entry (the default target
|
|
69
|
+
# for generate! and the rake tasks); assigning one replaces the list.
|
|
70
|
+
attr_writer :queries_paths, :generated_paths, :schema_path
|
|
71
|
+
|
|
72
|
+
# Entries may be glob patterns — the generated default also matches
|
|
73
|
+
# per-schema layouts (app/graphql/github/generated). Queries stay
|
|
74
|
+
# single-schema: load_queries! parses everything against one client.
|
|
75
|
+
def queries_paths = @queries_paths ||= ["app/graphql/queries"]
|
|
76
|
+
def generated_paths = @generated_paths ||= ["app/graphql/generated", "app/graphql/*/generated"]
|
|
77
|
+
|
|
78
|
+
def queries_path = queries_paths.first
|
|
79
|
+
def generated_path = generated_paths.first
|
|
80
|
+
|
|
81
|
+
def queries_path=(path)
|
|
82
|
+
@queries_paths = path.nil? ? nil : [path]
|
|
83
|
+
end
|
|
23
84
|
|
|
24
|
-
def
|
|
25
|
-
@
|
|
85
|
+
def generated_path=(path)
|
|
86
|
+
@generated_paths = path.nil? ? nil : [path]
|
|
26
87
|
end
|
|
27
88
|
|
|
89
|
+
def schema_path = @schema_path || "app/graphql/schema.json"
|
|
90
|
+
|
|
91
|
+
# The shared-inputs module name: set it globally, pass inputs_module:
|
|
92
|
+
# per generate!, or let it derive from the output path — the
|
|
93
|
+
# directory above generated/ names the schema in multi-schema
|
|
94
|
+
# layouts (app/graphql/github/generated => GithubInputs); the
|
|
95
|
+
# conventional layout (and anything unrecognizable) stays
|
|
96
|
+
# GraphQLInputs.
|
|
97
|
+
attr_writer :inputs_module
|
|
98
|
+
|
|
99
|
+
def inputs_module(output = generated_path)
|
|
100
|
+
return @inputs_module if @inputs_module
|
|
101
|
+
|
|
102
|
+
segments = File.expand_path(output.to_s).split(File::SEPARATOR)
|
|
103
|
+
segments.pop if segments.last == "generated"
|
|
104
|
+
parent = segments.last.to_s
|
|
105
|
+
if parent.match?(/\A[a-zA-Z]\w*\z/) && !%w[graphql app lib spec support test].include?(parent)
|
|
106
|
+
"#{Inflect.camelize(parent)}Inputs"
|
|
107
|
+
else
|
|
108
|
+
"GraphQLInputs"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Generate every .graphql query in a directory into checked-in Ruby
|
|
113
|
+
# files. Paths default to the conventions above; schema: defaults to
|
|
114
|
+
# the dump at schema_path (any supported extension):
|
|
115
|
+
#
|
|
116
|
+
# GraphWeaver.generate! # queries_path -> generated_path
|
|
117
|
+
#
|
|
118
|
+
# person.graphql => person_query.rb defining PersonQuery. Returns the
|
|
119
|
+
# written paths. Pair with a freshness spec (docs/generated_modules.md).
|
|
120
|
+
def generate!(schema: nil, queries: queries_path, output: generated_path, client: nil,
|
|
121
|
+
shared_inputs: true, inputs_module: nil)
|
|
122
|
+
schema ||= locate_schema!
|
|
123
|
+
inputs_module ||= self.inputs_module(output)
|
|
124
|
+
|
|
125
|
+
plan = generation_plan(queries:, schema:, client:, shared_inputs:, inputs_module:)
|
|
126
|
+
written = plan.map do |filename, source|
|
|
127
|
+
target = File.join(output, filename)
|
|
128
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
129
|
+
File.write(target, source)
|
|
130
|
+
log(:info) { "generated #{target}" }
|
|
131
|
+
target
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# a type dropped from the schema must not linger as a stale file —
|
|
135
|
+
# inputs/ is wholly generated, so pruning is safe
|
|
136
|
+
(Dir[File.join(output, "inputs", "*.rb")] - written).each do |orphan|
|
|
137
|
+
File.delete(orphan)
|
|
138
|
+
log(:info) { "pruned #{orphan}" }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
written
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# The freshness guard: raise unless every generated file matches what
|
|
145
|
+
# the current schema + queries + scalar registrations would produce.
|
|
146
|
+
# One line in a spec, or `rake graph_weaver:verify` in CI:
|
|
147
|
+
#
|
|
148
|
+
# it "generated queries are current" do
|
|
149
|
+
# GraphWeaver.verify_generated!
|
|
150
|
+
# end
|
|
151
|
+
def verify_generated!(schema: nil, queries: queries_path, output: generated_path, client: nil,
|
|
152
|
+
shared_inputs: true, inputs_module: nil)
|
|
153
|
+
schema ||= locate_schema!
|
|
154
|
+
inputs_module ||= self.inputs_module(output)
|
|
155
|
+
plan = generation_plan(queries:, schema:, client:, shared_inputs:, inputs_module:)
|
|
156
|
+
stale = plan.filter_map do |filename, source|
|
|
157
|
+
target = File.join(output, filename)
|
|
158
|
+
target unless File.exist?(target) && File.read(target) == source
|
|
159
|
+
end
|
|
160
|
+
# strays: a type file the current schema no longer produces
|
|
161
|
+
stale += Dir[File.join(output, "inputs", "*.rb")] - plan.map { |f, _| File.join(output, f) }
|
|
162
|
+
|
|
163
|
+
unless stale.empty?
|
|
164
|
+
raise Error, "stale generated queries — regenerate (rake graph_weaver:generate): #{stale.join(", ")}"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
true
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Load the generated modules — one line in an initializer or spec
|
|
171
|
+
# helper (loading happens only when you call this; skip it and
|
|
172
|
+
# require files yourself if you'd rather):
|
|
173
|
+
#
|
|
174
|
+
# GraphWeaver.load_generated!
|
|
175
|
+
#
|
|
176
|
+
# In Rails, prefer this over autoloading: Zeitwerk would expect
|
|
177
|
+
# Generated::PersonQuery from generated/person_query.rb, and
|
|
178
|
+
# generated code only changes on regeneration anyway (restart, like
|
|
179
|
+
# a schema migration).
|
|
180
|
+
def load_generated!(path = nil)
|
|
181
|
+
paths = path ? [path] : generated_paths
|
|
182
|
+
files = paths.flat_map { |dir| Dir[File.join(dir, "**/*.rb")].sort }.uniq
|
|
183
|
+
files.each { |file| require File.expand_path(file) }
|
|
184
|
+
log(:info) { "loaded #{files.size} generated module(s) from #{paths.join(", ")}" }
|
|
185
|
+
files
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# the conventional schema dump, required
|
|
189
|
+
def locate_schema!
|
|
190
|
+
SchemaLoader.locate or raise Error,
|
|
191
|
+
"no schema dump at #{schema_path} (.json/.graphql/.gql) — pass schema:, or cache one: GraphWeaver.new(url, cache: true).schema"
|
|
192
|
+
end
|
|
193
|
+
private :locate_schema!
|
|
194
|
+
|
|
195
|
+
# (filename, source) per artifact: shared_inputs (the default) emits
|
|
196
|
+
# every variable type once into inputs.rb, with query modules
|
|
197
|
+
# aliasing what they use — the difference between hundreds of
|
|
198
|
+
# duplicated bool_exp structs and one copy per schema.
|
|
199
|
+
def generation_plan(queries:, schema:, client:, shared_inputs:, inputs_module: self.inputs_module)
|
|
200
|
+
namespace = shared_inputs ? inputs_module : nil
|
|
201
|
+
used = { inputs: [], enums: [], mapped: [] }
|
|
202
|
+
|
|
203
|
+
plan = Dir[File.join(queries, "*.graphql")].sort.map do |path|
|
|
204
|
+
base = File.basename(path, ".graphql")
|
|
205
|
+
codegen = Codegen.new(
|
|
206
|
+
schema:,
|
|
207
|
+
query: File.read(path),
|
|
208
|
+
module_name: "#{Inflect.camelize(base)}Query",
|
|
209
|
+
client:,
|
|
210
|
+
inputs_namespace: namespace,
|
|
211
|
+
)
|
|
212
|
+
source = codegen.generate
|
|
213
|
+
codegen.variable_type_names.each { |kind, names| used[kind] |= names }
|
|
214
|
+
["#{base}_query.rb", source]
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
if namespace && used.values.any?(&:any?)
|
|
218
|
+
shared = Codegen.generate_inputs(
|
|
219
|
+
schema:, module_name: namespace,
|
|
220
|
+
input_types: used[:inputs], enum_types: used[:enums] + used[:mapped],
|
|
221
|
+
)
|
|
222
|
+
plan = shared.to_a + plan
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
plan
|
|
226
|
+
end
|
|
227
|
+
private :generation_plan
|
|
228
|
+
|
|
229
|
+
# Default input coercion for scalars that don't say coerce: themselves,
|
|
230
|
+
# resolved lazily at generation time (so set it any time before you
|
|
231
|
+
# generate — no reset_scalars! ordering dance):
|
|
232
|
+
#
|
|
233
|
+
# GraphWeaver.auto_coerce = true
|
|
234
|
+
#
|
|
235
|
+
# Convertible built-ins take their conversion (Int accepts 5/"5"),
|
|
236
|
+
# and any scalar with a full cast/serialize pair (Date, your Money)
|
|
237
|
+
# accepts its raw wire form. An explicit coerce: true/false/Symbol on
|
|
238
|
+
# a registration always wins.
|
|
239
|
+
attr_accessor :auto_coerce
|
|
240
|
+
|
|
28
241
|
# Teach the generator how a GraphQL custom scalar deserializes into a
|
|
29
242
|
# rich Ruby object (and serializes back onto the wire when used as a
|
|
30
243
|
# variable):
|
|
31
244
|
#
|
|
32
|
-
#
|
|
245
|
+
# GraphWeaver.register_scalar("Money", Money, requires: "bigdecimal")
|
|
33
246
|
#
|
|
34
247
|
# A field typed `Money` then generates `const :price, T.nilable(Money)`
|
|
35
248
|
# and casts with `Money.parse(...)` in from_h. Pass a real class as
|
|
@@ -44,16 +257,54 @@ module GraphWeaver
|
|
|
44
257
|
# serializing — it raises on bad input, so some safety survives. Built-in
|
|
45
258
|
# scalars are pre-registered the same way, so this also overrides them.
|
|
46
259
|
# Call before generating.
|
|
47
|
-
def register_scalar(graphql_name, type
|
|
48
|
-
Codegen.register_scalar(graphql_name, type
|
|
260
|
+
def register_scalar(graphql_name, type, cast: nil, serialize: nil, requires: nil, coerce: nil)
|
|
261
|
+
Codegen.register_scalar(graphql_name, type, cast:, serialize:, requires:, coerce:)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Map a GraphQL enum onto an app-owned T::Enum, so generated code
|
|
265
|
+
# speaks YOUR enum — casting wire values in, serializing members out:
|
|
266
|
+
#
|
|
267
|
+
# GraphWeaver.register_enum("Species", PetKind)
|
|
268
|
+
#
|
|
269
|
+
# The mapping is inferred by name ("CAT" <-> PetKind::Cat); map: pins
|
|
270
|
+
# renames, fallback: absorbs unknown wire values on cast (inputs stay
|
|
271
|
+
# strict), requires: names files the generated code should require.
|
|
272
|
+
# Generation fails naming any schema value that doesn't resolve —
|
|
273
|
+
# exhaustiveness checked ahead of runtime. Global; client.register_enum
|
|
274
|
+
# scopes to one client.
|
|
275
|
+
def register_enum(graphql_name, type, map: nil, fallback: nil, requires: nil)
|
|
276
|
+
Codegen.register_enum(graphql_name, type, map:, fallback:, requires:)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Bulk, inference-only form: register_enums("Species" => PetKind, ...)
|
|
280
|
+
def register_enums(mappings)
|
|
281
|
+
Codegen.register_enums(mappings)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Include app-owned helper modules into every struct generated from a
|
|
285
|
+
# GraphQL type — derived values live as methods next to the honest
|
|
286
|
+
# wire data, and srb tc checks them against each query's selection:
|
|
287
|
+
#
|
|
288
|
+
# GraphWeaver.register_type("Pet", PetHelpers)
|
|
289
|
+
#
|
|
290
|
+
# Or build the mixin inline with a block (module_eval'd into an
|
|
291
|
+
# auto-named module — quick, but invisible to srb tc):
|
|
292
|
+
#
|
|
293
|
+
# GraphWeaver.register_type("Pet") do
|
|
294
|
+
# def display_name = "#{name} the pet"
|
|
295
|
+
# end
|
|
296
|
+
#
|
|
297
|
+
# Additive (repeated and client-scoped registrations stack). Global;
|
|
298
|
+
# client.register_type scopes to one client.
|
|
299
|
+
def register_type(graphql_name, *mixins, requires: nil, &block)
|
|
300
|
+
Codegen.register_type(graphql_name, *mixins, requires:, &block)
|
|
49
301
|
end
|
|
50
302
|
|
|
51
303
|
# 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:)
|
|
304
|
+
# the clean slate to reach for between tests or to undo overrides.
|
|
305
|
+
# (Coercible built-ins are auto_coerce's job, not a reset flavor.)
|
|
306
|
+
def reset_scalars!
|
|
307
|
+
Codegen.reset_scalars!
|
|
57
308
|
end
|
|
58
309
|
|
|
59
310
|
# Empty the scalar registry entirely, built-ins included (see
|
|
@@ -64,42 +315,42 @@ module GraphWeaver
|
|
|
64
315
|
|
|
65
316
|
# Parse a query into a typed query module:
|
|
66
317
|
#
|
|
67
|
-
#
|
|
318
|
+
# PersonQuery = GraphWeaver.parse(schema:, query: "queries/person.graphql")
|
|
68
319
|
#
|
|
69
320
|
# query is a .graphql/.gql path (module name derived from the file
|
|
70
321
|
# name) or a raw query string (name derived from the operation name,
|
|
71
322
|
# falling back to "Query" for anonymous operations — collisions are
|
|
72
323
|
# impossible since each parse gets its own container). Pass name: to
|
|
73
|
-
# override,
|
|
74
|
-
def parse(schema:, query:, name: nil,
|
|
324
|
+
# override, client: to bake the module's default client/transport.
|
|
325
|
+
def parse(schema:, query:, name: nil, client: nil, scalars: nil, enums: nil, types: nil)
|
|
75
326
|
if query.end_with?(".graphql", ".gql")
|
|
76
327
|
name ||= "#{Inflect.camelize(File.basename(query, ".*"))}Query"
|
|
77
328
|
query = File.read(query)
|
|
78
329
|
end
|
|
79
330
|
|
|
80
|
-
Codegen.parse(schema:, query:, module_name: name,
|
|
331
|
+
Codegen.parse(schema:, query:, module_name: name, client:, scalars:, enums:, types:)
|
|
81
332
|
end
|
|
82
333
|
|
|
83
|
-
# One-shot dynamic execution —
|
|
334
|
+
# One-shot dynamic execution — a throwaway client, no build step:
|
|
84
335
|
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
336
|
+
# GraphWeaver.execute(schema, "query($id: ID!) { ... }", id: "1") # => Response
|
|
337
|
+
# GraphWeaver.execute!(url, "query { viewer { login } }") # => Result (or raise)
|
|
87
338
|
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
339
|
+
# The first argument is a url or schema source, exactly as
|
|
340
|
+
# GraphWeaver.new; this is Client#execute on a client you don't keep.
|
|
341
|
+
# (A url source introspects the schema on every call — keep a client
|
|
342
|
+
# for more than one query.) Variables are plain kwargs, as on a
|
|
343
|
+
# generated module (nothing reserved). execute returns the
|
|
344
|
+
# Response envelope, execute! the typed result, raising QueryError on
|
|
345
|
+
# top-level errors.
|
|
346
|
+
def execute(source, query, **variables)
|
|
347
|
+
client = source.is_a?(Client) ? source : Client.new(source)
|
|
348
|
+
client.execute(query, **variables)
|
|
98
349
|
end
|
|
99
350
|
|
|
100
351
|
# execute + data! — the typed result, or a raised QueryError. See execute.
|
|
101
|
-
def execute!(
|
|
102
|
-
execute(
|
|
352
|
+
def execute!(source, query, **variables)
|
|
353
|
+
execute(source, query, **variables).data!
|
|
103
354
|
end
|
|
104
355
|
end
|
|
105
356
|
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.1
|
|
4
|
+
version: 0.2.1
|
|
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,44 @@ 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/input_struct.rb
|
|
259
|
+
- lib/graph_weaver/logging.rb
|
|
260
|
+
- lib/graph_weaver/railtie.rb
|
|
196
261
|
- lib/graph_weaver/response.rb
|
|
262
|
+
- lib/graph_weaver/retry.rb
|
|
197
263
|
- lib/graph_weaver/rspec.rb
|
|
198
264
|
- lib/graph_weaver/schema_loader.rb
|
|
199
265
|
- lib/graph_weaver/selection.rb
|
|
266
|
+
- lib/graph_weaver/tasks.rb
|
|
200
267
|
- lib/graph_weaver/testing.rb
|
|
201
268
|
- lib/graph_weaver/testing/cassette.rb
|
|
202
269
|
- lib/graph_weaver/testing/failure.rb
|
|
203
|
-
- lib/graph_weaver/testing/
|
|
204
|
-
- lib/graph_weaver/testing/rspec.rb
|
|
270
|
+
- lib/graph_weaver/testing/fake_client.rb
|
|
205
271
|
- lib/graph_weaver/testing/values.rb
|
|
272
|
+
- lib/graph_weaver/transport.rb
|
|
273
|
+
- lib/graph_weaver/transport/faraday.rb
|
|
274
|
+
- lib/graph_weaver/transport/http.rb
|
|
206
275
|
- lib/graph_weaver/version.rb
|
|
207
276
|
homepage: https://github.com/dpep/graph_weaver
|
|
208
277
|
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
|