graph_weaver 0.0.1 → 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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +6 -0
  3. data/CHANGELOG.md +279 -0
  4. data/Gemfile.lock +75 -17
  5. data/Makefile +11 -1
  6. data/NOTES.md +1 -1
  7. data/PLAN.md +85 -22
  8. data/README.md +86 -26
  9. data/docs/cassettes.md +76 -0
  10. data/docs/errors.md +134 -0
  11. data/docs/generated_modules.md +229 -0
  12. data/docs/getting_started.md +134 -0
  13. data/docs/logging.md +33 -0
  14. data/docs/real_world.md +53 -0
  15. data/docs/scalars.md +183 -0
  16. data/docs/testing.md +103 -0
  17. data/docs/transports.md +124 -0
  18. data/graph_weaver.gemspec +10 -1
  19. data/lib/graph_weaver/client.rb +200 -0
  20. data/lib/graph_weaver/codegen/emit.rb +286 -0
  21. data/lib/graph_weaver/codegen/enum_type.rb +149 -0
  22. data/lib/graph_weaver/codegen/nodes.rb +356 -0
  23. data/lib/graph_weaver/codegen/scalar_type.rb +256 -0
  24. data/lib/graph_weaver/codegen.rb +396 -401
  25. data/lib/graph_weaver/errors.rb +322 -0
  26. data/lib/graph_weaver/hints.rb +63 -0
  27. data/lib/graph_weaver/inflect.rb +19 -0
  28. data/lib/graph_weaver/logging.rb +41 -0
  29. data/lib/graph_weaver/railtie.rb +29 -0
  30. data/lib/graph_weaver/response.rb +55 -0
  31. data/lib/graph_weaver/retry.rb +97 -0
  32. data/lib/graph_weaver/rspec.rb +59 -0
  33. data/lib/graph_weaver/schema_loader.rb +208 -8
  34. data/lib/graph_weaver/selection.rb +68 -0
  35. data/lib/graph_weaver/tasks.rb +71 -0
  36. data/lib/graph_weaver/testing/cassette.rb +224 -0
  37. data/lib/graph_weaver/testing/failure.rb +109 -0
  38. data/lib/graph_weaver/testing/fake_client.rb +228 -0
  39. data/lib/graph_weaver/testing/values.rb +98 -0
  40. data/lib/graph_weaver/testing.rb +106 -0
  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 +268 -4
  46. metadata +132 -2
  47. data/lib/graph_weaver/http_executor.rb +0 -31
