graph_weaver 0.6.1 → 0.7.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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +8 -0
  3. data/Gemfile.lock +153 -4
  4. data/README.md +45 -79
  5. data/docs/alternatives.md +195 -0
  6. data/docs/cassettes.md +61 -50
  7. data/docs/editors.md +32 -47
  8. data/docs/errors.md +360 -103
  9. data/docs/federation.md +692 -473
  10. data/docs/generated_modules.md +441 -314
  11. data/docs/getting_started.md +370 -194
  12. data/docs/i18n.md +171 -0
  13. data/docs/logging.md +197 -50
  14. data/docs/real_world.md +42 -27
  15. data/docs/scalars.md +307 -176
  16. data/docs/testing.md +473 -220
  17. data/docs/transports.md +224 -151
  18. data/docs/upgrading.md +258 -305
  19. data/examples/README.md +38 -0
  20. data/examples/countries.rb +39 -0
  21. data/examples/federation.rb +62 -0
  22. data/examples/github/generate.rb +20 -0
  23. data/examples/github/generated/star_mutation.rb +126 -0
  24. data/examples/github/generated/stargazers_query.rb +232 -0
  25. data/examples/github/generated/starred_query.rb +151 -0
  26. data/examples/github/queries/star.graphql +8 -0
  27. data/examples/github/queries/stargazers.graphql +22 -0
  28. data/examples/github/queries/starred.graphql +11 -0
  29. data/examples/github/run.rb +43 -0
  30. data/examples/github/setup.rb +18 -0
  31. data/examples/rick_and_morty.rb +57 -0
  32. data/graph_weaver.gemspec +19 -3
  33. data/lib/generators/graph_weaver/install_generator.rb +138 -4
  34. data/lib/graph_weaver/client.rb +69 -11
  35. data/lib/graph_weaver/codegen/aliases.rb +7 -5
  36. data/lib/graph_weaver/codegen/emit.rb +98 -29
  37. data/lib/graph_weaver/codegen/enum_type.rb +2 -1
  38. data/lib/graph_weaver/codegen/nodes.rb +39 -6
  39. data/lib/graph_weaver/codegen/registry.rb +175 -0
  40. data/lib/graph_weaver/codegen/scalar_type.rb +123 -30
  41. data/lib/graph_weaver/codegen/type_helpers.rb +56 -11
  42. data/lib/graph_weaver/codegen.rb +406 -195
  43. data/lib/graph_weaver/coerce.rb +155 -26
  44. data/lib/graph_weaver/context_seam.rb +54 -0
  45. data/lib/graph_weaver/errors.rb +284 -46
  46. data/lib/graph_weaver/federation.rb +129 -27
  47. data/lib/graph_weaver/graph.rb +315 -0
  48. data/lib/graph_weaver/hints.rb +100 -24
  49. data/lib/graph_weaver/in_process.rb +27 -15
  50. data/lib/graph_weaver/input_struct.rb +119 -32
  51. data/lib/graph_weaver/internal/endpoint.rb +80 -0
  52. data/lib/graph_weaver/internal/headers.rb +70 -0
  53. data/lib/graph_weaver/internal/overrides.rb +67 -5
  54. data/lib/graph_weaver/internal/planner.rb +45 -15
  55. data/lib/graph_weaver/internal/refusal.rb +49 -0
  56. data/lib/graph_weaver/internal/schemas.rb +23 -9
  57. data/lib/graph_weaver/internal/selection.rb +34 -0
  58. data/lib/graph_weaver/internal/server_input.rb +251 -0
  59. data/lib/graph_weaver/internal/test_clients.rb +276 -0
  60. data/lib/graph_weaver/internal/unused.rb +287 -0
  61. data/lib/graph_weaver/internal/values.rb +40 -4
  62. data/lib/graph_weaver/internal.rb +249 -14
  63. data/lib/graph_weaver/log_subscriber.rb +74 -0
  64. data/lib/graph_weaver/logging.rb +163 -19
  65. data/lib/graph_weaver/query_module.rb +44 -3
  66. data/lib/graph_weaver/railtie.rb +237 -17
  67. data/lib/graph_weaver/representation.rb +55 -17
  68. data/lib/graph_weaver/result_struct.rb +90 -0
  69. data/lib/graph_weaver/retry.rb +45 -13
  70. data/lib/graph_weaver/rspec.rb +404 -93
  71. data/lib/graph_weaver/schema_loader.rb +266 -56
  72. data/lib/graph_weaver/tasks.rb +380 -89
  73. data/lib/graph_weaver/testing/cassette.rb +34 -10
  74. data/lib/graph_weaver/testing/endpoint.rb +107 -0
  75. data/lib/graph_weaver/testing/failure.rb +69 -12
  76. data/lib/graph_weaver/testing/fake_client.rb +164 -45
  77. data/lib/graph_weaver/testing/router.rb +64 -13
  78. data/lib/graph_weaver/testing.rb +200 -58
  79. data/lib/graph_weaver/transport/faraday.rb +41 -8
  80. data/lib/graph_weaver/transport/http.rb +48 -6
  81. data/lib/graph_weaver/transport.rb +134 -27
  82. data/lib/graph_weaver/version.rb +1 -1
  83. data/lib/graph_weaver.rb +495 -106
  84. metadata +71 -3
  85. data/CHANGELOG.md +0 -2355
