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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +6 -0
  3. data/CHANGELOG.md +213 -0
  4. data/Gemfile.lock +32 -2
  5. data/Makefile +7 -2
  6. data/NOTES.md +1 -1
  7. data/PLAN.md +34 -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 +139 -29
  12. data/docs/getting_started.md +134 -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 +44 -34
  17. data/docs/transports.md +124 -0
  18. data/graph_weaver.gemspec +7 -1
  19. data/lib/graph_weaver/client.rb +200 -0
  20. data/lib/graph_weaver/codegen/emit.rb +88 -39
  21. data/lib/graph_weaver/codegen/enum_type.rb +149 -0
  22. data/lib/graph_weaver/codegen/nodes.rb +109 -9
  23. data/lib/graph_weaver/codegen/scalar_type.rb +53 -35
  24. data/lib/graph_weaver/codegen.rb +301 -67
  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/logging.rb +41 -0
  29. data/lib/graph_weaver/railtie.rb +29 -0
  30. data/lib/graph_weaver/retry.rb +97 -0
  31. data/lib/graph_weaver/rspec.rb +19 -14
  32. data/lib/graph_weaver/schema_loader.rb +156 -20
  33. data/lib/graph_weaver/selection.rb +3 -3
  34. data/lib/graph_weaver/tasks.rb +71 -0
  35. data/lib/graph_weaver/testing/cassette.rb +42 -21
  36. data/lib/graph_weaver/testing/failure.rb +22 -22
  37. data/lib/graph_weaver/testing/{fake_executor.rb → fake_client.rb} +13 -13
  38. data/lib/graph_weaver/testing/values.rb +2 -2
  39. data/lib/graph_weaver/testing.rb +32 -16
  40. data/lib/graph_weaver/transport/faraday.rb +56 -0
  41. data/lib/graph_weaver/transport/http.rb +87 -0
  42. data/lib/graph_weaver/transport.rb +120 -0
  43. data/lib/graph_weaver/version.rb +1 -1
  44. data/lib/graph_weaver.rb +208 -38
  45. metadata +73 -5
  46. data/lib/graph_weaver/faraday_executor.rb +0 -61
  47. data/lib/graph_weaver/http_executor.rb +0 -44
  48. 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
@@ -1,3 +1,3 @@
1
1
  module GraphWeaver
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
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/http_executor"
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
- # 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)
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
- # 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
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
- def executor
25
- @executor or raise Error, "no executor configured set GraphWeaver.executor= or pass executor:"
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
- # GraphWeaver.register_scalar("Money", type: Money, requires: "bigdecimal")
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:, cast: nil, serialize: nil, requires: nil, coerce: false)
48
- Codegen.register_scalar(graphql_name, type:, cast:, serialize:, requires:, coerce:)
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. 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:)
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
- # PersonQuery = GraphWeaver.parse(schema:, query: "queries/person.graphql")
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, executor: to set the module's transport.
74
- def parse(schema:, query:, name: nil, executor: 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, executor:)
250
+ Codegen.parse(schema:, query:, module_name: name, client:, scalars:, enums:, types:)
81
251
  end
82
252
 
83
- # One-shot dynamic execution — no module handling, no build step:
253
+ # One-shot dynamic execution — a throwaway client, no build step:
84
254
  #
85
- # GraphWeaver.execute(schema:, query:, variables: { id: "1" }) # => Response
86
- # GraphWeaver.execute!(schema:, query:, variables: { id: "1" }) # => Result (or raise)
255
+ # GraphWeaver.execute(schema, "query($id: ID!) { ... }", id: "1") # => Response
256
+ # GraphWeaver.execute!(url, "query { viewer { login } }") # => Result (or raise)
87
257
  #
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)
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!(schema:, query:, variables: {}, executor: nil)
102
- execute(schema:, query:, variables:, executor:).data!
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.1.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/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/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/fake_executor.rb
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