data/docs/testing.md ADDED
@@ -0,0 +1,103 @@
1
+ # Testing
2
+
3
+ Everything here is a *client* — the one interface queries run
4
+ through: anything with `execute(query, variables:)` returning
5
+ `{"data" => ..., "errors" => ...}` (see [transports](transports.md)).
6
+ Fakes, failures, and cassettes all slot in wherever a real transport
7
+ would.
8
+
9
+ `require "graph_weaver/rspec"` from your spec helper (or
10
+ `graph_weaver/testing` outside rspec — never in production) for a
11
+ zero-setup fake backend. `FakeClient` fabricates
12
+ schema-correct responses for whatever query arrives: real enum values,
13
+ valid `__typename` members, iso8601 date scalars — every fake casts
14
+ cleanly through your generated structs.
15
+
16
+ ```ruby
17
+ fake = GraphWeaver::Testing::FakeClient.new(schema:)
18
+
19
+ person = PersonQuery.execute!(fake, id: "1").person
20
+ person.name # => "Eliza Kertzmann" (faker-matched on field name, when faker is loaded)
21
+ person.birthday # => a real Date
22
+ ```
23
+
24
+ Pin what matters, keyed by GraphQL names (schema vocabulary — keys
25
+ survive query refactors); `"Type.field"` beats `"field"`:
26
+
27
+ ```ruby
28
+ GraphWeaver::Testing::FakeClient.new(schema:, overrides: {
29
+ "Person.name" => "Daniel",
30
+ "email" => -> { "test@example.com" },
31
+ })
32
+ ```
33
+
34
+ With rspec, one line in `spec/support/graph_weaver.rb` is the whole
35
+ default setup — the schema auto-locates from the committed dump at
36
+ `GraphWeaver.schema_path`, and `auto_fake` defaults on:
37
+
38
+ ```ruby
39
+ require "graph_weaver/rspec" # seed follows --seed; every example auto-fakes
40
+ ```
41
+
42
+ Configure to override any of it:
43
+
44
+ ```ruby
45
+ GraphWeaver::Testing.configure do |config|
46
+ config.schema = MySchema # an in-process class instead of the located dump
47
+ config.auto_fake = false # opt out of per-example fakes
48
+ config.mode = :faker # or :literal (plain typed values); nil = auto
49
+ config.overrides = { "Person.name" => "Daniel" }
50
+ config.list_size = 1..3
51
+ config.null_chance = 0.1 # nullable fields go nil sometimes
52
+ end
53
+ ```
54
+
55
+ With the rspec integration, `rspec --seed 1234` reproduces fake data
56
+ along with test order, and `auto_fake` installs a seeded fake as the
57
+ app client per example (generate modules *without* a baked `client:` so
58
+ they consult `GraphWeaver.client`). `mode:` picks value fabrication: `:faker`
59
+ (semantic, field-name matched — raises if the gem is missing),
60
+ `:literal` (plain type-derived), or nil to auto-detect faker.
61
+
62
+ **Simulating failures** — every failure mode is just a client, so
63
+ error-handling paths are testable without a server that misbehaves on cue:
64
+
65
+ ```ruby
66
+ Failure = GraphWeaver::Testing::Failure
67
+
68
+ PersonQuery.execute(id: "1", client: Failure.transport) # TransportError (cause preserved)
69
+ PersonQuery.execute(id: "1", client: Failure.server(status: 502)) # ServerError
70
+ PersonQuery.execute(id: "1", client: Failure.throttled) # QueryError, code THROTTLED
71
+ PersonQuery.execute(id: "1", client: Failure.stale_schema) # schema_stale? => true
72
+ PersonQuery.execute(id: "1", client: Failure.graphql("boom", data: {...})) # partial failure
73
+
74
+ # retries: clients run in sequence (the last repeats) — here, two
75
+ # transport failures and then a FakeClient serving good responses
76
+ fake = GraphWeaver::Testing::FakeClient.new(schema:)
77
+ GraphWeaver::Testing::Sequence.new(Failure.transport, Failure.transport, fake)
78
+
79
+ # type mismatch: corrupt: derives a wrong-typed wire value for the field —
80
+ # casting raises GraphWeaver::TypeError (overrides remain the manual escape hatch)
81
+ GraphWeaver::Testing::FakeClient.new(schema:, corrupt: "Person.birthday")
82
+
83
+ # stale schema naming a real (sampled) field
84
+ Failure.stale_schema(schema: MySchema)
85
+
86
+ # field-level partial failure with real GraphQL null propagation: the error
87
+ # lands with its concrete path and nulls bubble to the nearest nullable spot
88
+ GraphWeaver::Testing::FakeClient.new(schema:, fail_at: { path: "person.email", code: "PRIVATE" })
89
+ ```
90
+
91
+ **Capture and replay** — cassettes record real API responses and replay
92
+ them offline, above the transport (no HTTP interception):
93
+
94
+ ```ruby
95
+ # records against the live client when the file is missing, replays after
96
+ cassette = GraphWeaver::Testing::Cassette.use("github", client: live)
97
+ ```
98
+
99
+ Re-record with `GRAPHWEAVER_RECORD=1`, anonymize before committing
100
+ (`config.anonymize = true` scrubs as recordings happen, or
101
+ `rake graph_weaver:cassettes:anonymize` after) — the full workflow guide
102
+ is **[cassettes](cassettes.md)**.
103
+
@@ -0,0 +1,124 @@
1
+ # Transports
2
+
3
+ A *client* is anything with `execute(query, variables:)` whose result
4
+ `to_h`s into `{"data" => ..., "errors" => ...}` — from a full
5
+ `GraphWeaver::Client` down to a schema class (in-process execution), a
6
+ [FakeClient](testing.md), or anything you write. Every slot that takes a
7
+ client accepts any of them.
8
+
9
+ A *transport* is the network end of that contract — GraphQL-over-HTTP. The bundled
10
+ two — `Transport::HTTP` (net/http, zero dependencies, loaded by default)
11
+ and `Transport::Faraday` (opt-in) — subclass `GraphWeaver::Transport`,
12
+ which owns the shared flow: encode the request, reclassify network
13
+ failures as `TransportError`, raise `ServerError` on non-2xx, parse the
14
+ body. A subclass only implements `post(body) => [status, body]` — that's
15
+ the whole recipe for bringing your own HTTP client.
16
+
17
+ ## One-shot setup: a client
18
+
19
+ Most apps need one line:
20
+
21
+ ```ruby
22
+ github = GraphWeaver.new("https://api.example.com/graphql", auth: ENV["API_TOKEN"])
23
+ ```
24
+
25
+ `GraphWeaver.new` builds a [`Client`](real_world.md): the best transport
26
+ with auth applied (exposed as `client.transport`), the schema
27
+ introspected lazily, and `parse`/`execute` bound to both.
28
+
29
+ - `auth:` — a token; "Bearer" is assumed unless the string carries its own
30
+ scheme (`"Basic dXNlcjpwYXNz..."`)
31
+ - `headers:` — anything else (API keys, custom headers)
32
+ - `retries:` — off by default; `true` for a `Retry` with defaults,
33
+ or a Hash of its options
34
+ - `cache:` / `ttl:` — schema introspection caching (see
35
+ [real world](real_world.md)); url clients only — a schema source never
36
+ introspects, so passing them raises
37
+ - a block customizes the Faraday connection (Faraday only — raises without it)
38
+
39
+ To wire generated modules that don't bake a client, make it the app's
40
+ default: `GraphWeaver.client = github`. Anything satisfying the execute
41
+ contract works there — testing's auto_fake swaps in a fake per example.
42
+
43
+ **Transport pick**: `Transport::Faraday` when the app already loads
44
+ faraday (its middleware/proxy/timeout ecosystem comes along), the
45
+ zero-dependency `Transport::HTTP` otherwise. Detection is `defined?(Faraday)` —
46
+ deliberately *not* a require: faraday rides along transitively in most
47
+ bundles (stripe, octokit, ...), and try-requiring would silently switch
48
+ transports on apps that never chose it. With faraday under
49
+ `require: false`, load it before building the client.
50
+
51
+ ## Building blocks
52
+
53
+ The client is convenience, not the only door — construct and assign
54
+ yourself for full control:
55
+
56
+ ```ruby
57
+ # zero-dependency Net::HTTP — persistent (keep-alive) connection,
58
+ # mutex-serialized; timeouts raise retriable TransportError
59
+ GraphWeaver::Transport::HTTP.new(
60
+ url,
61
+ headers: { ... },
62
+ open_timeout: 10, read_timeout: 30, # seconds (the defaults)
63
+ keep_alive_timeout: 2, # idle window before reconnecting
64
+ )
65
+
66
+ # Faraday: a url (+ optional middleware block), or a ready connection
67
+ GraphWeaver::Transport::Faraday.new(url) do |conn|
68
+ conn.request :authorization, "Bearer", -> { Tokens.fetch } # dynamic tokens
69
+ conn.response :logger
70
+ end
71
+ GraphWeaver::Transport::Faraday.new(MyApp.faraday_connection)
72
+
73
+ # One Faraday::Connection is reused for the transport's lifetime, but
74
+ # socket keep-alive depends on the ADAPTER: Faraday's default net_http
75
+ # adapter opens a fresh connection per request. For persistent sockets
76
+ # (and real pooling), pick a persistent adapter:
77
+ GraphWeaver::Transport::Faraday.new(url) do |conn|
78
+ conn.adapter :net_http_persistent # gem "net-http-persistent"
79
+ end
80
+
81
+ GraphWeaver.client = ... # the app default (a Client or any of the above)
82
+ ```
83
+
84
+ ## Client resolution
85
+
86
+ The canonical order — how a generated module finds its client (each slot
87
+ takes a `Client` or any bare transport/fake):
88
+
89
+ 1. per call: `execute(some_client, ...)` — the optional first positional
90
+ argument, so variables keep the entire kwarg namespace
91
+ 2. per module: `MyQuery.client = something`
92
+ 3. baked constant: `Codegen.generate(..., client: MyApi::CLIENT)`
93
+ 4. the app default: `GraphWeaver.client=`
94
+
95
+ Nothing set anywhere raises with a message saying which knobs exist.
96
+
97
+ ## Retries
98
+
99
+ `Retry` wraps any client/transport:
100
+
101
+ ```ruby
102
+ GraphWeaver::Retry.new(
103
+ inner_transport,
104
+ tries: 5, # total attempts, first included
105
+ backoff: :exponential, # or :linear, or ->(attempt) { seconds }
106
+ base: 0.5, max: 30, # seconds; delays clamp at max:
107
+ jitter: true, # randomize each delay by 50-100%
108
+ on: [GraphWeaver::TransportError, GraphWeaver::ServerError],
109
+ retry_if: ->(error) { ... }, # fine-grain within on:
110
+ retry_codes: ["THROTTLED"], # also retry GraphQL errors by code
111
+ )
112
+ ```
113
+
114
+ Defaults: transport failures always retry (the request never arrived);
115
+ `ServerError` only on 5xx — a 4xx is a bug in the request, retrying won't
116
+ fix it. `retry_codes:` re-inspects response envelopes so GraphQL-level
117
+ throttling can retry too (off by default — pass the codes your API uses).
118
+ Exhausting `tries:` re-raises the last error (or returns the last
119
+ code-matched response).
120
+
121
+ Or via the client: `GraphWeaver.new(url, retries: { tries: 5, retry_codes: ["THROTTLED"] })`.
122
+
123
+ What classifies as a transport failure is an extensible set — see
124
+ [errors](errors.md#extending-transporterror) (`GraphWeaver.register_transport_error`).
data/graph_weaver.gemspec CHANGED
@@ -3,7 +3,9 @@ require_relative "lib/graph_weaver/version"
3
3
  Gem::Specification.new do |s|
4
4
  s.authors = ["Daniel Pepper"]
5
5
  s.description = "A typed GraphQL client for Ruby — generate Sorbet T::Structs from queries, with federation, extensibility, and testing in mind"
6
- s.files = `git ls-files * ':!:spec' ':!:sorbet' ':!:bin'`.split("\n")
6
+ # ".yardopts" explicitly: `git ls-files *` skips dotfiles, and
7
+ # rubydoc.info needs it shipped to render docstrings as markdown
8
+ s.files = `git ls-files * ':!:spec' ':!:sorbet' ':!:bin' ':!:examples'`.split("\n") + [".yardopts"]
7
9
  s.homepage = "https://github.com/dpep/graph_weaver"
8
10
  s.license = "MIT"
9
11
  s.name = "graph_weaver"
@@ -15,10 +17,17 @@ Gem::Specification.new do |s|
15
17
  s.add_dependency "graphql", ">= 2"
16
18
  s.add_dependency "sorbet-runtime"
17
19
 
20
+ s.add_development_dependency "apollo-federation" # federation integration subgraphs
21
+ s.add_development_dependency "bigdecimal"
18
22
  s.add_development_dependency "debug"
23
+ s.add_development_dependency "faker"
24
+ s.add_development_dependency "faraday"
25
+ s.add_development_dependency "rake"
26
+ s.add_development_dependency "redcarpet" # yard --markup markdown
19
27
  s.add_development_dependency "rspec"
20
28
  s.add_development_dependency "simplecov"
21
29
  s.add_development_dependency "sorbet"
22
30
  s.add_development_dependency "tapioca"
23
31
  s.add_development_dependency "webrick"
32
+ s.add_development_dependency "yard"
24
33
  end
@@ -0,0 +1,200 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "codegen"
5
+ require_relative "errors"
6
+ require_relative "inflect"
7
+ require_relative "retry"
8
+ require_relative "schema_loader"
9
+ require_relative "transport/http"
10
+
11
+ # One object tying the whole flow together — transport, schema, and
12
+ # generation:
13
+ #
14
+ # github = GraphWeaver.new("https://api.github.com/graphql", auth: token, cache: true)
15
+ # github.register_scalar("DateTime", Time, serialize: :iso8601, requires: "time")
16
+ #
17
+ # RepoQuery = github.parse("queries/repo.graphql") # implicit schema + transport
18
+ # github.execute!("query { viewer { login } }") # one-shot
19
+ #
20
+ # The first argument is a url (a transport is built; the schema comes
21
+ # from introspection on first use, cached per cache:/ttl:) or a schema
22
+ # source — a live schema class (which also executes in-process), or a
23
+ # path/SDL/introspection dump via SchemaLoader. Pass transport: to
24
+ # bring your own transport for a schema source.
25
+ #
26
+ # Clients are independent: each has its own transport, schema, and
27
+ # scalar registrations, so one app can talk to several GraphQL servers —
28
+ # even ones that disagree about what a "DateTime" is.
29
+ class GraphWeaver::Client
30
+ URL = %r{\Ahttps?://}i
31
+
32
+ def initialize(source, auth: nil, headers: {}, retries: false, transport: nil, cache: nil, ttl: nil, &middleware)
33
+ if source.is_a?(String) && source.match?(URL)
34
+ raise ArgumentError, "pass a url or transport:, not both" if transport
35
+
36
+ @transport = wrap_retries(build_transport(source, auth:, headers:, &middleware), retries)
37
+ else
38
+ if auth || middleware || retries != false
39
+ raise ArgumentError, "auth:/retries:/middleware apply to a url — got a schema source"
40
+ end
41
+ if cache || ttl
42
+ # a schema source never introspects, so a cache would silently no-op
43
+ raise ArgumentError, "cache:/ttl: apply to url introspection — got a schema source"
44
+ end
45
+
46
+ # a live schema class doubles as an in-process transport; a loaded
47
+ # dump has no resolvers, so it is type information only
48
+ @schema = source.is_a?(Module) ? source : GraphWeaver::SchemaLoader.load(source)
49
+ @transport = transport || (source if source.is_a?(Module))
50
+ end
51
+
52
+ @cache = cache
53
+ @ttl = ttl
54
+ @scalars = {}
55
+ @enums = {}
56
+ @types = {}
57
+ end
58
+
59
+ # The transport queries run through: a url-built transport, an
60
+ # explicit transport:, or the live schema class executing in-process.
61
+ # Clients are self-contained — the app default never leaks in; nil for
62
+ # schema-dump clients (type information only).
63
+ attr_reader :transport
64
+
65
+ # transport, when this client must be able to execute
66
+ def transport!
67
+ transport or raise GraphWeaver::Error,
68
+ "this client has no transport (built from a schema dump) — pass a url or transport:"
69
+ end
70
+
71
+ # The schema, introspecting through the transport on first use (cached
72
+ # per the client's cache:/ttl:) unless one was given up front.
73
+ def schema
74
+ @schema ||= GraphWeaver::SchemaLoader.introspect(transport!, cache: @cache, ttl: @ttl)
75
+ end
76
+
77
+ # Client-scoped scalar registration: consulted before the global
78
+ # registry when this client generates code, so two clients can map the
79
+ # same scalar name onto different Ruby types. Same signature as
80
+ # GraphWeaver.register_scalar.
81
+ def register_scalar(graphql_name, type, cast: nil, serialize: nil, requires: nil, coerce: nil)
82
+ validate_registration!("scalar", graphql_name.to_s)
83
+ @scalars[graphql_name.to_s] =
84
+ GraphWeaver::Codegen::ScalarType.new(graphql_name, type, cast:, serialize:, requires:, coerce:)
85
+ end
86
+
87
+ # Client-scoped enum mapping: this client's generated code speaks your
88
+ # T::Enum for the named GraphQL enum (see Codegen::EnumType — inference
89
+ # by name, map: for renames, fallback: to absorb unknown wire values).
90
+ def register_enum(graphql_name, type, map: nil, fallback: nil, requires: nil)
91
+ validate_registration!("enum", graphql_name.to_s)
92
+ @enums[graphql_name.to_s] =
93
+ GraphWeaver::Codegen::EnumType.new(graphql_name, type, map:, fallback:, requires:)
94
+ end
95
+
96
+ # Bulk, inference-only form: register_enums("Species" => PetKind, ...)
97
+ def register_enums(mappings)
98
+ mappings.each { |graphql_name, type| register_enum(graphql_name, type) }
99
+ end
100
+
101
+ # Client-scoped type helpers: include app-owned modules into every
102
+ # struct this client generates from the named GraphQL type — pass
103
+ # modules, or a block to build one inline. Additive with global
104
+ # registrations (see GraphWeaver.register_type).
105
+ def register_type(graphql_name, *mixins, requires: nil, &block)
106
+ validate_registration!("type", graphql_name.to_s)
107
+ entry = @types[graphql_name.to_s] ||= { mixins: [], requires: [] }
108
+ GraphWeaver::Codegen.add_type_helpers(entry, graphql_name, mixins, requires, block)
109
+ end
110
+
111
+ # Parse a query (a .graphql path or raw string) into a typed module
112
+ # bound to this client's schema, scalars, enums, helpers, and transport
113
+ # (including a live schema class executing in-process — the module came
114
+ # from this client, so it runs against it; pass a client per call to
115
+ # override, e.g. with a fake).
116
+ def parse(query, name: nil)
117
+ GraphWeaver.parse(schema:, query:, name:, client: transport,
118
+ scalars: @scalars, enums: @enums, types: @types)
119
+ end
120
+
121
+ # Parse every .graphql query in a directory into typed modules, named
122
+ # like generation would name them — the no-build-step analog of
123
+ # generate! + load_generated!:
124
+ #
125
+ # github.load_queries! # queries/person.graphql => ::PersonQuery
126
+ # github.load_queries!(namespace: Github) # => Github::PersonQuery
127
+ #
128
+ # Reloadable (constants are replaced), so it suits consoles and dev.
129
+ # Returns the modules.
130
+ def load_queries!(dir = GraphWeaver.queries_path, namespace: Object)
131
+ Dir[File.join(dir, "*.graphql")].sort.map do |path|
132
+ name = "#{GraphWeaver::Inflect.camelize(File.basename(path, ".graphql"))}Query"
133
+ namespace.send(:remove_const, name) if namespace.const_defined?(name, false)
134
+ GraphWeaver.log(:info) { "loaded #{name} from #{path}" }
135
+ namespace.const_set(name, parse(path))
136
+ end
137
+ end
138
+
139
+ # One-shot dynamic execution — parse + execute, returning the typed
140
+ # Response envelope (execute! returns the result or raises). Variables
141
+ # are plain kwargs, exactly as on a generated module; graphql-cased
142
+ # string keys work too.
143
+ def execute(query, **variables)
144
+ mod = parse(query)
145
+ kwargs = variables.to_h { |key, value| [GraphWeaver::Inflect.underscore(key.to_s).to_sym, value] }
146
+ mod.execute(transport!, **kwargs)
147
+ end
148
+
149
+ def execute!(query, **variables)
150
+ execute(query, **variables).data!
151
+ end
152
+
153
+ private
154
+
155
+ # Fail a typo'd registration at the call site when the schema is
156
+ # already in hand (a schema-source client, or a url client after first
157
+ # use) — immediate feedback in consoles. Lazily-introspecting clients
158
+ # get the same check at generation time instead; never trigger an
159
+ # introspection just to validate a name.
160
+ def validate_registration!(kind, name)
161
+ return unless @schema
162
+
163
+ GraphWeaver::Codegen.validate_registration!(@schema, kind, name)
164
+ end
165
+
166
+ # auth: is a token — "Bearer" is assumed unless the string carries its
167
+ # own scheme ("Basic dXNlcjpwYXNz..."). Transport pick: Faraday when
168
+ # the app already loads it (its middleware/proxy/timeout ecosystem
169
+ # comes along), the zero-dependency Transport::HTTP otherwise.
170
+ # Detection is `defined?(Faraday)` — deliberately NOT a require:
171
+ # faraday rides along transitively in most bundles (stripe, octokit,
172
+ # ...), and try-requiring would switch transports on apps that never
173
+ # chose it. With faraday under `require: false`, load it before
174
+ # building the client.
175
+ def build_transport(url, auth:, headers:, &middleware)
176
+ headers = headers.dup
177
+ if auth
178
+ headers["Authorization"] ||= auth.include?(" ") ? auth : "Bearer #{auth}"
179
+ end
180
+
181
+ if defined?(::Faraday)
182
+ require_relative "transport/faraday"
183
+ GraphWeaver::Transport::Faraday.new(url, headers:, &middleware)
184
+ elsif middleware
185
+ raise ArgumentError, "middleware blocks require the faraday gem"
186
+ else
187
+ GraphWeaver::Transport::HTTP.new(url, headers:)
188
+ end
189
+ end
190
+
191
+ # retries: is off by default — true for Retry defaults, or a
192
+ # Hash of its options
193
+ def wrap_retries(transport, retries)
194
+ case retries
195
+ when true then GraphWeaver::Retry.new(transport)
196
+ when false, nil then transport
197
+ else GraphWeaver::Retry.new(transport, **retries)
198
+ end
199
+ end
200
+ end