@@ -38,7 +38,8 @@ require_relative "transport"
38
38
  # default — pass the codes your API uses for transient failures)
39
39
  #
40
40
  # A server that answers with Retry-After sets the delay itself (clamped
41
- # to max_delay:); otherwise the configured backoff decides.
41
+ # to max_delay:) whether that answer raised or came back as a response
42
+ # carrying GraphQL errors; otherwise the configured backoff decides.
42
43
  #
43
44
  # Exhausting the retries re-raises the last error (or returns the last
44
45
  # code-matched response).
@@ -52,22 +53,31 @@ class GraphWeaver::Retry
52
53
  # server asking you to come back later rather than to fix anything
53
54
  RETRIABLE_CLIENT_STATUSES = [408, 429].freeze
54
55
 
56
+ # The status half of the policy, asked of a raised ServerError's status
57
+ # and of the status a response arrived on — one answer, so the two can't
58
+ # drift apart.
59
+ RETRIABLE_STATUS = ->(status) { status >= 500 || RETRIABLE_CLIENT_STATUSES.include?(status) }
60
+
55
61
  # retry 5xx (and 408/429), not the rest of 4xx; everything else listed
56
62
  # in retry_on: retries
57
63
  DEFAULT_RETRY_IF = lambda do |error|
58
- !error.is_a?(GraphWeaver::ServerError) ||
59
- error.status >= 500 || RETRIABLE_CLIENT_STATUSES.include?(error.status)
64
+ !error.is_a?(GraphWeaver::ServerError) || RETRIABLE_STATUS.call(error.status)
60
65
  end
61
66
 
62
67
  # said once, where the decision is made and where it is explained
63
68
  MUTATION_HINT = "not retrying a mutation — a request that failed without an answer " \
64
69
  "may still have been applied; pass retry_mutations: true if yours are idempotent"
65
- private_constant :DEFAULT_RETRY_IF, :MUTATION_HINT
70
+ private_constant :RETRIABLE_STATUS, :DEFAULT_RETRY_IF, :MUTATION_HINT
66
71
 
67
72
  def initialize(client, retries: 2, retry_on: [GraphWeaver::TransportError, GraphWeaver::ServerError],
68
73
  backoff: :exponential, base_delay: 0.5, max_delay: 30, jitter: true, retry_if: DEFAULT_RETRY_IF,
69
74
  retry_codes: [], retry_mutations: false, sleeper: nil)
70
75
  raise ArgumentError, "retries: must be >= 0" unless retries.is_a?(Integer) && retries >= 0
76
+ # a negative would reach Kernel#sleep, which raises — and the failure being
77
+ # retried would be lost behind an ArgumentError from somewhere else
78
+ { base_delay:, max_delay: }.each do |name, value|
79
+ raise ArgumentError, "#{name}: must be >= 0, got #{value}" unless value >= 0
80
+ end
71
81
 
