@apollo-deploy/tesseract 1.9.0 → 2.1.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 (194) hide show
  1. package/README.md +218 -110
  2. package/dist/adapters/csharp/index.d.ts +5 -8
  3. package/dist/adapters/csharp/index.d.ts.map +1 -1
  4. package/dist/adapters/csharp/index.js +314 -74
  5. package/dist/adapters/csharp/index.js.map +1 -1
  6. package/dist/adapters/documentation.d.ts +17 -0
  7. package/dist/adapters/documentation.d.ts.map +1 -0
  8. package/dist/adapters/documentation.js +268 -0
  9. package/dist/adapters/documentation.js.map +1 -0
  10. package/dist/adapters/go/index.d.ts +5 -8
  11. package/dist/adapters/go/index.d.ts.map +1 -1
  12. package/dist/adapters/go/index.js +197 -101
  13. package/dist/adapters/go/index.js.map +1 -1
  14. package/dist/adapters/kotlin/index.d.ts +5 -9
  15. package/dist/adapters/kotlin/index.d.ts.map +1 -1
  16. package/dist/adapters/kotlin/index.js +280 -212
  17. package/dist/adapters/kotlin/index.js.map +1 -1
  18. package/dist/adapters/php/index.d.ts +5 -15
  19. package/dist/adapters/php/index.d.ts.map +1 -1
  20. package/dist/adapters/php/index.js +362 -90
  21. package/dist/adapters/php/index.js.map +1 -1
  22. package/dist/adapters/python/index.d.ts +5 -8
  23. package/dist/adapters/python/index.d.ts.map +1 -1
  24. package/dist/adapters/python/index.js +240 -109
  25. package/dist/adapters/python/index.js.map +1 -1
  26. package/dist/adapters/ruby/index.d.ts +3 -3
  27. package/dist/adapters/ruby/index.d.ts.map +1 -1
  28. package/dist/adapters/ruby/index.js +235 -46
  29. package/dist/adapters/ruby/index.js.map +1 -1
  30. package/dist/adapters/rust/index.d.ts +5 -9
  31. package/dist/adapters/rust/index.d.ts.map +1 -1
  32. package/dist/adapters/rust/index.js +237 -169
  33. package/dist/adapters/rust/index.js.map +1 -1
  34. package/dist/adapters/shared.d.ts +7 -0
  35. package/dist/adapters/shared.d.ts.map +1 -0
  36. package/dist/adapters/shared.js +68 -0
  37. package/dist/adapters/shared.js.map +1 -0
  38. package/dist/adapters/type-utils.d.ts +13 -0
  39. package/dist/adapters/type-utils.d.ts.map +1 -0
  40. package/dist/adapters/type-utils.js +105 -0
  41. package/dist/adapters/type-utils.js.map +1 -0
  42. package/dist/adapters/types.d.ts.map +1 -1
  43. package/dist/adapters/typescript/emitter/emitSchema.d.ts +2 -2
  44. package/dist/adapters/typescript/emitter/emitSchema.js +39 -27
  45. package/dist/adapters/typescript/emitter/emitSchema.js.map +1 -1
  46. package/dist/adapters/typescript/emitter/emitTypeScriptModels.d.ts +2 -2
  47. package/dist/adapters/typescript/emitter/emitTypeScriptModels.js +15 -15
  48. package/dist/adapters/typescript/emitter/importCollector.d.ts +2 -4
  49. package/dist/adapters/typescript/emitter/importCollector.d.ts.map +1 -1
  50. package/dist/adapters/typescript/emitter/importCollector.js +40 -50
  51. package/dist/adapters/typescript/emitter/importCollector.js.map +1 -1
  52. package/dist/adapters/typescript/emitter/parsers.d.ts +1 -1
  53. package/dist/adapters/typescript/emitter/parsers.js +13 -13
  54. package/dist/adapters/typescript/emitter/typeHoister.d.ts +1 -1
  55. package/dist/adapters/typescript/emitter/typeHoister.d.ts.map +1 -1
  56. package/dist/adapters/typescript/emitter/typeHoister.js +12 -20
  57. package/dist/adapters/typescript/emitter/typeHoister.js.map +1 -1
  58. package/dist/adapters/typescript/index.d.ts +3 -3
  59. package/dist/adapters/typescript/index.d.ts.map +1 -1
  60. package/dist/adapters/typescript/index.js +148 -166
  61. package/dist/adapters/typescript/index.js.map +1 -1
  62. package/dist/adapters/typescript/shared.d.ts +2 -2
  63. package/dist/adapters/typescript/shared.js +12 -12
  64. package/dist/cli.js +50 -37
  65. package/dist/cli.js.map +1 -1
  66. package/dist/collector.d.ts +32 -7
  67. package/dist/collector.d.ts.map +1 -1
  68. package/dist/collector.js +149 -49
  69. package/dist/collector.js.map +1 -1
  70. package/dist/elysia.js +2 -2
  71. package/dist/elysia.js.map +1 -1
  72. package/dist/express.d.ts.map +1 -1
  73. package/dist/express.js.map +1 -1
  74. package/dist/fastify.d.ts +9 -2
  75. package/dist/fastify.d.ts.map +1 -1
  76. package/dist/fastify.js +40 -21
  77. package/dist/fastify.js.map +1 -1
  78. package/dist/helpers/handlebars.d.ts +7 -16
  79. package/dist/helpers/handlebars.d.ts.map +1 -1
  80. package/dist/helpers/handlebars.js +127 -177
  81. package/dist/helpers/handlebars.js.map +1 -1
  82. package/dist/hono.d.ts.map +1 -1
  83. package/dist/hono.js.map +1 -1
  84. package/dist/index.d.ts +14 -12
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +32 -30
  87. package/dist/index.js.map +1 -1
  88. package/dist/koa.d.ts.map +1 -1
  89. package/dist/koa.js.map +1 -1
  90. package/dist/nestjs.d.ts.map +1 -1
  91. package/dist/nestjs.js +41 -14
  92. package/dist/nestjs.js.map +1 -1
  93. package/dist/pipeline/enrich.d.ts +2 -13
  94. package/dist/pipeline/enrich.d.ts.map +1 -1
  95. package/dist/pipeline/enrich.js +157 -212
  96. package/dist/pipeline/enrich.js.map +1 -1
  97. package/dist/pipeline/intake.d.ts +3 -3
  98. package/dist/pipeline/intake.d.ts.map +1 -1
  99. package/dist/pipeline/intake.js +342 -238
  100. package/dist/pipeline/intake.js.map +1 -1
  101. package/dist/pipeline/write.d.ts +1 -14
  102. package/dist/pipeline/write.d.ts.map +1 -1
  103. package/dist/pipeline/write.js +73 -113
  104. package/dist/pipeline/write.js.map +1 -1
  105. package/dist/types/config.d.ts +14 -26
  106. package/dist/types/config.d.ts.map +1 -1
  107. package/dist/types/config.js +16 -12
  108. package/dist/types/config.js.map +1 -1
  109. package/dist/types/ir.d.ts +12 -8
  110. package/dist/types/ir.d.ts.map +1 -1
  111. package/dist/types/manifest.d.ts +10 -11
  112. package/dist/types/manifest.d.ts.map +1 -1
  113. package/dist/types/manifest.js +54 -14
  114. package/dist/types/manifest.js.map +1 -1
  115. package/dist/types/sdk-module.d.ts +2 -2
  116. package/dist/types/sdk-module.js +1 -1
  117. package/dist/utils/format.d.ts +1 -3
  118. package/dist/utils/format.d.ts.map +1 -1
  119. package/dist/utils/format.js +3 -10
  120. package/dist/utils/format.js.map +1 -1
  121. package/dist/utils/naming.d.ts.map +1 -1
  122. package/dist/utils/naming.js +174 -184
  123. package/dist/utils/naming.js.map +1 -1
  124. package/dist/utils/npm-token.d.ts +1 -17
  125. package/dist/utils/npm-token.d.ts.map +1 -1
  126. package/dist/utils/npm-token.js +27 -76
  127. package/dist/utils/npm-token.js.map +1 -1
  128. package/dist/utils/version.d.ts +14 -0
  129. package/dist/utils/version.d.ts.map +1 -0
  130. package/dist/utils/version.js +60 -0
  131. package/dist/utils/version.js.map +1 -0
  132. package/package.json +4 -3
  133. package/templates/csharp/client.hbs +26 -49
  134. package/templates/csharp/csproj.hbs +7 -0
  135. package/templates/csharp/domain.hbs +48 -130
  136. package/templates/csharp/errors.hbs +17 -25
  137. package/templates/csharp/readme.hbs +31 -44
  138. package/templates/csharp/transport.hbs +238 -136
  139. package/templates/csharp/types.hbs +30 -76
  140. package/templates/go/client.hbs +44 -109
  141. package/templates/go/domain.hbs +83 -25
  142. package/templates/go/go-mod.hbs +1 -4
  143. package/templates/go/readme.hbs +32 -29
  144. package/templates/go/transport.hbs +174 -173
  145. package/templates/go/types.hbs +17 -53
  146. package/templates/go/utils.hbs +3 -2
  147. package/templates/kotlin/build-gradle.hbs +1 -1
  148. package/templates/kotlin/client.hbs +18 -15
  149. package/templates/kotlin/domain.hbs +54 -51
  150. package/templates/kotlin/errors.hbs +1 -1
  151. package/templates/kotlin/readme.hbs +35 -137
  152. package/templates/kotlin/transport.hbs +140 -123
  153. package/templates/kotlin/types.hbs +30 -81
  154. package/templates/php/client.hbs +8 -46
  155. package/templates/php/composer.hbs +2 -1
  156. package/templates/php/domain.hbs +86 -117
  157. package/templates/php/errors.hbs +1 -1
  158. package/templates/php/readme.hbs +42 -49
  159. package/templates/php/transport.hbs +192 -145
  160. package/templates/php/types.hbs +63 -60
  161. package/templates/python/client.hbs +15 -2
  162. package/templates/python/domain.hbs +50 -108
  163. package/templates/python/pyproject.hbs +5 -0
  164. package/templates/python/readme.hbs +47 -30
  165. package/templates/python/resources-init.hbs +1 -1
  166. package/templates/python/transport.hbs +167 -53
  167. package/templates/python/types.hbs +22 -58
  168. package/templates/ruby/client.hbs +10 -6
  169. package/templates/ruby/domain.hbs +48 -67
  170. package/templates/ruby/errors.hbs +3 -3
  171. package/templates/ruby/gemspec.hbs +3 -10
  172. package/templates/ruby/index.hbs +3 -3
  173. package/templates/ruby/readme.hbs +33 -70
  174. package/templates/ruby/transport.hbs +129 -103
  175. package/templates/ruby/types.hbs +11 -16
  176. package/templates/ruby/version.hbs +1 -1
  177. package/templates/rust/cargo.hbs +2 -0
  178. package/templates/rust/client.hbs +10 -1
  179. package/templates/rust/domain.hbs +69 -52
  180. package/templates/rust/error.hbs +3 -1
  181. package/templates/rust/lib.hbs +4 -5
  182. package/templates/rust/readme.hbs +33 -46
  183. package/templates/rust/transport.hbs +170 -159
  184. package/templates/rust/types.hbs +34 -111
  185. package/templates/typescript/domain-class.hbs +14 -6
  186. package/templates/typescript/domain.hbs +13 -5
  187. package/templates/typescript/package-json.hbs +3 -1
  188. package/templates/typescript/readme.hbs +42 -42
  189. package/templates/typescript/transport.hbs +46 -8
  190. package/templates/typescript/types-errors.hbs +1 -1
  191. package/dist/adapters/typescript/model-emitter.d.ts +0 -13
  192. package/dist/adapters/typescript/model-emitter.d.ts.map +0 -1
  193. package/dist/adapters/typescript/model-emitter.js +0 -415
  194. package/dist/adapters/typescript/model-emitter.js.map +0 -1
