@apollo-deploy/tesseract 1.8.0 → 2.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 (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 +77 -3
  67. package/dist/collector.d.ts.map +1 -1
  68. package/dist/collector.js +198 -20
  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 +8 -9
  75. package/dist/fastify.d.ts.map +1 -1
  76. package/dist/fastify.js +105 -164
  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
@@ -9,34 +9,45 @@ import io.ktor.client.request.*
9
9
  import io.ktor.client.statement.*
10
10
  import io.ktor.http.*
11
11
  import io.ktor.serialization.kotlinx.json.*
12
+ import java.time.ZonedDateTime
13
+ import java.time.format.DateTimeFormatter
14
+ import kotlin.math.max
15
+ import kotlin.random.Random
16
+ import kotlinx.coroutines.CancellationException
17
+ import kotlinx.coroutines.delay
12
18
  import kotlinx.serialization.json.Json
19
+ import kotlinx.serialization.json.JsonElement
13
20
  import kotlinx.serialization.json.JsonObject
14
21
  import kotlinx.serialization.json.jsonPrimitive
15
- import kotlinx.coroutines.*
16
22
 
17
- /**
18
- * Internal transport configuration.
19
- */
23
+ /** Internal transport configuration. */
20
24
  data class TransportConfig(
21
25
  val baseUrl: String,
22
26
  {{#each securitySchemes}}
23
27
  val {{configKey}}: String? = null,
24
28
  {{/each}}
25
29
  val defaultHeaders: Map<String, String> = emptyMap(),
26
- val serviceToken: String? = null,
27
- val timeoutMs: Long = 15000,
30
+ val timeoutMs: Long = 15_000,
28
31
  val maxRetries: Int = 3,
32
+ val retryUnsafeRequests: Boolean = false,
29
33
  )
30
34
 
31
35
  /**
32
36
  * HTTP transport layer for the SDK.
33
37
  *
34
- * Handles request building, authentication, retries, and error mapping
35
- * using Ktor's HttpClient.
38
+ * Retries transient failures for idempotent methods. POST and PATCH are retried
39
+ * only when [TransportConfig.retryUnsafeRequests] is enabled or an
40
+ * `X-Idempotency-Key` header is present.
36
41
  */
37
42
  class Transport(
38
- private val config: TransportConfig
39
- ) {
43
+ private val config: TransportConfig,
44
+ ) : AutoCloseable {
45
+ init {
46
+ require(config.baseUrl.isNotBlank()) { "baseUrl must not be blank" }
47
+ require(config.timeoutMs > 0) { "timeoutMs must be greater than zero" }
48
+ require(config.maxRetries >= 0) { "maxRetries must not be negative" }
49
+ }
50
+
40
51
  @PublishedApi
41
52
  internal val json = Json {
42
53
  ignoreUnknownKeys = true
@@ -58,8 +69,10 @@ class Transport(
58
69
 
59
70
  defaultRequest {
60
71
  url(config.baseUrl)
61
- contentType(ContentType.Application.Json)
62
72
  accept(ContentType.Application.Json)
73
+ if (config.defaultHeaders.keys.none { it.equals(HttpHeaders.UserAgent, ignoreCase = true) }) {
74
+ header(HttpHeaders.UserAgent, "{{packageName}}/{{packageVersion}}")
75
+ }
63
76
  {{#each securitySchemes}}
64
77
  {{#if (eq type "apiKey")}}
65
78
  {{#if (eq in "header")}}
@@ -67,169 +80,173 @@ class Transport(
67
80
  {{/if}}
68
81
  {{/if}}
69
82
  {{#if (and (eq type "http") (eq scheme "bearer"))}}
70
- config.{{configKey}}?.let { token -> headers.append("Authorization", "Bearer $token") }
83
+ config.{{configKey}}?.let { token -> headers.append(HttpHeaders.Authorization, "Bearer $token") }
71
84
  {{/if}}
72
85
  {{#if (eq type "oauth2")}}
73
- config.{{configKey}}?.let { token -> headers.append("Authorization", "Bearer $token") }
86
+ config.{{configKey}}?.let { token -> headers.append(HttpHeaders.Authorization, "Bearer $token") }
74
87
  {{/if}}
75
88
  {{#if (eq type "openIdConnect")}}
76
- config.{{configKey}}?.let { token -> headers.append("Authorization", "Bearer $token") }
89
+ config.{{configKey}}?.let { token -> headers.append(HttpHeaders.Authorization, "Bearer $token") }
77
90
  {{/if}}
78
91
  {{/each}}
79
- config.defaultHeaders.forEach { (k, v) -> headers.append(k, v) }
92
+ config.defaultHeaders.forEach { (key, value) -> headers.append(key, value) }
80
93
  }
81
94
  }
82
95
 
83
- /**
84
- * Execute an HTTP request, deserializing the response body as JSON.
85
- */
96
+ /** Execute an HTTP request and deserialize its JSON response. */
86
97
  suspend inline fun <reified T> execute(
87
98
  method: HttpMethod,
88
99
  path: String,
89
100
  query: Map<String, String?>? = null,
90
- body: Any? = null,
101
+ body: JsonElement? = null,
91
102
  headers: Map<String, String>? = null,
92
103
  ): T {
93
104
  val response = executeRaw(method, path, query, body, headers)
105
+ if (response.isBlank()) {
106
+ @Suppress("UNCHECKED_CAST")
107
+ return Unit as T
108
+ }
94
109
  return json.decodeFromString<T>(response)
95
110
  }
96
111
 
97
- /**
98
- * Execute an HTTP request, returning the raw response body as a string.
99
- */
112
+ /** Execute an HTTP request and return its raw response body. */
100
113
  suspend fun executeRaw(
101
114
  method: HttpMethod,
102
115
  path: String,
103
116
  query: Map<String, String?>? = null,
104
- body: Any? = null,
117
+ body: JsonElement? = null,
105
118
  headers: Map<String, String>? = null,
106
119
  ): String {
107
- return executeWithRetry(method, path, query, body, headers)
108
- }
109
-
110
- /**
111
- * Close the underlying HTTP client. Call this when the SDK is no longer needed.
112
- */
113
- fun close() {
114
- client.close()
115
- }
120
+ val canRetry = canRetry(method, headers)
121
+ var lastError: Throwable? = null
116
122
 
117
- // ── private helpers ─────────────────────────────────────────────────────
123
+ for (attempt in 0..config.maxRetries) {
124
+ try {
125
+ val response = client.request {
126
+ this.method = method
127
+ url {
128
+ takeFrom("${config.baseUrl.trimEnd('/')}$path")
129
+ query?.forEach { (key, value) ->
130
+ value?.let { parameters.append(key, it) }
131
+ }
132
+ }
118
133
 
119
- @OptIn(ExperimentalCoroutinesApi::class)
120
- private suspend fun executeWithRetry(
121
- method: HttpMethod,
122
- path: String,
123
- query: Map<String, String?>? = null,
124
- body: Any? = null,
125
- headers: Map<String, String>? = null,
126
- attempt: Int = 0,
127
- ): String {
128
- return try {
129
- val response = client.request {
130
- this.method = method
131
- url {
132
- // Base URL is set as default; append the path
133
- val fullUrl = "${config.baseUrl.trimEnd('/')}${path}"
134
- takeFrom(fullUrl)
135
-
136
- // Add query parameters
137
- query?.forEach { (key, value) ->
138
- if (value != null) {
139
- parameters.append(key, value)
134
+ if (body != null) {
135
+ if (headers?.keys?.none { it.equals(HttpHeaders.ContentType, ignoreCase = true) } != false) {
136
+ contentType(ContentType.Application.Json)
140
137
  }
138
+ setBody(body)
141
139
  }
140
+
141
+ headers?.forEach { (key, value) -> this.headers.append(key, value) }
142
142
  }
143
143
 
144
- // Set request body
145
- if (body != null) {
146
- setBody(body)
144
+ if (response.status.value in 200..299) {
145
+ return response.bodyAsText()
147
146
  }
148
147
 
149
- // Set custom headers
150
- headers?.forEach { (key, value) ->
151
- this.headers.append(key, value)
148
+ val error = buildSdkException(response)
149
+ if (!canRetry || !isRetryableStatus(response.status.value) || attempt == config.maxRetries) {
150
+ throw error
152
151
  }
153
- }
154
152
 
155
- val status = response.status.value
156
-
157
- if (status in 200..299) {
158
- response.bodyAsText()
159
- } else if ((status >= 500 || status == 429) && attempt < config.maxRetries) {
160
- // Retry on server errors and rate limiting
161
- val backoffMs = 200L * (1L shl attempt)
162
- delay(backoffMs)
163
- executeWithRetry(method, path, query, body, headers, attempt + 1)
164
- } else {
165
- throw buildSdkException(response)
166
- }
167
- } catch (e: SdkException) {
168
- throw e
169
- } catch (e: CancellationException) {
170
- throw e
171
- } catch (e: Exception) {
172
- if (attempt < config.maxRetries) {
173
- val backoffMs = 200L * (1L shl attempt)
174
- delay(backoffMs)
175
- executeWithRetry(method, path, query, body, headers, attempt + 1)
176
- } else {
177
- throw SdkException(
178
- status = null,
179
- code = null,
180
- message = "Network error: ${e.message}",
181
- requestId = null,
182
- cause = e,
183
- )
153
+ lastError = error
154
+ delay(retryDelayMs(response.headers[HttpHeaders.RetryAfter], attempt))
155
+ } catch (error: CancellationException) {
156
+ throw error
157
+ } catch (error: SdkException) {
158
+ throw error
159
+ } catch (error: Exception) {
160
+ if (!canRetry || attempt == config.maxRetries) {
161
+ throw SdkException(
162
+ status = null,
163
+ code = null,
164
+ message = "Network error: ${error.message ?: error::class.simpleName}",
165
+ requestId = null,
166
+ cause = error,
167
+ )
168
+ }
169
+
170
+ lastError = error
171
+ delay(retryDelayMs(null, attempt))
184
172
  }
185
173
  }
174
+
175
+ throw SdkException(
176
+ status = null,
177
+ code = null,
178
+ message = "Request failed after ${config.maxRetries + 1} attempts",
179
+ requestId = null,
180
+ cause = lastError,
181
+ )
186
182
  }
187
183
 
188
- private suspend fun buildSdkException(response: HttpResponse): SdkException {
189
- val status = response.status.value
190
- val body = try {
191
- response.bodyAsText()
192
- } catch (e: Exception) {
193
- null
184
+ override fun close() {
185
+ client.close()
186
+ }
187
+
188
+ private fun canRetry(method: HttpMethod, headers: Map<String, String>?): Boolean {
189
+ if (config.retryUnsafeRequests || method in RETRYABLE_METHODS) return true
190
+ return config.defaultHeaders.any { (name, value) ->
191
+ name.equals("X-Idempotency-Key", ignoreCase = true) && value.isNotEmpty()
192
+ } || headers?.any { (name, value) ->
193
+ name.equals("X-Idempotency-Key", ignoreCase = true) && value.isNotEmpty()
194
+ } == true
195
+ }
196
+
197
+ private fun retryDelayMs(retryAfter: String?, attempt: Int): Long {
198
+ val serverDelay = retryAfter?.trim()?.let { value ->
199
+ value.toLongOrNull()?.let { seconds -> max(0L, seconds * 1_000L) }
200
+ ?: runCatching {
201
+ max(
202
+ 0L,
203
+ ZonedDateTime.parse(value, DateTimeFormatter.RFC_1123_DATE_TIME)
204
+ .toInstant()
205
+ .toEpochMilli() - System.currentTimeMillis(),
206
+ )
207
+ }.getOrNull()
194
208
  }
209
+ if (serverDelay != null) return serverDelay.coerceAtMost(MAX_RETRY_DELAY_MS)
210
+
211
+ val base = (200L * (1L shl attempt.coerceAtMost(10))).coerceAtMost(MAX_RETRY_DELAY_MS)
212
+ return (base + Random.nextLong(base / 2 + 1)).coerceAtMost(MAX_RETRY_DELAY_MS)
213
+ }
195
214
 
196
- val requestId = response.headers["x-request-id"]
197
- ?: response.headers["request-id"]
215
+ private suspend fun buildSdkException(response: HttpResponse): SdkException {
216
+ val status = response.status.value
217
+ val body = runCatching { response.bodyAsText() }.getOrNull()
218
+ val requestId = response.headers["x-request-id"] ?: response.headers["request-id"]
198
219
 
199
- // Try to extract structured error info from the JSON body
200
220
  var code: String? = null
201
- var message: String = body ?: "API error (status $status)"
221
+ var message = body?.takeIf { it.isNotBlank() } ?: "API error (status $status)"
202
222
 
203
- if (body != null) {
204
- try {
223
+ if (!body.isNullOrBlank()) {
224
+ runCatching {
205
225
  val jsonBody = json.decodeFromString<JsonObject>(body)
226
+ val error = jsonBody["error"]
206
227
  code = jsonBody["code"]?.jsonPrimitive?.content
207
- ?: jsonBody["error"]?.let { err ->
208
- when (err) {
209
- is JsonObject -> err["code"]?.jsonPrimitive?.content
210
- else -> err.jsonPrimitive.content
211
- }
212
- }
228
+ ?: (error as? JsonObject)?.get("code")?.jsonPrimitive?.content
213
229
  message = jsonBody["message"]?.jsonPrimitive?.content
214
- ?: jsonBody["error"]?.let { err ->
215
- when (err) {
216
- is JsonObject -> err["message"]?.jsonPrimitive?.content
217
- else -> err.jsonPrimitive.content
218
- }
219
- }
230
+ ?: (error as? JsonObject)?.get("message")?.jsonPrimitive?.content
231
+ ?: error?.jsonPrimitive?.content
220
232
  ?: message
221
- } catch (_: Exception) {
222
- // Use raw body as the message
223
- message = body
224
233
  }
225
234
  }
226
235
 
227
- return SdkException(
228
- status = status,
229
- code = code,
230
- message = message,
231
- requestId = requestId,
232
- )
236
+ return SdkException(status, code, message, requestId)
233
237
  }
234
238
 
239
+ private companion object {
240
+ val RETRYABLE_METHODS = setOf(
241
+ HttpMethod.Get,
242
+ HttpMethod.Head,
243
+ HttpMethod.Options,
244
+ HttpMethod.Put,
245
+ HttpMethod.Delete,
246
+ )
247
+ val RETRYABLE_STATUSES = setOf(408, 425, 429, 500, 502, 503, 504)
248
+ const val MAX_RETRY_DELAY_MS = 30_000L
249
+
250
+ fun isRetryableStatus(status: Int): Boolean = status in RETRYABLE_STATUSES
251
+ }
235
252
  }
@@ -1,100 +1,49 @@
1
1
  package {{packageName}}.models
2
2
 
3
- import kotlinx.serialization.*
4
- import kotlinx.serialization.descriptors.*
5
- import kotlinx.serialization.encoding.*
6
- import kotlinx.serialization.json.*
3
+ import kotlinx.serialization.SerialName
4
+ import kotlinx.serialization.Serializable
5
+ import kotlinx.serialization.json.JsonElement
7
6
 
8
7
  {{#each schemas}}
9
- {{#if (not ownership)}}
10
- {{#unless (or externalImport (eq ownership.kind "external"))}}
11
- {{#if (not isEnum)}}
12
- {{#if (not isUnionType)}}
13
- {{#if (not isIntersectionType)}}
14
- {{#if (not isTypeAlias)}}
15
- {{#if description}}
16
- /**
17
- * {{description}}
18
- */
19
- {{/if}}
20
- @Serializable
21
- data class {{name}}(
22
- {{#each properties}}
23
- {{#if description}}
24
- /** {{description}} */
25
- {{/if}}
26
- {{#if deprecated}}
27
- @Deprecated("{{#if deprecationMessage}}{{deprecationMessage}}{{else}}Use a different property.{{/if}}")
28
- {{/if}}
29
- {{#if (and nullable (not required))}}
30
- @SerialName("{{name}}")
31
- val {{name}}: {{{kotlinType type}}}? = null,
32
- {{else if nullable}}
33
- @SerialName("{{name}}")
34
- val {{name}}: {{{kotlinType type}}}? = null,
35
- {{else if required}}
36
- @SerialName("{{name}}")
37
- val {{name}}: {{{kotlinType type}}},
38
- {{else}}
39
- @SerialName("{{name}}")
40
- val {{name}}: {{{kotlinType type}}}? = null,
41
- {{/if}}
42
- {{/each}}
43
- )
44
- {{/if}}
45
- {{/if}}
46
- {{/if}}
47
- {{/if}}
48
- {{/unless}}
49
- {{/if}}
8
+ {{#if external}}
9
+ typealias {{name}} = JsonElement
50
10
 
51
- {{#if isEnum}}
52
- {{#if description}}
53
- /**
54
- * {{description}}
55
- */
56
- {{/if}}
11
+ {{else if isEnum}}
12
+ {{#if stringEnum}}
57
13
  @Serializable
58
14
  enum class {{name}} {
59
- {{#each enumValues}}
60
- @SerialName("{{this}}")
61
- {{pascalCase this}},
62
- {{/each}}
15
+ {{#each enumValues}}
16
+ @SerialName("{{value}}") {{name}},
17
+ {{/each}}
63
18
  }
19
+ {{else}}
20
+ typealias {{name}} = Double
64
21
  {{/if}}
65
22
 
66
- {{#if isUnionType}}
67
- {{#if unionMembers.length}}
68
- {{#if description}}
69
- /**
70
- * {{description}}
71
- */
72
- {{/if}}
73
- @Serializable
74
- sealed class {{name}} {
75
- {{#each unionMembers}}
76
- @Serializable
77
- data class {{pascalCase this}}(val value: {{{kotlinType this}}}) : {{../name}}()
78
- {{/each}}
79
- }
80
- {{/if}}
81
- {{/if}}
23
+ {{else if isUnionType}}
24
+ typealias {{name}} = JsonElement
25
+
26
+ {{else if isIntersectionType}}
27
+ typealias {{name}} = JsonElement
82
28
 
83
- {{#if isIntersectionType}}
84
- {{#if intersectionMembers.length}}
29
+ {{else if isTypeAlias}}
30
+ typealias {{name}} = {{{aliasType}}}
31
+
32
+ {{else}}
85
33
  {{#if description}}
86
- /**
87
- * {{description}}
88
- */
34
+ /** {{description}} */
89
35
  {{/if}}
90
- {{! Intersection type — emit as interface implemented by concrete types }}
91
36
  @Serializable
37
+ {{#if hasProperties}}
92
38
  data class {{name}}(
93
- // Intersection of: {{join intersectionMembers ", "}}
94
- // Fields from all intersected types should be inlined here.
95
- // TODO: generate combined fields
39
+ {{#each properties}}
40
+ @SerialName("{{name}}")
41
+ val {{fieldName}}: {{{targetType}}}{{#if hasDefault}} = null{{/if}},
42
+ {{/each}}
96
43
  )
97
- {{/if}}
44
+ {{else}}
45
+ class {{name}}
98
46
  {{/if}}
99
47
 
48
+ {{/if}}
100
49
  {{/each}}
@@ -1,74 +1,36 @@
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 groups}}
14
- use {{namespace}}\Api\{{className}};
8
+ use {{fullClassName}};
15
9
  {{/each}}
16
10
 
17
- /**
18
- * {{title}} SDK Client.
19
- *
20
- * Entry point for all API operations. Provides accessor methods
21
- * for each domain group and manages shared transport configuration.
22
- *
23
- * Usage:
24
- * <code>
25
- * $client = new Client(['baseUrl' => '{{baseUrl}}']);
26
- * $result = $client->{{groups.[0].name}}()->{{groups.[0].operations.[0].name}}(...);
27
- * </code>
28
- */
11
+ /** {{title}} SDK client. */
29
12
  class Client
30
13
  {
31
14
  private Transport $transport;
32
15
 
33
- /**
34
- * @param array{
35
- * baseUrl?: string,
36
- * timeoutMs?: int,
37
- * retries?: array{attempts?: int, backoffMs?: int, jitter?: bool, maxBackoffMs?: int},
38
- * defaultHeaders?: array<string, string>,
39
- {{#each securitySchemes}}
40
- * {{configKey}}?: string,
41
- {{/each}}
42
- * } $config
43
- */
16
+ /** @param array<string, mixed> $config */
44
17
  public function __construct(array $config = [])
45
18
  {
46
19
  $this->transport = new Transport($config);
47
20
  }
48
21
 
49
- /**
50
- * Get the underlying transport instance for advanced use.
51
- */
52
22
  public function getTransport(): Transport
53
23
  {
54
24
  return $this->transport;
55
25
  }
56
26
 
57
- {{#each groups}}
58
- /** @var {{className}}|null */
59
- private ?{{className}} $_{{{name}}} = null;
27
+ {{#each groups}}
28
+ private ?{{className}} $_{{accessorName}} = null;
60
29
 
61
- /**
62
- {{#if description}}
63
- * {{description}}
64
- *
65
- {{/if}}
66
- * @return {{className}}
67
- */
68
- public function {{name}}(): {{className}}
30
+ public function {{accessorName}}(): {{className}}
69
31
  {
70
- return $this->_{{{name}}} ??= new {{className}}($this->transport);
32
+ return $this->_{{accessorName}} ??= new {{className}}($this->transport);
71
33
  }
72
34
 
73
- {{/each}}
35
+ {{/each}}
74
36
  }
@@ -6,7 +6,8 @@
6
6
  "autoload": {
7
7
  "psr-4": {
8
8
  "{{namespace}}\\": "src/"
9
- }
9
+ },
10
+ "classmap": ["src/Types.php"]
10
11
  },
11
12
  "require": {
12
13
  "php": ">=8.1",