@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
@@ -4,16 +4,15 @@ using System;
4
4
  using System.Collections.Generic;
5
5
  using System.IO;
6
6
  using System.Linq;
7
+ using System.Net;
7
8
  using System.Net.Http;
8
9
  using System.Text;
9
10
  using System.Text.Json;
10
11
  using System.Threading;
11
12
  using System.Threading.Tasks;
12
13
 
13
- /// <summary>
14
- /// Configuration for the HTTP transport layer.
15
- /// </summary>
16
- public class TransportConfig
14
+ /// <summary>Configuration for the HTTP transport layer.</summary>
15
+ public sealed class TransportConfig
17
16
  {
18
17
  public string BaseUrl { get; set; } = "";
19
18
  {{#each securitySchemes}}
@@ -21,33 +20,54 @@ public class TransportConfig
21
20
  {{/each}}
22
21
  public int TimeoutMs { get; set; } = 15000;
23
22
  public int MaxRetries { get; set; } = 3;
23
+ public bool RetryUnsafeRequests { get; set; }
24
24
  public Dictionary<string, string>? DefaultHeaders { get; set; }
25
25
  }
26
26
 
27
27
  /// <summary>
28
- /// Low-level HTTP transport handling authentication, serialization, retries, and error normalization.
28
+ /// Low-level HTTP transport handling authentication, serialization, bounded retries,
29
+ /// cancellation, and normalized API errors.
29
30
  /// </summary>
30
- public class Transport
31
+ public sealed class Transport : IDisposable
31
32
  {
33
+ private static readonly HashSet<HttpStatusCode> RetryableStatuses = new()
34
+ {
35
+ HttpStatusCode.RequestTimeout,
36
+ (HttpStatusCode)425,
37
+ (HttpStatusCode)429,
38
+ HttpStatusCode.InternalServerError,
39
+ HttpStatusCode.BadGateway,
40
+ HttpStatusCode.ServiceUnavailable,
41
+ HttpStatusCode.GatewayTimeout,
42
+ };
43
+
32
44
  private readonly TransportConfig _config;
33
45
  private readonly HttpClient _httpClient;
34
- private static readonly JsonSerializerOptions _jsonOptions = new()
46
+ private readonly bool _ownsHttpClient;
47
+
48
+ private static readonly JsonSerializerOptions JsonOptions = new()
35
49
  {
36
50
  PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
37
51
  PropertyNameCaseInsensitive = true,
38
52
  DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
39
53
  };
40
54
 
41
- public Transport(TransportConfig config)
55
+ public Transport(TransportConfig config, HttpClient? httpClient = null)
42
56
  {
57
+ if (string.IsNullOrWhiteSpace(config.BaseUrl))
58
+ throw new ArgumentException("BaseUrl must not be blank", nameof(config));
59
+ if (config.TimeoutMs <= 0)
60
+ throw new ArgumentOutOfRangeException(nameof(config), "TimeoutMs must be greater than zero");
61
+ if (config.MaxRetries < 0)
62
+ throw new ArgumentOutOfRangeException(nameof(config), "MaxRetries must not be negative");
63
+
43
64
  _config = config;
44
- _httpClient = new HttpClient
45
- {
46
- Timeout = TimeSpan.FromMilliseconds(config.TimeoutMs),
47
- };
65
+ _ownsHttpClient = httpClient is null;
66
+ _httpClient = httpClient ?? new HttpClient();
67
+ _httpClient.Timeout = TimeSpan.FromMilliseconds(config.TimeoutMs);
48
68
  }
49
69
 
50
- public class Request
70
+ public sealed class Request
51
71
  {
52
72
  public HttpMethod Method { get; set; } = HttpMethod.Get;
53
73
  public string Path { get; set; } = "";
@@ -59,136 +79,165 @@ public class Transport
59
79
 
60
80
  public async Task<T> ExecuteAsync<T>(Request request, CancellationToken cancellationToken = default)
61
81
  {
62
- return await ExecuteWithRetriesAsync<T>(request, _config.MaxRetries, cancellationToken);
82
+ using var response = await SendWithRetriesAsync(
83
+ request,
84
+ HttpCompletionOption.ResponseContentRead,
85
+ cancellationToken);
86
+ var body = await response.Content.ReadAsStringAsync(cancellationToken);
87
+ EnsureSuccess(response, body);
88
+
89
+ if (string.IsNullOrWhiteSpace(body)) return default!;
90
+
91
+ try
92
+ {
93
+ return JsonSerializer.Deserialize<T>(body, JsonOptions)
94
+ ?? throw new JsonException("Response body was JSON null");
95
+ }
96
+ catch (JsonException error)
97
+ {
98
+ throw new SdkException(
99
+ (int)response.StatusCode,
100
+ "DESERIALIZE_ERROR",
101
+ "Failed to deserialize response body",
102
+ GetRequestId(response),
103
+ error);
104
+ }
63
105
  }
64
106
 
65
107
  public async Task<byte[]> ExecuteRawAsync(Request request, CancellationToken cancellationToken = default)
66
108
  {
67
- return await ExecuteRawWithRetriesAsync(request, _config.MaxRetries, cancellationToken);
109
+ using var response = await SendWithRetriesAsync(
110
+ request,
111
+ HttpCompletionOption.ResponseContentRead,
112
+ cancellationToken);
113
+ var body = await response.Content.ReadAsByteArrayAsync(cancellationToken);
114
+ if (!response.IsSuccessStatusCode)
115
+ throw ParseErrorResponse(response, Encoding.UTF8.GetString(body));
116
+ return body;
68
117
  }
69
118
 
70
- public async Task<HttpResponseMessage> StreamAsync(Request request, CancellationToken cancellationToken = default)
119
+ /// <summary>
120
+ /// Executes a streaming request. The caller owns and must dispose the returned response.
121
+ /// </summary>
122
+ public async Task<HttpResponseMessage> StreamAsync(
123
+ Request request,
124
+ CancellationToken cancellationToken = default)
71
125
  {
72
- using var httpRequest = BuildRequest(request);
73
- var response = await _httpClient.SendAsync(
74
- httpRequest,
126
+ var response = await SendWithRetriesAsync(
127
+ request,
75
128
  HttpCompletionOption.ResponseHeadersRead,
76
129
  cancellationToken);
77
- if (!response.IsSuccessStatusCode)
78
- {
79
- var errorBody = await response.Content.ReadAsStringAsync(cancellationToken);
80
- throw ParseErrorResponse(response, errorBody);
81
- }
82
- return response;
130
+ if (response.IsSuccessStatusCode) return response;
131
+
132
+ var body = await response.Content.ReadAsStringAsync(cancellationToken);
133
+ var error = ParseErrorResponse(response, body);
134
+ response.Dispose();
135
+ throw error;
136
+ }
137
+
138
+ public void Dispose()
139
+ {
140
+ if (_ownsHttpClient) _httpClient.Dispose();
83
141
  }
84
142
 
85
- private async Task<T> ExecuteWithRetriesAsync<T>(
86
- Request request, int maxRetries, CancellationToken cancellationToken)
143
+ private async Task<HttpResponseMessage> SendWithRetriesAsync(
144
+ Request request,
145
+ HttpCompletionOption completionOption,
146
+ CancellationToken cancellationToken)
87
147
  {
88
- Exception? lastException = null;
89
- for (int attempt = 0; attempt <= maxRetries; attempt++)
148
+ var canRetry = CanRetry(request);
149
+
150
+ for (var attempt = 0; attempt <= _config.MaxRetries; attempt++)
90
151
  {
91
- if (attempt > 0)
92
- {
93
- var backoff = (int)Math.Pow(2, attempt - 1) * 200;
94
- var jitter = Random.Shared.Next(backoff / 2);
95
- await Task.Delay(backoff + jitter, cancellationToken);
96
- }
97
152
  try
98
153
  {
99
154
  using var httpRequest = BuildRequest(request);
100
- using var response = await _httpClient.SendAsync(httpRequest, cancellationToken);
101
- var body = await response.Content.ReadAsStringAsync(cancellationToken);
102
- if (response.IsSuccessStatusCode)
103
- {
104
- if (string.IsNullOrEmpty(body)) return default!;
105
- var result = JsonSerializer.Deserialize<T>(body, _jsonOptions);
106
- if (result == null)
107
- throw new SdkException((int)response.StatusCode, "DESERIALIZE_ERROR",
108
- "Failed to deserialize response body", GetRequestId(response));
109
- return result;
110
- }
111
- if ((int)response.StatusCode >= 500 && attempt < maxRetries)
155
+ var response = await _httpClient.SendAsync(
156
+ httpRequest,
157
+ completionOption,
158
+ cancellationToken);
159
+
160
+ if (response.IsSuccessStatusCode ||
161
+ !canRetry ||
162
+ !RetryableStatuses.Contains(response.StatusCode) ||
163
+ attempt == _config.MaxRetries)
112
164
  {
113
- lastException = ParseErrorResponse(response, body);
114
- continue;
165
+ return response;
115
166
  }
116
- throw ParseErrorResponse(response, body);
117
- }
118
- catch (SdkException) { throw; }
119
- catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested) { throw; }
120
- catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; }
121
- catch (Exception ex) when (IsRetryable(ex) && attempt < maxRetries) { lastException = ex; }
122
- catch (Exception ex)
123
- {
124
- throw new SdkException(0, "REQUEST_ERROR", ex.Message, null, ex);
125
- }
126
- }
127
- throw new SdkException(0, "MAX_RETRIES_EXCEEDED",
128
- "Request failed after " + (maxRetries + 1) + " attempts", null, lastException);
129
- }
130
167
 
131
- private async Task<byte[]> ExecuteRawWithRetriesAsync(
132
- Request request, int maxRetries, CancellationToken cancellationToken)
133
- {
134
- Exception? lastException = null;
135
- for (int attempt = 0; attempt <= maxRetries; attempt++)
136
- {
137
- if (attempt > 0)
168
+ var delay = GetRetryDelay(response, attempt);
169
+ response.Dispose();
170
+ await Task.Delay(delay, cancellationToken);
171
+ }
172
+ catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
138
173
  {
139
- var backoff = (int)Math.Pow(2, attempt - 1) * 200;
140
- var jitter = Random.Shared.Next(backoff / 2);
141
- await Task.Delay(backoff + jitter, cancellationToken);
174
+ throw;
142
175
  }
143
- try
176
+ catch (Exception error) when (IsNetworkError(error))
144
177
  {
145
- using var httpRequest = BuildRequest(request);
146
- using var response = await _httpClient.SendAsync(httpRequest, cancellationToken);
147
- var body = await response.Content.ReadAsByteArrayAsync(cancellationToken);
148
- if (response.IsSuccessStatusCode) return body;
149
- var bodyString = Encoding.UTF8.GetString(body);
150
- if ((int)response.StatusCode >= 500 && attempt < maxRetries)
178
+ if (!canRetry || attempt == _config.MaxRetries)
151
179
  {
152
- lastException = ParseErrorResponse(response, bodyString);
153
- continue;
180
+ throw new SdkException(
181
+ 0,
182
+ "REQUEST_ERROR",
183
+ "Network error: " + error.Message,
184
+ null,
185
+ error);
154
186
  }
155
- throw ParseErrorResponse(response, bodyString);
187
+
188
+ await Task.Delay(GetBackoff(attempt), cancellationToken);
156
189
  }
157
- catch (SdkException) { throw; }
158
- catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested) { throw; }
159
- catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; }
160
- catch (Exception ex) when (IsRetryable(ex) && attempt < maxRetries) { lastException = ex; }
161
- catch (Exception ex)
190
+ catch (SdkException)
162
191
  {
163
- throw new SdkException(0, "REQUEST_ERROR", ex.Message, null, ex);
192
+ throw;
193
+ }
194
+ catch (Exception error)
195
+ {
196
+ throw new SdkException(0, "REQUEST_ERROR", error.Message, null, error);
164
197
  }
165
198
  }
166
- throw new SdkException(0, "MAX_RETRIES_EXCEEDED",
167
- "Request failed after " + (maxRetries + 1) + " attempts", null, lastException);
199
+
200
+ throw new SdkException(
201
+ 0,
202
+ "MAX_RETRIES_EXCEEDED",
203
+ "Request failed after " + (_config.MaxRetries + 1) + " attempts",
204
+ null);
168
205
  }
