@apollo-deploy/tesseract 0.9.1 → 1.0.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 (110) hide show
  1. package/dist/adapters/csharp/index.d.ts +12 -0
  2. package/dist/adapters/csharp/index.d.ts.map +1 -0
  3. package/dist/adapters/csharp/index.js +99 -0
  4. package/dist/adapters/csharp/index.js.map +1 -0
  5. package/dist/adapters/go/index.d.ts +12 -0
  6. package/dist/adapters/go/index.d.ts.map +1 -0
  7. package/dist/adapters/go/index.js +135 -0
  8. package/dist/adapters/go/index.js.map +1 -0
  9. package/dist/adapters/kotlin/index.d.ts +13 -0
  10. package/dist/adapters/kotlin/index.d.ts.map +1 -0
  11. package/dist/adapters/kotlin/index.js +186 -0
  12. package/dist/adapters/kotlin/index.js.map +1 -0
  13. package/dist/adapters/php/index.d.ts +20 -0
  14. package/dist/adapters/php/index.d.ts.map +1 -0
  15. package/dist/adapters/php/index.js +123 -0
  16. package/dist/adapters/php/index.js.map +1 -0
  17. package/dist/adapters/python/index.d.ts +12 -0
  18. package/dist/adapters/python/index.d.ts.map +1 -0
  19. package/dist/adapters/python/index.js +145 -0
  20. package/dist/adapters/python/index.js.map +1 -0
  21. package/dist/adapters/ruby/index.d.ts +12 -0
  22. package/dist/adapters/ruby/index.d.ts.map +1 -0
  23. package/dist/adapters/ruby/index.js +86 -0
  24. package/dist/adapters/ruby/index.js.map +1 -0
  25. package/dist/adapters/rust/index.d.ts +13 -0
  26. package/dist/adapters/rust/index.d.ts.map +1 -0
  27. package/dist/adapters/rust/index.js +213 -0
  28. package/dist/adapters/rust/index.js.map +1 -0
  29. package/dist/adapters/typescript/emitter/emitSchema.d.ts +1 -1
  30. package/dist/adapters/typescript/emitter/emitSchema.d.ts.map +1 -1
  31. package/dist/adapters/typescript/emitter/emitSchema.js +1 -1
  32. package/dist/adapters/typescript/emitter/emitSchema.js.map +1 -1
  33. package/dist/adapters/typescript/emitter/emitTypeScriptModels.d.ts +2 -2
  34. package/dist/adapters/typescript/emitter/emitTypeScriptModels.d.ts.map +1 -1
  35. package/dist/adapters/typescript/emitter/emitTypeScriptModels.js +2 -2
  36. package/dist/adapters/typescript/emitter/emitTypeScriptModels.js.map +1 -1
  37. package/dist/adapters/typescript/emitter/importCollector.d.ts +1 -1
  38. package/dist/adapters/typescript/emitter/importCollector.d.ts.map +1 -1
  39. package/dist/cli.js +2 -1
  40. package/dist/cli.js.map +1 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +14 -0
  43. package/dist/index.js.map +1 -1
  44. package/dist/pipeline/intake.js +23 -16
  45. package/dist/pipeline/intake.js.map +1 -1
  46. package/dist/types/ir.d.ts +1 -1
  47. package/dist/types/ir.d.ts.map +1 -1
  48. package/dist/types/sdk-module.d.ts +22 -2
  49. package/dist/types/sdk-module.d.ts.map +1 -1
  50. package/dist/types/sdk-module.js +1 -1
  51. package/package.json +1 -1
  52. package/templates/csharp/client.hbs +95 -0
  53. package/templates/csharp/csproj.hbs +18 -0
  54. package/templates/csharp/domain.hbs +183 -0
  55. package/templates/csharp/errors.hbs +45 -0
  56. package/templates/csharp/readme.hbs +76 -0
  57. package/templates/csharp/transport.hbs +291 -0
  58. package/templates/csharp/types.hbs +104 -0
  59. package/templates/go/client.hbs +158 -0
  60. package/templates/go/domain.hbs +89 -0
  61. package/templates/go/errors.hbs +38 -0
  62. package/templates/go/go-mod.hbs +6 -0
  63. package/templates/go/readme.hbs +73 -0
  64. package/templates/go/transport.hbs +379 -0
  65. package/templates/go/types.hbs +80 -0
  66. package/templates/go/utils.hbs +45 -0
  67. package/templates/kotlin/build-gradle.hbs +41 -0
  68. package/templates/kotlin/client.hbs +57 -0
  69. package/templates/kotlin/domain.hbs +73 -0
  70. package/templates/kotlin/errors.hbs +42 -0
  71. package/templates/kotlin/gradle-properties.hbs +2 -0
  72. package/templates/kotlin/readme.hbs +105 -0
  73. package/templates/kotlin/settings-gradle.hbs +1 -0
  74. package/templates/kotlin/transport.hbs +246 -0
  75. package/templates/kotlin/types.hbs +100 -0
  76. package/templates/php/client.hbs +74 -0
  77. package/templates/php/composer.hbs +19 -0
  78. package/templates/php/domain.hbs +173 -0
  79. package/templates/php/errors.hbs +131 -0
  80. package/templates/php/readme.hbs +89 -0
  81. package/templates/php/transport.hbs +285 -0
  82. package/templates/php/types.hbs +95 -0
  83. package/templates/python/client.hbs +102 -0
  84. package/templates/python/domain.hbs +139 -0
  85. package/templates/python/index.hbs +20 -0
  86. package/templates/python/pyproject.hbs +26 -0
  87. package/templates/python/readme.hbs +64 -0
  88. package/templates/python/resources-init.hbs +11 -0
  89. package/templates/python/transport.hbs +191 -0
  90. package/templates/python/types-errors.hbs +75 -0
  91. package/templates/python/types.hbs +79 -0
  92. package/templates/ruby/Gemfile.hbs +5 -0
  93. package/templates/ruby/client.hbs +86 -0
  94. package/templates/ruby/domain.hbs +110 -0
  95. package/templates/ruby/errors.hbs +150 -0
  96. package/templates/ruby/gemspec.hbs +30 -0
  97. package/templates/ruby/index.hbs +54 -0
  98. package/templates/ruby/readme.hbs +113 -0
  99. package/templates/ruby/transport.hbs +177 -0
  100. package/templates/ruby/types.hbs +126 -0
  101. package/templates/ruby/version.hbs +5 -0
  102. package/templates/rust/api-mod.hbs +3 -0
  103. package/templates/rust/cargo.hbs +22 -0
  104. package/templates/rust/client.hbs +114 -0
  105. package/templates/rust/domain.hbs +94 -0
  106. package/templates/rust/error.hbs +74 -0
  107. package/templates/rust/lib.hbs +85 -0
  108. package/templates/rust/readme.hbs +77 -0
  109. package/templates/rust/transport.hbs +287 -0
  110. package/templates/rust/types.hbs +142 -0