72
82
  @client = client
73
83
  @retries = retries
@@ -96,13 +106,19 @@ class GraphWeaver::Retry
96
106
  def execute(query, variables: {}, operation_name: nil)
97
107
  attempts = mutation?(query) ? 1 : @retries + 1
98
108
  attempt = 0
99
- failure = T.let(nil, T.nilable(Exception))
100
109
 
101
110
  loop do
102
111
  attempt += 1
112
+ failure = nil
103
113
  begin
104
- response = @client.execute(query, variables:, operation_name:)
114
+ # each attempt is its own EXECUTE_EVENT; :retries says which one,
115
+ # so "slow" and "slow after two 502s" don't read the same in an APM
116
+ response = GraphWeaver::Internal::Log.with_retries(attempt - 1) do
117
+ @client.execute(query, variables:, operation_name:)
118
+ end
105
119
  return response unless attempt < attempts && retryable_response?(response)
120
+
121
+ failure = response
106
122
  rescue *@retry_on => e
107
123
  if attempt >= attempts || !@retry_if.call(e)
108
124
  GraphWeaver::Internal::Log.log(:warn) { MUTATION_HINT } if attempts == 1 && @retries.positive?
@@ -119,7 +135,6 @@ class GraphWeaver::Retry
119
135
  "retrying #{operation_name || "query"} in #{seconds.round(2)}s (attempt #{attempt + 1} of #{attempts})"
120
136
  end
121
137
  @sleeper.call(seconds)
122
- failure = nil
123
138
  end
124
139
  end
125
140
 
@@ -130,6 +145,19 @@ class GraphWeaver::Retry
130
145
  end
131
146
 
132
147
  def retryable_response?(response)
148
+ retryable_status?(response) || retryable_code?(response)
149
+ end
150
+
151
+ # The status a response arrived on, where it came back with one — the
152
+ # bundled transports say; a schema class, a fake and a hand-rolled client
153
+ # answer a plain Hash, and a response with no status is never retried on
154
+ # one.
155
+ def retryable_status?(response)
156
+ status = response.http_status if response.respond_to?(:http_status)
157
+ !status.nil? && RETRIABLE_STATUS.call(status)
158
+ end
159
+
160
+ def retryable_code?(response)
133
161
  return false if @retry_codes.empty?
134
162
 
135
163
  codes = (response.to_h["errors"] || []).filter_map { |error| error.dig("extensions", "code") }
@@ -137,14 +165,18 @@ class GraphWeaver::Retry
137
165
  end
138
166
 
139
167
  def delay(attempt, failure)
140
- # A Retry-After wins over our backoff: the server is the only party
141
- # that knows when its window reopens, and it isn't guessing. Still
142
- # clamped to max_delay:, so "come back in an hour" can't park a thread for
143
- # an hour and not jittered, since it's an instruction, not a guess.
144
- after = failure.retry_after if failure.is_a?(GraphWeaver::ServerError)
168
+ # A Retry-After wins over our backoff, however the failure arrived a
169
+ # raised ServerError, or the envelope a rate limit that came back with an
170
+ # errors body makes. The server is the only party that knows when its
171
+ # window reopens, and it isn't guessing. Still clamped to max_delay:, so
172
+ # "come back in an hour" can't park a thread for an hour — and not
173
+ # jittered, since it's an instruction, not a guess.
174
+ after = failure.retry_after if failure.respond_to?(:retry_after)
145
175
  return [after, @max_delay].min.to_f if after
146
176
 
147
- seconds = [@backoff.call(@base_delay, attempt), @max_delay].min.to_f
177
+ # floored at 0: a custom backoff: is the caller's arithmetic, and a
178
+ # negative from it would raise out of Kernel#sleep rather than retry
179
+ seconds = [[@backoff.call(@base_delay, attempt), @max_delay].min.to_f, 0.0].max
148
180
  @jitter ? seconds * (0.5 + rand * 0.5) : seconds
149
181
  end
150
182
  end