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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +6 -0
  3. data/CHANGELOG.md +256 -0
  4. data/Gemfile.lock +32 -2
  5. data/Makefile +7 -2
  6. data/NOTES.md +1 -1
  7. data/PLAN.md +41 -1
  8. data/README.md +56 -41
  9. data/docs/cassettes.md +76 -0
  10. data/docs/errors.md +37 -9
  11. data/docs/generated_modules.md +178 -29
  12. data/docs/getting_started.md +136 -0
  13. data/docs/logging.md +33 -0
  14. data/docs/real_world.md +27 -20
  15. data/docs/scalars.md +122 -8
  16. data/docs/testing.md +55 -38
  17. data/docs/transports.md +124 -0
  18. data/graph_weaver.gemspec +7 -1
  19. data/lib/graph_weaver/client.rb +201 -0
  20. data/lib/graph_weaver/codegen/emit.rb +315 -76
  21. data/lib/graph_weaver/codegen/enum_type.rb +149 -0
  22. data/lib/graph_weaver/codegen/nodes.rb +123 -68
  23. data/lib/graph_weaver/codegen/scalar_type.rb +53 -35
  24. data/lib/graph_weaver/codegen.rb +272 -101
  25. data/lib/graph_weaver/errors.rb +37 -25
  26. data/lib/graph_weaver/hints.rb +63 -0
  27. data/lib/graph_weaver/inflect.rb +2 -1
  28. data/lib/graph_weaver/input_struct.rb +59 -0
  29. data/lib/graph_weaver/logging.rb +41 -0
  30. data/lib/graph_weaver/railtie.rb +30 -0
  31. data/lib/graph_weaver/retry.rb +97 -0
  32. data/lib/graph_weaver/rspec.rb +17 -14
  33. data/lib/graph_weaver/schema_loader.rb +156 -20
  34. data/lib/graph_weaver/selection.rb +3 -3
  35. data/lib/graph_weaver/tasks.rb +71 -0
  36. data/lib/graph_weaver/testing/cassette.rb +42 -21
  37. data/lib/graph_weaver/testing/failure.rb +22 -22
  38. data/lib/graph_weaver/testing/{fake_executor.rb → fake_client.rb} +13 -13
  39. data/lib/graph_weaver/testing/values.rb +2 -2
  40. data/lib/graph_weaver/testing.rb +31 -15
  41. data/lib/graph_weaver/transport/faraday.rb +56 -0
  42. data/lib/graph_weaver/transport/http.rb +87 -0
  43. data/lib/graph_weaver/transport.rb +120 -0
  44. data/lib/graph_weaver/version.rb +1 -1
  45. data/lib/graph_weaver.rb +289 -38
  46. metadata +74 -5
  47. data/lib/graph_weaver/faraday_executor.rb +0 -61
  48. data/lib/graph_weaver/http_executor.rb +0 -44
  49. 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/http_executor"
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
- # require "graph_weaver/faraday_executor" # Faraday transport
14
- # require "graph_weaver/directive_defaults_patch" # fix graphql-ruby
15
- # dropping directive argument defaults when loading SDL (needed for
16
- # Apollo supergraph SDL until rmosolgo/graphql-ruby#5659 ships)
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
- # global default transport; generated modules fall back to this
20
- # (override per module with MyQuery.executor=, or per call with
21
- # execute(executor:))
22
- attr_writer :executor
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 executor
25
- @executor or raise Error, "no executor configured — set GraphWeaver.executor= or pass executor:"
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
- # GraphWeaver.register_scalar("Money", type: Money, requires: "bigdecimal")
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:, cast: nil, serialize: nil, requires: nil, coerce: false)
48
- Codegen.register_scalar(graphql_name, type:, cast:, serialize:, requires:, coerce:)
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. Pass
53
- # coerce: true to reload the built-ins with input coercion enabled
54
- # (Float accepts 5/"5", etc.), then register your own scalars on top.
55
- def reset_scalars!(coerce: false)
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
- # PersonQuery = GraphWeaver.parse(schema:, query: "queries/person.graphql")
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, executor: to set the module's transport.
74
- def parse(schema:, query:, name: nil, executor: 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, executor:)
331
+ Codegen.parse(schema:, query:, module_name: name, client:, scalars:, enums:, types:)
81
332
  end
82
333
 
83
- # One-shot dynamic execution — no module handling, no build step:
334
+ # One-shot dynamic execution — a throwaway client, no build step:
84
335
  #
85
- # GraphWeaver.execute(schema:, query:, variables: { id: "1" }) # => Response
86
- # GraphWeaver.execute!(schema:, query:, variables: { id: "1" }) # => Result (or raise)
336
+ # GraphWeaver.execute(schema, "query($id: ID!) { ... }", id: "1") # => Response
337
+ # GraphWeaver.execute!(url, "query { viewer { login } }") # => Result (or raise)
87
338
  #
88
- # Mirrors a generated module: execute returns the Response envelope,
89
- # execute! returns the typed result directly and raises QueryError on
90
- # top-level errors. Transport precedence: executor: param, then
91
- # GraphWeaver.executor, then in-process execution against schema.
92
- # Variable keys may be graphql-cased strings or ruby symbols.
93
- def execute(schema:, query:, variables: {}, executor: nil)
94
- executor ||= @executor || schema
95
- mod = parse(schema:, query:, executor:)
96
- kwargs = variables.to_h { |key, value| [Inflect.underscore(key.to_s).to_sym, value] }
97
- mod.execute(**kwargs)
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!(schema:, query:, variables: {}, executor: nil)
102
- execute(schema:, query:, variables:, executor:).data!
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.0
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/faraday_executor.rb
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/fake_executor.rb
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
@@ -1,5 +0,0 @@
1
- # typed: true
2
- # frozen_string_literal: true
3
-
4
- # moved — the canonical entry point is:
5
- require_relative "../rspec"