@@ -0,0 +1,173 @@
1
+ <?php
2
+
3
+ /**
4
+ * @generated
5
+ * This file was automatically generated by Tesseract.
6
+ * DO NOT MODIFY THIS FILE DIRECTLY.
7
+ */
8
+
9
+ declare(strict_types=1);
10
+
11
+ namespace {{namespace}}\Api;
12
+
13
+ use Psr\Http\Message\ResponseInterface;
14
+
15
+ /**
16
+ * {{interfaceName}} domain operations.
17
+ *
18
+ {{#if description}}
19
+ * {{description}}
20
+ *
21
+ {{/if}}
22
+ * @generated
23
+ */
24
+ class {{className}}
25
+ {
26
+ private Transport $transport;
27
+
28
+ public function __construct(Transport $transport)
29
+ {
30
+ $this->transport = $transport;
31
+ }
32
+
33
+ {{#each operations}}
34
+ {{#if (isEventStream this)}}
35
+ // SSE operations are not yet supported in the PHP adapter — skipping {{name}}
36
+
37
+ {{else}}
38
+ /**
39
+ {{#if deprecated}}
40
+ * @deprecated{{#if deprecationMessage}} {{deprecationMessage}}{{/if}}
41
+ *
42
+ {{/if}}
43
+ {{#if (or summary description)}}
44
+ * {{#if summary}}{{summary}}{{#if description}} — {{description}}{{/if}}{{else}}{{description}}{{/if}}
45
+ *
46
+ {{/if}}
47
+ {{#each pathParams}}
48
+ * @param {{type}} ${{name}}{{#if description}} {{description}}{{/if}}
49
+ {{/each}}
50
+ {{#if requestBody}}
51
+ * @param array<string, mixed>{{#unless requestBody.required}}|null{{/unless}} $input Request body
52
+ {{/if}}
53
+ {{#each queryParams}}
54
+ * @param {{type}}|null ${{name}}{{#if description}} {{description}}{{/if}}
55
+ {{/each}}
56
+ {{#each headerParams}}
57
+ * @param {{type}}|null ${{name}}{{#if description}} {{description}}{{/if}}
58
+ {{/each}}
59
+ * @return {{responseType}}
60
+ * @throws SDKError
61
+ */
62
+ public function {{name}}(
63
+ {{#each pathParams}}
64
+ {{type}} ${{name}},
65
+ {{/each}}
66
+ {{#if requestBody}}
67
+ {{#unless requestBody.required}}?{{/unless}}array $input{{#unless requestBody.required}} = null{{/unless}},
68
+ {{/if}}
69
+ {{#each queryParams}}
70
+ ?{{type}} ${{name}} = null,
71
+ {{/each}}
72
+ {{#each headerParams}}
73
+ ?{{type}} ${{name}} = null,
74
+ {{/each}}
75
+ ): {{responseType}}
76
+ {
77
+ // Build path with parameter substitution
78
+ $path = '{{path}}';
79
+ {{#if (hasPathParams this)}}
80
+ $path = str_replace(
81
+ [{{#each pathParams}}'{{@root.LBRACE}}{{originalName}}{{@root.RBRACE}}'{{#unless @last}}, {{/unless}}{{/each}}],
82
+ [{{#each pathParams}}${{name}}{{#unless @last}}, {{/unless}}{{/each}}],
83
+ $path
84
+ );
85
+ {{/if}}
86
+
87
+ // Build query parameters
88
+ $query = [];
89
+ {{#if (hasQueryParams this)}}
90
+ {{#each queryParams}}
91
+ if (${{name}} !== null) {
92
+ $query['{{originalName}}'] = ${{name}};
93
+ }
94
+ {{/each}}
95
+ {{/if}}
96
+
97
+ // Build extra headers
98
+ $headers = [];
99
+ {{#each headerParams}}
100
+ if (${{name}} !== null) {
101
+ $headers['{{originalName}}'] = (string) ${{name}};
102
+ }
103
+ {{/each}}
104
+
105
+ // Execute request
106
+ $requestDef = [
107
+ 'method' => '{{httpMethod}}',
108
+ 'path' => $path,
109
+ 'query' => $query,
110
+ 'headers' => $headers,
111
+ ];
112
+
113
+ {{#if (hasRequestBody this)}}
114
+ if ($input !== null) {
115
+ $requestDef['body'] = $input;
116
+ }
117
+ {{/if}}
118
+
119
+ $response = $this->transport->executeRequest($requestDef);
120
+
121
+ // Deserialize response
122
+ $data = $this->decodeResponse($response);
123
+
124
+ {{#if (eq responseType "void")}}
125
+ return;
126
+ {{else if (eq responseType "string")}}
127
+ return (string) $data;
128
+ {{else if (eq responseType "int")}}
129
+ return (int) $data;
130
+ {{else if (eq responseType "float")}}
131
+ return (float) $data;
132
+ {{else if (eq responseType "bool")}}
133
+ return (bool) $data;
134
+ {{else if (eq responseType "array")}}
135
+ return $data;
136
+ {{else}}
137
+ // Complex type: deserialize via fromArray
138
+ if (class_exists({{responseType}}::class) && method_exists({{responseType}}::class, 'fromArray')) {
139
+ return {{responseType}}::fromArray($data);
140
+ }
141
+ return $data;
142
+ {{/if}}
143
+ }
144
+
145
+ {{/if}}
146
+ {{/each}}
147
+
148
+ /**
149
+ * Decode a PSR-7 response body to an associative array or raw value.
150
+ *
151
+ * @return array<string, mixed>|string
152
+ */
153
+ private function decodeResponse(ResponseInterface $response): array|string
154
+ {
155
+ $contents = $response->getBody()->__toString();
156
+
157
+ try {
158
+ $decoded = json_decode($contents, true, 512, JSON_THROW_ON_ERROR);
159
+ if (is_array($decoded)) {
160
+ return $decoded;
161
+ }
162
+ if (is_scalar($decoded)) {
163
+ return (string) $decoded;
164
+ }
165
+ return ['raw' => $decoded];
166
+ } catch (\JsonException) {
167
+ if (trim($contents) === '') {
168
+ return [];
169
+ }
170
+ return $contents;
171
+ }
172
+ }
173
+ }
@@ -0,0 +1,131 @@
1
+ <?php
2
+
3
+ /**
4
+ * @generated
5
+ * This file was automatically generated by Tesseract.
6
+ * DO NOT MODIFY THIS FILE DIRECTLY.
7
+ */
8
+
9
+ declare(strict_types=1);
10
+
11
+ namespace {{namespace}};
12
+
13
+ use Throwable;
14
+
15
+ /**
16
+ * Standard SDK error class.
17
+ *
18
+ * Wraps HTTP errors and transport failures with structured metadata
19
+ * including HTTP status, error code, and request ID for debugging.
20
+ */
21
+ class SDKError extends \Exception
22
+ {
23
+ /**
24
+ * HTTP status code (0 for non-HTTP / connection errors).
25
+ */
26
+ private int $httpStatus;
27
+
28
+ /**
29
+ * Machine-readable error code (e.g. "NOT_FOUND", "VALIDATION_ERROR").
30
+ */
31
+ private string $errorCode;
32
+
33
+ /**
34
+ * The x-request-id header value, if present in the response.
35
+ */
36
+ private ?string $requestId;
37
+
38
+ /**
39
+ * Raw response body for inspection.
40
+ *
41
+ * @var array<string, mixed>|null
42
+ */
43
+ private ?array $responseBody;
44
+
45
+ public function __construct(
46
+ string $message = 'An SDK error occurred',
47
+ int $status = 0,
48
+ string $code = 'SDK_ERROR',
49
+ ?string $requestId = null,
50
+ ?array $responseBody = null,
51
+ ?Throwable $previous = null,
52
+ ) {
53
+ parent::__construct($message, $status, $previous);
54
+
55
+ $this->httpStatus = $status;
56
+ $this->errorCode = $code;
57
+ $this->requestId = $requestId;
58
+ $this->responseBody = $responseBody;
59
+ }
60
+
61
+ /**
62
+ * Create an SDKError from a generic Throwable.
63
+ */
64
+ public static function fromException(Throwable $exception): self
65
+ {
66
+ return new self(
67
+ message: $exception->getMessage(),
68
+ status: $exception->getCode(),
69
+ code: 'TRANSPORT_ERROR',
70
+ previous: $exception,
71
+ );
72
+ }
73
+
74
+ /**
75
+ * Get the HTTP status code.
76
+ */
77
+ public function getHttpStatus(): int
78
+ {
79
+ return $this->httpStatus;
80
+ }
81
+
82
+ /**
83
+ * Get the machine-readable error code.
84
+ */
85
+ public function getErrorCode(): string
86
+ {
87
+ return $this->errorCode;
88
+ }
89
+
90
+ /**
91
+ * Get the request ID from the x-request-id header, if available.
92
+ */
93
+ public function getRequestId(): ?string
94
+ {
95
+ return $this->requestId;
96
+ }
97
+
98
+ /**
99
+ * Get the decoded response body, if available.
100
+ *
101
+ * @return array<string, mixed>|null
102
+ */
103
+ public function getResponseBody(): ?array
104
+ {
105
+ return $this->responseBody;
106
+ }
107
+
108
+ /**
109
+ * Check if this error is retryable (status >= 500 or 429).
110
+ */
111
+ public function isRetryable(): bool
112
+ {
113
+ return $this->httpStatus >= 500 || $this->httpStatus === 429;
114
+ }
115
+
116
+ /**
117
+ * Check if this is a client error (4xx).
118
+ */
119
+ public function isClientError(): bool
120
+ {
121
+ return $this->httpStatus >= 400 && $this->httpStatus < 500;
122
+ }
123
+
124
+ /**
125
+ * Check if this is a server error (5xx).
126
+ */
127
+ public function isServerError(): bool
128
+ {
129
+ return $this->httpStatus >= 500;
130
+ }
131
+ }
@@ -0,0 +1,89 @@
1
+ # {{title}} PHP SDK
2
+
3
+ {{#if description}}
4
+ {{description}}
5
+
6
+ {{/if}}
7
+ > This SDK was automatically generated by [Tesseract](https://www.npmjs.com/package/@apollo-deploy/tesseract).
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ composer require {{composerName}}
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```php
18
+ <?php
19
+
20
+ require_once __DIR__ . '/vendor/autoload.php';
21
+
22
+ use {{namespace}}\Client;
23
+
24
+ $client = new Client([
25
+ 'baseUrl' => '{{baseUrl}}',
26
+ {{#each securitySchemes}}
27
+ '{{configKey}}' => 'your-{{kebabCase name}}-credential',
28
+ {{/each}}
29
+ ]);
30
+
31
+ {{#with groups.[0]}}
32
+ $api = $client->{{name}}();
33
+ {{/with}}
34
+ ```
35
+
36
+ ## Configuration
37
+
38
+ The `Client` constructor accepts an associative array with the following keys:
39
+
40
+ | Option | Type | Default | Description |
41
+ |--------|------|---------|-------------|
42
+ | `baseUrl` | `string` | `'{{baseUrl}}'` | Base URL for the API |
43
+ {{#each securitySchemes}}
44
+ | `{{configKey}}` | `string` | `null` | {{#if description}}{{description}}{{else}}{{type}} credential for {{name}}{{/if}} |
45
+ {{/each}}
46
+ | `timeoutMs` | `int` | `15000` | Request timeout in milliseconds |
47
+ | `retries` | `array` | `['attempts' => 3, 'backoffMs' => 400, 'jitter' => true]` | Retry configuration |
48
+ | `defaultHeaders` | `array` | `[]` | Default headers sent with every request |
49
+
50
+ ## API Reference
51
+
52
+ {{#each groups}}
53
+ ### `{{className}}`
54
+
55
+ {{#if description}}
56
+ {{description}}
57
+
58
+ {{/if}}
59
+ | Method | HTTP | Path | Description |
60
+ |--------|------|------|-------------|
61
+ {{#each operations}}
62
+ | `{{name}}()` | `{{upperCase httpMethod}}` | `{{path}}` | {{#if description}}{{description}}{{else}}-{{/if}} |
63
+ {{/each}}
64
+
65
+ {{/each}}
66
+
67
+ ## Types
68
+
69
+ {{#each schemas}}
70
+ {{#if isEnum}}
71
+ - `{{name}}` — enum: {{#each enumValues}}`{{this}}`{{#unless @last}}, {{/unless}}{{/each}}
72
+ {{else}}
73
+ - `{{name}}`{{#if description}} — {{description}}{{/if}}
74
+ {{/if}}
75
+ {{/each}}
76
+
77
+ ## Error Handling
78
+
79
+ ```php
80
+ <?php
81
+
82
+ use {{namespace}}\SDKError;
83
+
84
+ try {
85
+ $result = $client->{{groups.[0].name}}()->{{groups.[0].operations.[0].name}}(/* params */);
86
+ } catch (SDKError $e) {
87
+ echo sprintf("Error %d: %s (%s)\n", $e->getHttpStatus(), $e->getMessage(), $e->getErrorCode());
88
+ }
89
+ ```
@@ -0,0 +1,285 @@
1
+ <?php
2
+
3
+ /**
4
+ * @generated
5
+ * This file was automatically generated by Tesseract.
6
+ * DO NOT MODIFY THIS FILE DIRECTLY.
7
+ */
8
+
9
+ declare(strict_types=1);
10
+
11
+ namespace {{namespace}};
12
+
13
+ use GuzzleHttp\Client as GuzzleClient;
14
+ use GuzzleHttp\ClientInterface;
15
+ use GuzzleHttp\Exception\ConnectException;
16
+ use GuzzleHttp\Exception\RequestException;
17
+ use GuzzleHttp\Psr7\Request;
18
+ use GuzzleHttp\RequestOptions;
19
+ use Psr\Http\Message\ResponseInterface;
20
+ use Throwable;
21
+
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
+ class Transport
30
+ {
31
+ private ClientInterface $http;
32
+
33
+ /** @var array<string, mixed> */
34
+ private array $config;
35
+
36
+ /**
37
+ * @param array{
38
+ * baseUrl?: string,
39
+ * timeoutMs?: int,
40
+ * retries?: array{attempts?: int, backoffMs?: int, jitter?: bool, maxBackoffMs?: int},
41
+ * defaultHeaders?: array<string, string>,
42
+ {{#each securitySchemes}}
43
+ * {{configKey}}?: string,
44
+ {{/each}}
45
+ * } $config
46
+ */
47
+ public function __construct(array $config = [])
48
+ {
49
+ $this->config = $config;
50
+
51
+ $baseUrl = rtrim($config['baseUrl'] ?? '{{baseUrl}}', '/');
52
+ $timeoutMs = $config['timeoutMs'] ?? 15000;
53
+
54
+ $headers = $config['defaultHeaders'] ?? [];
55
+ $headers['Accept'] = $headers['Accept'] ?? 'application/json';
56
+ $headers['User-Agent'] = $headers['User-Agent'] ?? '{{clientName}}-php-sdk/{{version}}';
57
+
58
+ $this->http = new GuzzleClient([
59
+ 'base_uri' => $baseUrl . '/',
60
+ 'timeout' => $timeoutMs / 1000.0,
61
+ 'http_errors' => false,
62
+ 'headers' => $headers,
63
+ ]);
64
+ }
65
+
66
+ /**
67
+ * Execute an HTTP request with retry logic and auth injection.
68
+ *
69
+ * @param array{
70
+ * method: string,
71
+ * path: string,
72
+ * query?: array<string, mixed>,
73
+ * body?: mixed,
74
+ * headers?: array<string, string>,
75
+ * timeoutMs?: int,
76
+ * } $requestDef
77
+ * @return ResponseInterface
78
+ * @throws SDKError
79
+ */
80
+ public function executeRequest(array $requestDef): ResponseInterface
81
+ {
82
+ $method = strtoupper($requestDef['method']);
83
+ $path = ltrim($requestDef['path'], '/');
84
+ $query = $requestDef['query'] ?? [];
85
+ $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
+ ];
95
+
96
+ if (!empty($query)) {
97
+ $options[RequestOptions::QUERY] = $query;
98
+ }
99
+
100
+ if ($body !== null) {
101
+ $options[RequestOptions::JSON] = $body;
102
+ }
103
+
104
+ if ($timeoutMs !== null) {
105
+ $options[RequestOptions::TIMEOUT] = $timeoutMs / 1000.0;
106
+ }
107
+
108
+ // ── Execute with retry ─────────────────────────────────────────────
109
+ return $this->executeWithRetry($method, $path, $options);
110
+ }
111
+
112
+ /**
113
+ * Execute with configurable retry logic.
114
+ */
115
+ private function executeWithRetry(string $method, string $path, array $options): ResponseInterface
116
+ {
117
+ $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;
124
+
125
+ for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
126
+ try {
127
+ $request = new Request($method, $path, $options[RequestOptions::HEADERS] ?? []);
128
+ $response = $this->http->send($request, $options);
129
+
130
+ // Retry on server errors (5xx) and 429 (rate limit)
131
+ $status = $response->getStatusCode();
132
+ if ($status < 500 && $status !== 429) {
133
+ return $response;
134
+ }
135
+
136
+ if ($attempt < $maxAttempts) {
137
+ $delayMs = $this->calculateBackoff($attempt, $baseBackoffMs, $useJitter, $maxBackoffMs);
138
+ $this->sleep($delayMs);
139
+ }
140
+ } catch (ConnectException $e) {
141
+ $lastException = $e;
142
+
143
+ if ($attempt < $maxAttempts) {
144
+ $delayMs = $this->calculateBackoff($attempt, $baseBackoffMs, $useJitter, $maxBackoffMs);
145
+ $this->sleep($delayMs);
146
+ }
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
+ }
159
+ }
160
+ throw $this->mapToSDKError($e);
161
+ }
162
+ }
163
+
164
+ // All retries exhausted
165
+ if ($lastException instanceof RequestException) {
166
+ throw $this->mapToSDKError($lastException);
167
+ }
168
+
169
+ if ($lastException instanceof Throwable) {
170
+ throw SDKError::fromException($lastException);
171
+ }
172
+
173
+ throw new SDKError('Request failed after ' . $maxAttempts . ' attempts');
174
+ }
175
+
176
+ /**
177
+ * Build auth headers from configured security schemes.
178
+ *
179
+ * @return array<string, string>
180
+ */
181
+ private function buildAuthHeaders(): array
182
+ {
183
+ $headers = [];
184
+
185
+ {{#each securitySchemes}}
186
+ {{#if (eq type "apiKey")}}
187
+ {{#if (eq in "header")}}
188
+ // {{name}} — API key in header
189
+ if (!empty($this->config['{{configKey}}'])) {
190
+ $headers['{{paramName}}'] = $this->config['{{configKey}}'];
191
+ }
192
+ {{/if}}
193
+ {{/if}}
194
+
195
+ {{#if (and (eq type "http") (eq scheme "bearer"))}}
196
+ // {{name}} — Bearer token
197
+ if (!empty($this->config['{{configKey}}'])) {
198
+ $headers['Authorization'] = 'Bearer ' . $this->config['{{configKey}}'];
199
+ }
200
+ {{/if}}
201
+
202
+ {{#if (eq type "oauth2")}}
203
+ // {{name}} — OAuth2 access token
204
+ if (!empty($this->config['{{configKey}}'])) {
205
+ $headers['Authorization'] = 'Bearer ' . $this->config['{{configKey}}'];
206
+ }
207
+ {{/if}}
208
+
209
+ {{#if (eq type "openIdConnect")}}
210
+ // {{name}} — OpenID Connect token
211
+ if (!empty($this->config['{{configKey}}'])) {
212
+ $headers['Authorization'] = 'Bearer ' . $this->config['{{configKey}}'];
213
+ }
214
+ {{/if}}
215
+ {{/each}}
216
+
217
+ return $headers;
218
+ }
219
+
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
+ private function sleep(int $milliseconds): void
238
+ {
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);
251
+ }
252
+
253
+ $status = $response->getStatusCode();
254
+ $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;
259
+
260
+ return new SDKError(
261
+ message: (string) $message,
262
+ status: $status,
263
+ code: (string) $code,
264
+ requestId: $requestId,
265
+ previous: $exception,
266
+ );
267
+ }
268
+
269
+ /**
270
+ * Decode a PSR-7 response body, returning an associative array.
271
+ *
272
+ * @return array<string, mixed>
273
+ */
274
+ private function decodeResponseBody(ResponseInterface $response): array
275
+ {
276
+ $contents = $response->getBody()->__toString();
277
+
278
+ try {
279
+ $decoded = json_decode($contents, true, 512, JSON_THROW_ON_ERROR);
280
+ return is_array($decoded) ? $decoded : ['raw' => $decoded];
281
+ } catch (Throwable) {
282
+ return ['raw' => $contents];
283
+ }
284
+ }
285
+ }