@@ -10,6 +10,7 @@ declare(strict_types=1);
10
10
 
11
11
  namespace {{namespace}};
12
12
 
13
+ use DateTimeImmutable;
13
14
  use GuzzleHttp\Client as GuzzleClient;
14
15
  use GuzzleHttp\ClientInterface;
15
16
  use GuzzleHttp\Exception\ConnectException;
@@ -19,15 +20,11 @@ use GuzzleHttp\RequestOptions;
19
20
  use Psr\Http\Message\ResponseInterface;
20
21
  use Throwable;
21
22
 
22
- /**
23
- * HTTP transport layer with retry logic and security scheme injection.
24
- *
25
- * Wraps a GuzzleHttp\Client and provides executeRequest() for typed
26
- * request/response handling with automatic auth header injection
27
- * and configurable retry with exponential backoff + jitter.
28
- */
29
23
  class Transport
30
24
  {
25
+ private const RETRYABLE_METHODS = ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE'];
26
+ private const RETRYABLE_STATUSES = [408, 425, 429, 500, 502, 503, 504];
27
+
31
28
  private ClientInterface $http;
32
29
 
33
30
  /** @var array<string, mixed> */
@@ -38,6 +35,7 @@ class Transport
38
35
  * baseUrl?: string,
39
36
  * timeoutMs?: int,
40
37
  * retries?: array{attempts?: int, backoffMs?: int, jitter?: bool, maxBackoffMs?: int},
38
+ * retryUnsafeRequests?: bool,
41
39
  * defaultHeaders?: array<string, string>,
42
40
  {{#each securitySchemes}}
43
41
  * {{configKey}}?: string,
@@ -46,14 +44,16 @@ class Transport
46
44
  */
47
45
  public function __construct(array $config = [])
48
46
  {
49
- $this->config = $config;
50
-
51
- $baseUrl = rtrim($config['baseUrl'] ?? '{{baseUrl}}', '/');
52
- $timeoutMs = $config['timeoutMs'] ?? 15000;
47
+ $timeoutMs = (int) ($config['timeoutMs'] ?? 15000);
48
+ if ($timeoutMs <= 0) {
49
+ throw new \InvalidArgumentException('timeoutMs must be greater than zero');
50
+ }
53
51
 
52
+ $this->config = $config;
53
+ $baseUrl = rtrim((string) ($config['baseUrl'] ?? '{{baseUrl}}'), '/');
54
54
  $headers = $config['defaultHeaders'] ?? [];
55
55
  $headers['Accept'] = $headers['Accept'] ?? 'application/json';
56
- $headers['User-Agent'] = $headers['User-Agent'] ?? '{{clientName}}-php-sdk/{{version}}';
56
+ $headers['User-Agent'] = $headers['User-Agent'] ?? '{{clientName}}-php-sdk/{{packageVersion}}';
57
57
 
58
58
  $this->http = new GuzzleClient([
59
59
  'base_uri' => $baseUrl . '/',
@@ -64,217 +64,264 @@ class Transport
64
64
  }
65
65
 
66
66
  /**
67
- * Execute an HTTP request with retry logic and auth injection.
68
- *
69
67
  * @param array{
70
68
  * method: string,
71
69
  * path: string,
72
70
  * query?: array<string, mixed>,
73
71
  * body?: mixed,
74
72
  * headers?: array<string, string>,
73
+ * contentType?: string,
75
74
  * timeoutMs?: int,
75
+ * retries?: int,
76
+ * retryUnsafeRequests?: bool,
77
+ * stream?: bool,
76
78
  * } $requestDef
77
- * @return ResponseInterface
78
79
  * @throws SDKError
79
80
  */
80
81
  public function executeRequest(array $requestDef): ResponseInterface
81
82
  {
82
- $method = strtoupper($requestDef['method']);
83
- $path = ltrim($requestDef['path'], '/');
83
+ $method = strtoupper((string) $requestDef['method']);
84
+ $path = ltrim((string) $requestDef['path'], '/');
84
85
  $query = $requestDef['query'] ?? [];
85
86
  $body = $requestDef['body'] ?? null;
86
- $extraHeaders = $requestDef['headers'] ?? [];
87
- $timeoutMs = $requestDef['timeoutMs'] ?? null;
88
-
89
- // ── Inject auth headers ────────────────────────────────────────────
90
- $authHeaders = $this->buildAuthHeaders();
91
-
92
- $options = [
93
- RequestOptions::HEADERS => array_merge($authHeaders, $extraHeaders),
94
- ];
87
+ $headers = array_merge($this->buildAuthHeaders(), $requestDef['headers'] ?? []);
88
+ $contentType = (string) ($requestDef['contentType'] ?? 'application/json');
95
89
 
96
- if (!empty($query)) {
90
+ $options = [RequestOptions::HEADERS => $headers];
91
+ if ($query !== []) {
97
92
  $options[RequestOptions::QUERY] = $query;
98
93
  }
99
-
100
94
  if ($body !== null) {
101
- $options[RequestOptions::JSON] = $body;
95
+ if ($contentType === 'application/json') {
96
+ $options[RequestOptions::JSON] = $body;
97
+ } elseif ($contentType === 'multipart/form-data' && is_array($body)) {
98
+ $options[RequestOptions::MULTIPART] = $body;
99
+ } else {
100
+ $options[RequestOptions::BODY] = $body;
101
+ $options[RequestOptions::HEADERS]['Content-Type'] = $contentType;
102
+ }
102
103
  }
103
-
104
- if ($timeoutMs !== null) {
104
+ if (($requestDef['stream'] ?? false) === true) {
105
+ $options[RequestOptions::STREAM] = true;
106
+ }
107
+ if (isset($requestDef['timeoutMs'])) {
108
+ $timeoutMs = (int) $requestDef['timeoutMs'];
109
+ if ($timeoutMs <= 0) {
110
+ throw new \InvalidArgumentException('timeoutMs must be greater than zero');
111
+ }
105
112
  $options[RequestOptions::TIMEOUT] = $timeoutMs / 1000.0;
106
113
  }
107
114
 
108
- // ── Execute with retry ─────────────────────────────────────────────
109
- return $this->executeWithRetry($method, $path, $options);
115
+ $retryConfig = $this->config['retries'] ?? [];
116
+ $maxRetries = max(0, (int) ($requestDef['retries'] ?? $retryConfig['attempts'] ?? 3));
117
+ $retryUnsafe = (bool) (
118
+ $requestDef['retryUnsafeRequests']
119
+ ?? $this->config['retryUnsafeRequests']
120
+ ?? false
121
+ );
122
+
123
+ return $this->executeWithRetry(
124
+ $method,
125
+ $path,
126
+ $options,
127
+ $maxRetries,
128
+ $retryUnsafe,
129
+ );
110
130
  }
111
131
 
112
- /**
113
- * Execute with configurable retry logic.
114
- */
115
- private function executeWithRetry(string $method, string $path, array $options): ResponseInterface
116
- {
132
+ /** @throws SDKError */
133
+ private function executeWithRetry(
134
+ string $method,
135
+ string $path,
136
+ array $options,
137
+ int $maxRetries,
138
+ bool $retryUnsafe,
139
+ ): ResponseInterface {
117
140
  $retryConfig = $this->config['retries'] ?? [];
118
- $maxAttempts = $retryConfig['attempts'] ?? 3;
119
- $baseBackoffMs = $retryConfig['backoffMs'] ?? 400;
120
- $useJitter = $retryConfig['jitter'] ?? true;
121
- $maxBackoffMs = $retryConfig['maxBackoffMs'] ?? 30000;
122
-
123
- $lastException = null;
141
+ $baseBackoffMs = max(0, (int) ($retryConfig['backoffMs'] ?? 400));
142
+ $useJitter = (bool) ($retryConfig['jitter'] ?? true);
143
+ $maxBackoffMs = max($baseBackoffMs, (int) ($retryConfig['maxBackoffMs'] ?? 30000));
144
+ $canRetry = $this->canRetryMethod($method, $options[RequestOptions::HEADERS] ?? [], $retryUnsafe);
145
+ $lastError = null;
124
146
 
125
- for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
147
+ for ($attempt = 0; $attempt <= $maxRetries; $attempt++) {
126
148
  try {
127
149
  $request = new Request($method, $path, $options[RequestOptions::HEADERS] ?? []);
128
150
  $response = $this->http->send($request, $options);
129
-
130
- // Retry on server errors (5xx) and 429 (rate limit)
131
151
  $status = $response->getStatusCode();
132
- if ($status < 500 && $status !== 429) {
152
+
153
+ if ($status >= 200 && $status < 300) {
133
154
  return $response;
134
155
  }
135
156
 
136
- if ($attempt < $maxAttempts) {
137
- $delayMs = $this->calculateBackoff($attempt, $baseBackoffMs, $useJitter, $maxBackoffMs);
138
- $this->sleep($delayMs);
157
+ $lastError = $this->errorFromResponse($response);
158
+ if (!$canRetry || !in_array($status, self::RETRYABLE_STATUSES, true) || $attempt >= $maxRetries) {
159
+ throw $lastError;
139
160
  }
140
- } catch (ConnectException $e) {
141
- $lastException = $e;
142
161
 
143
- if ($attempt < $maxAttempts) {
144
- $delayMs = $this->calculateBackoff($attempt, $baseBackoffMs, $useJitter, $maxBackoffMs);
145
- $this->sleep($delayMs);
162
+ $this->sleep($this->retryDelayMs(
163
+ $response->getHeaderLine('Retry-After'),
164
+ $attempt,
165
+ $baseBackoffMs,
166
+ $useJitter,
167
+ $maxBackoffMs,
168
+ ));
169
+ } catch (ConnectException $exception) {
170
+ $lastError = SDKError::fromException($exception);
171
+ if (!$canRetry || $attempt >= $maxRetries) {
172
+ throw $lastError;
146
173
  }
147
- } catch (RequestException $e) {
148
- $response = $e->getResponse();
149
- if ($response !== null) {
150
- $status = $response->getStatusCode();
151
- if ($status >= 500 || $status === 429) {
152
- $lastException = $e;
153
- if ($attempt < $maxAttempts) {
154
- $delayMs = $this->calculateBackoff($attempt, $baseBackoffMs, $useJitter, $maxBackoffMs);
155
- $this->sleep($delayMs);
156
- }
157
- continue;
158
- }
174
+ $this->sleep($this->retryDelayMs(
175
+ null,
176
+ $attempt,
177
+ $baseBackoffMs,
178
+ $useJitter,
179
+ $maxBackoffMs,
180
+ ));
181
+ } catch (RequestException $exception) {
182
+ $response = $exception->getResponse();
183
+ $lastError = $response === null
184
+ ? SDKError::fromException($exception)
185
+ : $this->errorFromResponse($response, $exception);
186
+ if (
187
+ !$canRetry
188
+ || $attempt >= $maxRetries
189
+ || ($response !== null && !in_array($response->getStatusCode(), self::RETRYABLE_STATUSES, true))
190
+ ) {
191
+ throw $lastError;
159
192
  }
160
- throw $this->mapToSDKError($e);
193
+ $this->sleep($this->retryDelayMs(
194
+ $response?->getHeaderLine('Retry-After'),
195
+ $attempt,
196
+ $baseBackoffMs,
197
+ $useJitter,
198
+ $maxBackoffMs,
199
+ ));
161
200
  }
162
201
  }
163
202
 
164
- // All retries exhausted
165
- if ($lastException instanceof RequestException) {
166
- throw $this->mapToSDKError($lastException);
203
+ throw $lastError ?? new SDKError('Request failed after retries', 0, 'RETRY_EXHAUSTED');
204
+ }
205
+
206
+ /** @param array<string, string> $headers */
207
+ private function canRetryMethod(string $method, array $headers, bool $retryUnsafe): bool
208
+ {
209
+ if ($retryUnsafe || in_array($method, self::RETRYABLE_METHODS, true)) {
210
+ return true;
211
+ }
212
+ $effectiveHeaders = array_merge($this->config['defaultHeaders'] ?? [], $headers);
213
+ foreach ($effectiveHeaders as $name => $value) {
214
+ if (strcasecmp((string) $name, 'X-Idempotency-Key') === 0 && $value !== '') {
215
+ return true;
216
+ }
167
217
  }
218
+ return false;
219
+ }
168
220
 
169
- if ($lastException instanceof Throwable) {
170
- throw SDKError::fromException($lastException);
221
+ private function retryDelayMs(
222
+ ?string $retryAfter,
223
+ int $attempt,
224
+ int $baseMs,
225
+ bool $jitter,
226
+ int $maxMs,
227
+ ): int {
228
+ $retryAfterMs = $this->parseRetryAfterMs($retryAfter);
229
+ if ($retryAfterMs !== null) {
230
+ return min($retryAfterMs, $maxMs);
171
231
  }
172
232
 
173
- throw new SDKError('Request failed after ' . $maxAttempts . ' attempts');
233
+ $cap = min($baseMs * (2 ** $attempt), $maxMs);
234
+ return $jitter ? random_int(0, max(0, $cap)) : $cap;
174
235
  }
175
236
 
176
- /**
177
- * Build auth headers from configured security schemes.
178
- *
179
- * @return array<string, string>
180
- */
237
+ private function parseRetryAfterMs(?string $value): ?int
238
+ {
239
+ if ($value === null || trim($value) === '') {
240
+ return null;
241
+ }
242
+ if (is_numeric($value)) {
243
+ return max(0, (int) ceil((float) $value * 1000));
244
+ }
245
+ try {
246
+ $time = new DateTimeImmutable($value);
247
+ return max(0, ($time->getTimestamp() - time()) * 1000);
248
+ } catch (Throwable) {
249
+ return null;
250
+ }
251
+ }
252
+
253
+ /** @return array<string, string> */
181
254
  private function buildAuthHeaders(): array
182
255
  {
183
256
  $headers = [];
184
-
185
- {{#each securitySchemes}}
186
- {{#if (eq type "apiKey")}}
187
- {{#if (eq in "header")}}
188
- // {{name}} — API key in header
257
+ {{#each securitySchemes}}
258
+ {{#if (eq type "apiKey")}}
259
+ {{#if (eq in "header")}}
189
260
  if (!empty($this->config['{{configKey}}'])) {
190
261
  $headers['{{paramName}}'] = $this->config['{{configKey}}'];
191
262
  }
192
- {{/if}}
193
- {{/if}}
194
-
195
- {{#if (and (eq type "http") (eq scheme "bearer"))}}
196
- // {{name}} — Bearer token
263
+ {{/if}}
264
+ {{/if}}
265
+ {{#if (and (eq type "http") (eq scheme "bearer"))}}
197
266
  if (!empty($this->config['{{configKey}}'])) {
198
267
  $headers['Authorization'] = 'Bearer ' . $this->config['{{configKey}}'];
199
268
  }
200
- {{/if}}
201
-
202
- {{#if (eq type "oauth2")}}
203
- // {{name}} — OAuth2 access token
269
+ {{/if}}
270
+ {{#if (eq type "oauth2")}}
204
271
  if (!empty($this->config['{{configKey}}'])) {
205
272
  $headers['Authorization'] = 'Bearer ' . $this->config['{{configKey}}'];
206
273
  }
207
- {{/if}}
208
-
209
- {{#if (eq type "openIdConnect")}}
210
- // {{name}} — OpenID Connect token
274
+ {{/if}}
275
+ {{#if (eq type "openIdConnect")}}
211
276
  if (!empty($this->config['{{configKey}}'])) {
212
277
  $headers['Authorization'] = 'Bearer ' . $this->config['{{configKey}}'];
213
278
  }
214
- {{/if}}
215
- {{/each}}
216
-
279
+ {{/if}}
280
+ {{/each}}
217
281
  return $headers;
218
282
  }
219
283
 
220
- /**
221
- * Calculate exponential backoff with optional jitter.
222
- */
223
- private function calculateBackoff(int $attempt, int $baseMs, bool $jitter, int $maxMs): int
224
- {
225
- $delay = $baseMs * (2 ** ($attempt - 1));
226
-
227
- if ($jitter) {
228
- $delay = (int) ($delay * (0.5 + (mt_rand() / mt_getrandmax()) * 0.5));
229
- }
230
-
231
- return min($delay, $maxMs);
232
- }
233
-
234
- /**
235
- * Sleep for a given number of milliseconds.
236
- */
237
284
  private function sleep(int $milliseconds): void
238
285
  {
239
- usleep($milliseconds * 1000);
240
- }
241
-
242
- /**
243
- * Map a Guzzle RequestException to an SDKError.
244
- */
245
- private function mapToSDKError(RequestException $exception): SDKError
246
- {
247
- $response = $exception->getResponse();
248
-
249
- if ($response === null) {
250
- return SDKError::fromException($exception);
286
+ if ($milliseconds > 0) {
287
+ usleep($milliseconds * 1000);
251
288
  }
289
+ }
252
290
 
253
- $status = $response->getStatusCode();
291
+ private function errorFromResponse(
292
+ ResponseInterface $response,
293
+ ?Throwable $previous = null,
294
+ ): SDKError {
254
295
  $body = $this->decodeResponseBody($response);
255
-
256
- $message = $body['message'] ?? $body['error'] ?? $exception->getMessage();
257
- $code = $body['code'] ?? $body['error_code'] ?? 'UNKNOWN';
258
- $requestId = $response->getHeaderLine('x-request-id') ?: null;
296
+ $nested = is_array($body['error'] ?? null) ? $body['error'] : [];
297
+ $message = $body['message'] ?? $nested['message'] ?? null;
298
+ if ($message === null && is_string($body['error'] ?? null)) {
299
+ $message = $body['error'];
300
+ }
301
+ $message ??= trim((string) ($body['raw'] ?? ''));
302
+ $code = $body['code'] ?? $body['error_code'] ?? $nested['code'] ?? null;
303
+ if ($code === null && is_string($body['error'] ?? null)) {
304
+ $code = $body['error'];
305
+ }
306
+ $requestId = $body['request_id']
307
+ ?? $response->getHeaderLine('x-request-id')
308
+ ?: $response->getHeaderLine('request-id')
309
+ ?: null;
259
310
 
260
311
  return new SDKError(
261
- message: (string) $message,
262
- status: $status,
263
- code: (string) $code,
312
+ message: (string) ($message ?: 'HTTP ' . $response->getStatusCode()),
313
+ status: $response->getStatusCode(),
314
+ code: (string) ($code ?: 'HTTP_ERROR'),
264
315
  requestId: $requestId,
265
- previous: $exception,
316
+ responseBody: $body,
317
+ previous: $previous,
266
318
  );
267
319
  }
268
320
 
269
- /**
270
- * Decode a PSR-7 response body, returning an associative array.
271
- *
272
- * @return array<string, mixed>
273
- */
321
+ /** @return array<string, mixed> */
274
322
  private function decodeResponseBody(ResponseInterface $response): array
275
323
  {
276
324
  $contents = $response->getBody()->__toString();
277
-
278
325
  try {
279
326
  $decoded = json_decode($contents, true, 512, JSON_THROW_ON_ERROR);
280
327
  return is_array($decoded) ? $decoded : ['raw' => $decoded];
@@ -1,92 +1,95 @@
1
1
  <?php
2
2
 
3
- /**
4
- * @generated
5
- * This file was automatically generated by Tesseract.
6
- * DO NOT MODIFY THIS FILE DIRECTLY.
7
- */
8
-
9
3
  declare(strict_types=1);
10
4
 
11
5
  namespace {{namespace}};
12
6
 
13
7
  {{#each schemas}}
14
- {{#if isEnum}}
15
- /**
16
- {{#if description}}
17
- * {{description}}
18
- *
19
- {{/if}}
20
- * @generated
21
- */
22
- enum {{name}}: {{#each properties}}{{#if @first}}{{type}}{{/if}}{{/each}}
8
+ {{#if external}}
9
+ /** External schema placeholder for {{name}}. */
10
+ class {{targetName}}
11
+ {
12
+ /** @param array<string, mixed> $data */
13
+ public function __construct(public array $data = [])
14
+ {
15
+ }
16
+
17
+ /** @param array<string, mixed> $data */
18
+ public static function fromArray(array $data): self
19
+ {
20
+ return new self($data);
21
+ }
22
+
23
+ /** @return array<string, mixed> */
24
+ public function toArray(): array
25
+ {
26
+ return $this->data;
27
+ }
28
+ }
29
+
30
+ {{else if isEnum}}
31
+ enum {{targetName}}: {{enumType}}
23
32
  {
24
33
  {{#each enumValues}}
25
- case {{this}} = '{{this}}';
34
+ case {{name}} = {{{literal}}};
26
35
  {{/each}}
27
36
  }
28
37
 
29
- {{else if isTypeAlias}}
30
- {{!-- PHP type aliases are not direct; they are used as docblock hints --}}
31
- {{!-- Skipping type alias: {{name}} --}}
32
-
33
38
  {{else if isUnionType}}
34
- {{!-- PHP union types are handled via properties, not standalone declarations --}}
35
- {{!-- Skipping union type: {{name}} --}}
39
+ /** Union schema {{name}}. */
40
+ final class {{targetName}}
41
+ {
42
+ public function __construct(public mixed $value)
43
+ {
44
+ }
45
+ }
46
+
47
+ {{else if isIntersectionType}}
48
+ /** Intersection schema {{name}}. */
49
+ class {{targetName}}
50
+ {
51
+ /** @param array<string, mixed> $data */
52
+ public function __construct(public array $data = [])
53
+ {
54
+ }
55
+ }
56
+
57
+ {{else if isTypeAlias}}
58
+ // Type alias {{name}} is represented inline as {{{aliasType}}}.
36
59
 
37
60
  {{else}}
38
- /**
39
- {{#if description}}
40
- * {{description}}
41
- *
42
- {{/if}}
43
- * @generated
61
+ /**{{#if description}}
62
+ * {{description}}{{/if}}
44
63
  */
45
- class {{name}}{{#if extends}} extends {{extends}}{{/if}}
64
+ class {{targetName}}{{#if targetExtends}} extends {{targetExtends}}{{/if}}
46
65
  {
47
66
  {{#each properties}}
48
- /**
49
- * @var {{tsType type nullable}}
50
- */
51
- public {{#if nullable}}?{{/if}}{{type}} ${{name}}{{#if defaultValue}} = {{{json defaultValue}}}{{/if}};
52
-
67
+ /** @var {{docType}} */
68
+ public {{targetType}} ${{propertyName}}{{#if hasDefault}} = {{{defaultLiteral}}}{{else if optional}} = null{{/if}};
53
69
  {{/each}}
54
70
 
55
- /**
56
- * @param array<string, mixed> $data
57
- */
71
+ /** @param array<string, mixed> $data */
58
72
  public static function fromArray(array $data): self
59
73
  {
60
74
  $instance = new self();
61
-
62
- {{#each properties}}
63
- {{#if (eq type "string")}}
64
- $instance->{{name}} = isset($data['{{name}}']) ? (string) $data['{{name}}'] : {{#if required}}throw new \InvalidArgumentException('Missing required field: {{name}}'){{else}}null{{/if}};
65
- {{else if (eq type "int")}}
66
- $instance->{{name}} = isset($data['{{name}}']) ? (int) $data['{{name}}'] : {{#if required}}throw new \InvalidArgumentException('Missing required field: {{name}}'){{else}}null{{/if}};
67
- {{else if (eq type "float")}}
68
- $instance->{{name}} = isset($data['{{name}}']) ? (float) $data['{{name}}'] : {{#if required}}throw new \InvalidArgumentException('Missing required field: {{name}}'){{else}}null{{/if}};
69
- {{else if (eq type "bool")}}
70
- $instance->{{name}} = isset($data['{{name}}']) ? (bool) $data['{{name}}'] : {{#if required}}throw new \InvalidArgumentException('Missing required field: {{name}}'){{else}}null{{/if}};
71
- {{else if (eq type "array")}}
72
- $instance->{{name}} = isset($data['{{name}}']) ? (array) $data['{{name}}'] : {{#if required}}[]{{else}}null{{/if}};
73
- {{else}}
74
- $instance->{{name}} = $data['{{name}}'] ?? {{#if required}}throw new \InvalidArgumentException('Missing required field: {{name}}'){{else}}null{{/if}};
75
- {{/if}}
76
-
77
- {{/each}}
75
+ {{#each properties}}
76
+ {{#if required}}
77
+ if (!array_key_exists('{{name}}', $data)) {
78
+ throw new \InvalidArgumentException('Missing required field: {{name}}');
79
+ }
80
+ {{/if}}
81
+ $instance->{{propertyName}} = $data['{{name}}'] ?? {{#if hasDefault}}{{{defaultLiteral}}}{{else}}null{{/if}};
82
+ {{/each}}
78
83
  return $instance;
79
84
  }
80
85
 
81
- /**
82
- * @return array<string, mixed>
83
- */
86
+ /** @return array<string, mixed> */
84
87
  public function toArray(): array
85
88
  {
86
89
  return [
87
- {{#each properties}}
88
- '{{name}}' => $this->{{name}},
89
- {{/each}}
90
+ {{#each properties}}
91
+ '{{name}}' => $this->{{propertyName}},
92
+ {{/each}}
90
93
  ];
91
94
  }
92
95
  }
@@ -4,7 +4,7 @@ from typing import Optional, Dict, Any, List, Callable, Union
4
4
  import httpx
5
5
  from .transport import create_transport, SDKTransport, RequestOptions
6
6
  {{#each groups}}
7
- from .resources.{{fileName}} import {{interfaceName}}
7
+ from .resources.{{fileModule}} import {{interfaceName}}
8
8
  {{/each}}
9
9
 
10
10
  {{#if hasSSE}}
@@ -34,6 +34,7 @@ class {{clientName}}ClientConfig:
34
34
  {{/each}}
35
35
  timeout_ms: int = 15000,
36
36
  retries: int = 3,
37
+ retry_unsafe_requests: bool = False,
37
38
  default_headers: Optional[Dict[str, str]] = None,
38
39
  on_request: Optional[Callable] = None,
39
40
  on_response: Optional[Callable] = None,
@@ -45,6 +46,7 @@ class {{clientName}}ClientConfig:
45
46
  {{/each}}
46
47
  self.timeout_ms = timeout_ms
47
48
  self.retries = retries
49
+ self.retry_unsafe_requests = retry_unsafe_requests
48
50
  self.default_headers = default_headers or {}
49
51
  self.on_request = on_request
50
52
  self.on_response = on_response
@@ -63,15 +65,26 @@ class {{clientName}}Client:
63
65
  {{/each}}
64
66
  timeout_ms=self.config.timeout_ms,
65
67
  retries=self.config.retries,
68
+ retry_unsafe_requests=self.config.retry_unsafe_requests,
66
69
  default_headers=self.config.default_headers,
67
70
  on_request=self.config.on_request,
68
71
  on_response=self.config.on_response,
69
72
  on_error=self.config.on_error,
70
73
  )
71
74
  {{#each groups}}
72
- self.{{snakeCase name}} = {{interfaceName}}(self._transport)
75
+ self.{{accessorName}} = {{interfaceName}}(self._transport)
73
76
  {{/each}}
74
77
 
78
+ def close(self) -> None:
79
+ """Close the underlying HTTP connection pool."""
80
+ self._transport.close()
81
+
82
+ def __enter__(self) -> "{{clientName}}Client":
83
+ return self
84
+
85
+ def __exit__(self, *_: Any) -> None:
86
+ self.close()
87
+
75
88
 
76
89
  def create_{{snakeCase clientName}}_client(
77
90
  base_url: str = "{{baseUrl}}",