169
206
 
170
207
  private HttpRequestMessage BuildRequest(Request request)
171
208
  {
172
- var baseUri = new Uri(_config.BaseUrl.TrimEnd('/') + "/");
173
- var fullUri = new Uri(baseUri, request.Path.TrimStart('/'));
209
+ var baseUri = new Uri(_config.BaseUrl.TrimEnd('/') + "/", UriKind.Absolute);
210
+ var builder = new UriBuilder(new Uri(baseUri, request.Path.TrimStart('/')));
211
+ var query = new List<string>();
174
212
 
175
- if (request.Query?.Count > 0)
213
+ if (!string.IsNullOrEmpty(builder.Query))
214
+ query.Add(builder.Query.TrimStart('?'));
215
+
216
+ if (request.Query != null)
176
217
  {
177
- var queryString = string.Join("&",
178
- request.Query.Select(kv =>
179
- Uri.EscapeDataString(kv.Key) + "=" + Uri.EscapeDataString(kv.Value)));
180
- var separator = fullUri.Query.Length > 0 ? "&" : "";
181
- fullUri = new Uri(fullUri, fullUri.PathAndQuery + separator + queryString);
218
+ query.AddRange(request.Query.Select(pair =>
219
+ Uri.EscapeDataString(pair.Key) + "=" + Uri.EscapeDataString(pair.Value)));
182
220
  }
183
221
 
184
- var httpRequest = new HttpRequestMessage(request.Method, fullUri);
222
+ {{#each securitySchemes}}
223
+ {{#if (eq type "apiKey")}}
224
+ {{#if (eq in "query")}}
225
+ if (!string.IsNullOrEmpty(_config.{{pascalCase configKey}}))
226
+ query.Add("{{paramName}}=" + Uri.EscapeDataString(_config.{{pascalCase configKey}}));
227
+ {{/if}}
228
+ {{/if}}
229
+ {{/each}}
230
+
231
+ builder.Query = string.Join("&", query.Where(value => !string.IsNullOrWhiteSpace(value)));
232
+ var httpRequest = new HttpRequestMessage(request.Method, builder.Uri);
233
+
234
+ httpRequest.Headers.TryAddWithoutValidation("Accept", "application/json");
235
+ httpRequest.Headers.TryAddWithoutValidation("User-Agent", "{{packageName}}/{{packageVersion}}");
185
236
 
186
237
  if (_config.DefaultHeaders != null)
187
238
  {
188
239
  foreach (var (key, value) in _config.DefaultHeaders)
189
- {
190
240
  httpRequest.Headers.TryAddWithoutValidation(key, value);
191
- }
192
241
  }
193
242
 
194
243
  {{#each securitySchemes}}
@@ -197,14 +246,6 @@ public class Transport
197
246
  if (!string.IsNullOrEmpty(_config.{{pascalCase configKey}}))
198
247
  httpRequest.Headers.TryAddWithoutValidation("{{paramName}}", _config.{{pascalCase configKey}});
199
248
  {{/if}}
200
- {{#if (eq in "query")}}
201
- if (!string.IsNullOrEmpty(_config.{{pascalCase configKey}}))
202
- {
203
- var sep = fullUri.Query.Length > 0 ? "&" : "";
204
- fullUri = new Uri(fullUri, fullUri.PathAndQuery + sep + "{{paramName}}=" + Uri.EscapeDataString(_config.{{pascalCase configKey}}));
205
- httpRequest.RequestUri = fullUri;
206
- }
207
- {{/if}}
208
249
  {{/if}}
209
250
  {{#if (and (eq type "http") (eq scheme "bearer"))}}
210
251
  if (!string.IsNullOrEmpty(_config.{{pascalCase configKey}}))
@@ -223,7 +264,7 @@ public class Transport
223
264
  if (request.Body != null)
224
265
  {
225
266
  var contentType = request.ContentType ?? "application/json";
226
- var json = JsonSerializer.Serialize(request.Body, _jsonOptions);
267
+ var json = JsonSerializer.Serialize(request.Body, JsonOptions);
227
268
  httpRequest.Content = new StringContent(json, Encoding.UTF8, contentType);
228
269
  }
229
270
 
@@ -232,28 +273,66 @@ public class Transport
232
273
  foreach (var (key, value) in request.Headers)
233
274
  {
234
275
  if (key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase) && httpRequest.Content != null)
235
- {
236
276
  httpRequest.Content.Headers.ContentType =
237
277
  new System.Net.Http.Headers.MediaTypeHeaderValue(value);
238
- }
239
278
  else
240
- {
241
279
  httpRequest.Headers.TryAddWithoutValidation(key, value);
242
- }
243
280
  }
244
281
  }
245
282
 
246
283
  return httpRequest;
247
284
  }
248
285
 
249
- private static bool IsRetryable(Exception ex)
286
+ private bool CanRetry(Request request)
287
+ {
288
+ if (_config.RetryUnsafeRequests ||
289
+ request.Method == HttpMethod.Get ||
290
+ request.Method == HttpMethod.Head ||
291
+ request.Method == HttpMethod.Options ||
292
+ request.Method == HttpMethod.Put ||
293
+ request.Method == HttpMethod.Delete)
294
+ {
295
+ return true;
296
+ }
297
+
298
+ return _config.DefaultHeaders?.Any(pair =>
299
+ pair.Key.Equals("X-Idempotency-Key", StringComparison.OrdinalIgnoreCase) &&
300
+ !string.IsNullOrEmpty(pair.Value)) == true ||
301
+ request.Headers?.Any(pair =>
302
+ pair.Key.Equals("X-Idempotency-Key", StringComparison.OrdinalIgnoreCase) &&
303
+ !string.IsNullOrEmpty(pair.Value)) == true;
304
+ }
305
+
306
+ private static TimeSpan GetRetryDelay(HttpResponseMessage response, int attempt)
250
307
  {
251
- return ex is HttpRequestException ||
252
- ex is TaskCanceledException ||
253
- ex is IOException ||
254
- ex.Message.Contains("connection") ||
255
- ex.Message.Contains("timeout") ||
256
- ex.Message.Contains("reset");
308
+ var retryAfter = response.Headers.RetryAfter;
309
+ if (retryAfter?.Delta is { } delta)
310
+ return ClampDelay(delta);
311
+ if (retryAfter?.Date is { } date)
312
+ return ClampDelay(date - DateTimeOffset.UtcNow);
313
+ return GetBackoff(attempt);
314
+ }
315
+
316
+ private static TimeSpan GetBackoff(int attempt)
317
+ {
318
+ var exponential = Math.Min(30_000, 200 * Math.Pow(2, Math.Min(attempt, 10)));
319
+ var jitter = Random.Shared.NextDouble() * exponential / 2;
320
+ return TimeSpan.FromMilliseconds(Math.Min(30_000, exponential + jitter));
321
+ }
322
+
323
+ private static TimeSpan ClampDelay(TimeSpan delay)
324
+ {
325
+ if (delay < TimeSpan.Zero) return TimeSpan.Zero;
326
+ return delay > TimeSpan.FromSeconds(30) ? TimeSpan.FromSeconds(30) : delay;
327
+ }
328
+
329
+ private static bool IsNetworkError(Exception error) =>
330
+ error is HttpRequestException or IOException or TaskCanceledException or TimeoutException;
331
+
332
+ private static void EnsureSuccess(HttpResponseMessage response, string body)
333
+ {
334
+ if (!response.IsSuccessStatusCode)
335
+ throw ParseErrorResponse(response, body);
257
336
  }
258
337
 
259
338
  private static SdkException ParseErrorResponse(HttpResponseMessage response, string body)
@@ -261,31 +340,54 @@ public class Transport
261
340
  var requestId = GetRequestId(response);
262
341
  try
263
342
  {
264
- using var doc = JsonDocument.Parse(body);
265
- var root = doc.RootElement;
266
- var code = "";
267
- var message = "";
268
- if (root.TryGetProperty("code", out var codeEl)) code = codeEl.GetString() ?? "";
269
- if (root.TryGetProperty("message", out var msgEl)) message = msgEl.GetString() ?? "";
270
- else if (root.TryGetProperty("error", out var errEl)) message = errEl.GetString() ?? "";
271
- if (string.IsNullOrEmpty(message)) message = body.Trim();
343
+ using var document = JsonDocument.Parse(body);
344
+ var root = document.RootElement;
345
+ var code = ReadString(root, "code");
346
+ var message = ReadString(root, "message");
347
+
348
+ if (root.TryGetProperty("error", out var error))
349
+ {
350
+ if (error.ValueKind == JsonValueKind.Object)
351
+ {
352
+ code ??= ReadString(error, "code");
353
+ message ??= ReadString(error, "message");
354
+ }
355
+ else if (error.ValueKind == JsonValueKind.String)
356
+ {
357
+ message ??= error.GetString();
358
+ }
359
+ }
360
+
361
+ message ??= string.IsNullOrWhiteSpace(body)
362
+ ? "HTTP " + (int)response.StatusCode + ": " + response.ReasonPhrase
363
+ : body.Trim();
364
+
272
365
  return new SdkException((int)response.StatusCode, code, message, requestId);
273
366
  }
274
367
  catch (JsonException)
275
368
  {
276
- return new SdkException((int)response.StatusCode, "",
369
+ return new SdkException(
370
+ (int)response.StatusCode,
371
+ null,
277
372
  string.IsNullOrWhiteSpace(body)
278
373
  ? "HTTP " + (int)response.StatusCode + ": " + response.ReasonPhrase
279
- : body.Trim(), requestId);
374
+ : body.Trim(),
375
+ requestId);
280
376
  }
281
377
  }
282
378
 
379
+ private static string? ReadString(JsonElement value, string property) =>
380
+ value.TryGetProperty(property, out var element) && element.ValueKind == JsonValueKind.String
381
+ ? element.GetString()
382
+ : null;
383
+
283
384
  private static string? GetRequestId(HttpResponseMessage response)
284
385
  {
285
- if (response.Headers.TryGetValues("X-Request-ID", out var values))
286
- return values.FirstOrDefault();
287
- if (response.Headers.TryGetValues("x-request-id", out values))
288
- return values.FirstOrDefault();
386
+ foreach (var name in new[] { "x-request-id", "request-id" })
387
+ {
388
+ if (response.Headers.TryGetValues(name, out var values))
389
+ return values.FirstOrDefault();
390
+ }
289
391
  return null;
290
392
  }
291
393
  }
@@ -2,103 +2,57 @@ namespace {{namespace}};
2
2
 
3
3
  using System;
4
4
  using System.Collections.Generic;
5
+ using System.Runtime.Serialization;
5
6
  using System.Text.Json;
6
7
  using System.Text.Json.Serialization;
7
8
 
8
9
  {{#each schemas}}
9
- {{#if (neq ownership.kind "external")}}
10
- {{#if isEnum}}
11
- /// <summary>
12
- {{#if description}}
13
- /// {{description}}
14
- {{else}}
15
- /// {{name}} enumeration.
16
- {{/if}}
17
- /// </summary>
10
+ {{#if external}}
11
+ /// <summary>External schema placeholder for {{targetName}}.</summary>
12
+ public sealed class {{targetName}}
13
+ {
14
+ public JsonElement Value { get; set; }
15
+ }
16
+
17
+ {{else if isEnum}}
18
+ /// <summary>{{#if description}}{{description}}{{else}}{{targetName}} enumeration.{{/if}}</summary>
18
19
  [JsonConverter(typeof(JsonStringEnumConverter))]
19
- public enum {{name}}
20
+ public enum {{targetName}}
20
21
  {
21
22
  {{#each enumValues}}
22
- /// <summary>
23
- /// {{this}} value.
24
- /// </summary>
25
- [JsonStringEnumMemberName("{{this}}")]
26
- {{pascalCase ../name}}{{pascalCase this}},
23
+ [EnumMember(Value = "{{value}}")] {{name}},
27
24
  {{/each}}
28
25
  }
29
26
 
30
27
  {{else if isUnionType}}
31
- /// <summary>
32
- {{#if description}}
33
- /// {{description}}
34
- {{else}}
35
- /// {{name}} union type.
36
- {{/if}}
37
- /// </summary>
38
- public class {{name}}
28
+ /// <summary>{{#if description}}{{description}}{{else}}{{targetName}} union value.{{/if}}</summary>
29
+ public sealed class {{targetName}}
39
30
  {
40
- private readonly object _value;
41
- private readonly Type _type;
42
-
43
- {{#each unionMembers}}
44
- /// <summary>
45
- /// Creates a {{../name}} from a {{this}} value.
46
- /// </summary>
47
- public {{../name}}({{this}} value)
48
- {
49
- _value = value!;
50
- _type = typeof({{this}});
51
- }
52
-
53
- {{/each}}
31
+ public {{targetName}}(object value) => Value = value;
32
+ public object Value { get; }
33
+ }
54
34
 
55
- /// <summary>
56
- /// Gets the underlying value.
57
- /// </summary>
58
- public object Value => _value;
35
+ {{else if isTypeAlias}}
36
+ // {{name}} is emitted inline as {{{targetAliasType}}}.
59
37
 
60
- /// <summary>
61
- /// Attempts to get the value as the specified type.
62
- /// </summary>
63
- public bool TryGet<T>(out T? value)
64
- {
65
- if (_value is T v)
66
- {
67
- value = v;
68
- return true;
69
- }
70
- value = default;
71
- return false;
72
- }
38
+ {{else if isIntersectionType}}
39
+ /// <summary>{{#if description}}{{description}}{{else}}{{targetName}} intersection value.{{/if}}</summary>
40
+ public sealed class {{targetName}}
41
+ {
42
+ public Dictionary<string, JsonElement> Values { get; set; } = new();
73
43
  }
74
44
 
75
45
  {{else}}
76
- /// <summary>
77
- {{#if description}}
78
- /// {{description}}
79
- {{else}}
80
- /// {{name}} schema type.
81
- {{/if}}
82
- /// </summary>
83
- public class {{name}}
46
+ /// <summary>{{#if description}}{{description}}{{else}}{{targetName}} schema type.{{/if}}</summary>
47
+ public class {{targetName}}{{#if targetExtends}} : {{targetExtends}}{{/if}}
84
48
  {
85
49
  {{#each properties}}
86
- /// <summary>
87
- {{#if description}}
88
- /// {{description}}
89
- {{/if}}
90
- /// </summary>
91
- [JsonPropertyName("{{name}}")]
92
- {{#if (or nullable (not required))}}
93
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
94
- {{/if}}
95
- {{#if deprecated}}
96
- [Obsolete]
97
- {{/if}}
98
- public {{#if (eq type "string")}}{{#if (eq format "date-time")}}DateTime{{else}}string{{/if}}{{else if (eq type "integer")}}int{{else if (eq type "number")}}double{{else if (eq type "boolean")}}bool{{else if (eq type "array")}}List<object>?{{else}}{{type}}{{/if}}{{#if (or nullable (not required))}}?{{/if}} {{pascalCase name}} { get; set; }
50
+ [JsonPropertyName("{{name}}")]{{#if optional}}
51
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]{{/if}}{{#if deprecated}}
52
+ [Obsolete]{{/if}}
53
+ public {{targetType}} {{pascalCase name}} { get; set; }{{{initializer}}}
99
54
  {{/each}}
100
55
  }
101
- {{/if}}
102
56
 
103
57
  {{/if}}
104
58
  {{/each}}