@apollo-deploy/tesseract 1.9.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 +13 -3
  67. package/dist/collector.d.ts.map +1 -1
  68. package/dist/collector.js +69 -32
  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 +7 -0
  75. package/dist/fastify.d.ts.map +1 -1
  76. package/dist/fastify.js +39 -20
  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
@@ -1,76 +1,63 @@
1
- # {{title}}
1
+ # {{title}} Rust SDK
2
2
 
3
- {{#if description}}{{description}}{{/if}}
3
+ {{#if description}}{{description}}{{else}}Rust client for the {{title}} API.{{/if}}
4
4
 
5
- {{#unless description}}An auto-generated Rust SDK for the {{title}} API.{{/unless}}
5
+ > Generated by Tesseract. Generated source files should not be edited manually.
6
6
 
7
7
  ## Installation
8
8
 
9
- Add this to your `Cargo.toml`:
10
-
11
9
  ```toml
12
10
  [dependencies]
13
- {{crateName}} = "{{#if packageVersion}}{{packageVersion}}{{else}}0.1.0{{/if}}"
11
+ {{crateName}} = "{{packageVersion}}"
14
12
  ```
15
13
 
16
- ## Quick Start
14
+ ## Quick start
17
15
 
18
16
  ```rust
19
- use {{crateName}}::Client;
20
- use {{crateName}}::ClientConfig;
17
+ use {{crateName}}::{Client, ClientConfig};
21
18
 
22
19
  #[tokio::main]
23
20
  async fn main() -> Result<(), Box<dyn std::error::Error>> {
24
- let mut config = ClientConfig::default();
25
- {{#each securitySchemes}}
26
- {{#if (eq type "apiKey")}}
27
- config.{{snakeCase configKey}} = Some("your-api-key".to_string());
28
- {{/if}}
29
- {{#if (and (eq type "http") (eq scheme "bearer"))}}
30
- config.{{snakeCase configKey}} = Some("your-bearer-token".to_string());
21
+ let client = Client::new(ClientConfig::default());
22
+ {{#if groups.length}}
23
+ let _api = &client.{{snakeCase groups.[0].name}};
31
24
  {{/if}}
32
- {{/each}}
33
-
34
- let client = Client::new(config);
35
-
36
- // Use domain clients
37
- {{#each groups}}
38
- {{#if @first}}
39
- // let result = client.{{snakeCase name}}.some_operation().await?;
40
- {{/if}}
41
- {{/each}}
42
-
43
25
  Ok(())
44
26
  }
45
27
  ```
46
28
 
47
- ## API Reference
29
+ ## Configuration and reliability
48
30
 
49
- {{#each groups}}
50
- ### {{interfaceName}}
31
+ The generated client supports bounded timeouts, exponential-backoff retries, `Retry-After` handling, structured `SdkError` values, and safe client reuse. Unsafe requests are not retried unless they are explicitly made idempotent or unsafe retries are enabled.
51
32
 
52
- {{#if description}}
53
- {{description}}
54
- {{/if}}
33
+ ```rust
34
+ use std::time::Duration;
55
35
 
56
- | Method | HTTP | Description |
57
- |--------|------|-------------|
58
- {{#each operations}}
59
- | `{{snakeCase name}}()` | `{{httpMethod}}` | {{#if summary}}{{summary}}{{else}}—{{/if}} |
60
- {{/each}}
36
+ let config = ClientConfig {
37
+ timeout: Duration::from_secs(30),
38
+ max_retries: 3,
39
+ ..Default::default()
40
+ };
41
+ ```
61
42
 
43
+ ## Documentation
44
+
45
+ - [Documentation index](./docs/README.md)
46
+ - [Generated types](./docs/types.md)
47
+
48
+ | Domain | Description |
49
+ |---|---|
50
+ {{#each groups}}
51
+ | [{{interfaceName}}](./docs/domains/{{fileName}}.md) | {{#if description}}{{description}}{{else}}{{name}} operations{{/if}} |
62
52
  {{/each}}
63
53
 
64
- ## Error Handling
54
+ ## Error handling
55
+
56
+ All operations return `Result<T, SdkError>`. API errors retain the HTTP status, machine-readable code, request ID, and response body where available.
65
57
 
66
- All API methods return `Result<T, SdkError>`. The `SdkError` enum has these variants:
58
+ ## Requirements
67
59
 
68
- | Variant | Description |
69
- |---------|-------------|
70
- | `ApiError` | Server returned an error (4xx/5xx) |
71
- | `NetworkError` | Request failed due to connectivity issues |
72
- | `DeserializationError` | Response body could not be parsed |
73
- | `ValidationError` | Invalid request parameters |
60
+ - Rust 1.75 or later
74
61
 
75
62
  ## License
76
63
 
@@ -2,11 +2,11 @@ use crate::error::SdkError;
2
2
  use reqwest::{Client as ReqwestClient, Method, RequestBuilder, Response, StatusCode};
3
3
  use serde::de::DeserializeOwned;
4
4
  use serde::Serialize;
5
+ use std::collections::HashMap;
5
6
  use std::sync::Arc;
6
- use std::time::Duration;
7
+ use std::time::{Duration, SystemTime, UNIX_EPOCH};
7
8
  use url::Url;
8
9
 
9
- /// Internal transport configuration.
10
10
  #[derive(Debug, Clone)]
11
11
  pub struct TransportConfig {
12
12
  pub base_url: String,
@@ -15,13 +15,10 @@ pub struct TransportConfig {
15
15
  {{/each}}
16
16
  pub timeout: Duration,
17
17
  pub max_retries: u32,
18
+ pub retry_unsafe_requests: bool,
19
+ pub default_headers: HashMap<String, String>,
18
20
  }
19
21
 
20
- /// HTTP transport layer for the SDK.
21
- ///
22
- /// Handles request building, authentication, retries, and error mapping.
23
- /// Cloning a `Transport` produces a shallow clone that shares the underlying
24
- /// reqwest client.
25
22
  #[derive(Clone)]
26
23
  pub struct Transport {
27
24
  client: ReqwestClient,
@@ -29,20 +26,15 @@ pub struct Transport {
29
26
  }
30
27
 
31
28
  impl Transport {
32
- /// Create a new transport with the given configuration.
33
29
  pub fn new(config: TransportConfig) -> Self {
34
30
  let client = ReqwestClient::builder()
35
31
  .timeout(config.timeout)
32
+ .user_agent("{{crateName}}-rust-sdk/{{packageVersion}}")
36
33
  .build()
37
34
  .expect("failed to build reqwest client");
38
-
39
- Self {
40
- client,
41
- config: Arc::new(config),
42
- }
35
+ Self { client, config: Arc::new(config) }
43
36
  }
44
37
 
45
- /// Execute a request, deserializing the response body as JSON.
46
38
  pub async fn execute<T: DeserializeOwned>(
47
39
  &self,
48
40
  method: Method,
@@ -51,23 +43,11 @@ impl Transport {
51
43
  body: Option<&(impl Serialize + ?Sized)>,
52
44
  headers: Option<&[(String, String)]>,
53
45
  ) -> Result<T, SdkError> {
54
- let url = self.build_url(path, query)?;
55
-
56
- let mut req = self.client.request(method, url);
57
-
58
- req = self.apply_auth(req);
59
- req = self.apply_headers(req, headers);
60
-
61
- if let Some(b) = body {
62
- req = req.json(b);
63
- }
64
-
65
- let response = self.execute_with_retry(req).await?;
66
-
46
+ let request = self.build_request(method.clone(), path, query, body, headers)?;
47
+ let response = self.execute_with_retry(request, &method, headers).await?;
67
48
  self.handle_response(response).await
68
49
  }
69
50
 
70
- /// Execute a request that returns the raw response text.
71
51
  pub async fn execute_raw(
72
52
  &self,
73
53
  method: Method,
@@ -76,212 +56,243 @@ impl Transport {
76
56
  body: Option<&(impl Serialize + ?Sized)>,
77
57
  headers: Option<&[(String, String)]>,
78
58
  ) -> Result<String, SdkError> {
79
- let url = self.build_url(path, query)?;
80
-
81
- let mut req = self.client.request(method, url);
82
-
83
- req = self.apply_auth(req);
84
- req = self.apply_headers(req, headers);
85
-
86
- if let Some(b) = body {
87
- req = req.json(b);
88
- }
89
-
90
- let response = self.execute_with_retry(req).await?;
91
-
59
+ let request = self.build_request(method.clone(), path, query, body, headers)?;
60
+ let response = self.execute_with_retry(request, &method, headers).await?;
92
61
  let status = response.status();
93
- let text = response.text().await.map_err(|e| {
94
- SdkError::DeserializationError(format!("failed to read response body: {}", e))
62
+ let request_id = response
63
+ .headers()
64
+ .get("x-request-id")
65
+ .or_else(|| response.headers().get("request-id"))
66
+ .and_then(|value| value.to_str().ok())
67
+ .map(String::from);
68
+ let text = response.text().await.map_err(|error| {
69
+ SdkError::DeserializationError(format!("failed to read response body: {}", error))
95
70
  })?;
96
-
97
71
  if status.is_success() {
98
72
  Ok(text)
99
73
  } else {
100
- Err(self.build_api_error(status, &text))
74
+ Err(Self::build_api_error(status, request_id, &text))
101
75
  }
102
76
  }
103
77
 
104
- // ── private helpers ─────────────────────────────────────────────────────
78
+ fn build_request(
79
+ &self,
80
+ method: Method,
81
+ path: &str,
82
+ query: Option<&[(String, String)]>,
83
+ body: Option<&(impl Serialize + ?Sized)>,
84
+ headers: Option<&[(String, String)]>,
85
+ ) -> Result<RequestBuilder, SdkError> {
86
+ let url = self.build_url(path, query)?;
87
+ let mut request = self.client.request(method, url);
88
+ request = self.apply_auth(request);
89
+ request = self.apply_headers(request, headers);
90
+ if let Some(body) = body {
91
+ request = request.json(body);
92
+ }
93
+ Ok(request)
94
+ }
105
95
 
106
96
  fn build_url(
107
97
  &self,
108
98
  path: &str,
109
99
  query: Option<&[(String, String)]>,
110
100
  ) -> Result<Url, SdkError> {
111
- let base = Url::parse(&self.config.base_url).map_err(|e| {
112
- SdkError::ValidationError(format!("invalid base URL: {}", e))
113
- })?;
114
-
115
- let joined = base.join(path).map_err(|e| {
116
- SdkError::ValidationError(format!("invalid path '{}': {}", path, e))
117
- })?;
118
-
101
+ let base = Url::parse(&self.config.base_url)
102
+ .map_err(|error| SdkError::ValidationError(format!("invalid base URL: {}", error)))?;
103
+ let mut url = base
104
+ .join(path)
105
+ .map_err(|error| SdkError::ValidationError(format!("invalid path '{}': {}", path, error)))?;
119
106
  if let Some(params) = query {
120
- let mut url = joined;
121
- {
122
- let mut qp = url.query_pairs_mut();
123
- for (k, v) in params {
124
- qp.append_pair(k, v);
125
- }
107
+ let mut pairs = url.query_pairs_mut();
108
+ for (key, value) in params {
109
+ pairs.append_pair(key, value);
126
110
  }
127
- Ok(url)
128
- } else {
129
- Ok(joined)
130
111
  }
112
+ Ok(url)
131
113
  }
132
114
 
133
- fn apply_auth(&self, mut req: RequestBuilder) -> RequestBuilder {
115
+ fn apply_auth(&self, mut request: RequestBuilder) -> RequestBuilder {
134
116
  {{#each securitySchemes}}
135
117
  {{#if (eq type "apiKey")}}
136
118
  {{#if (eq in "header")}}
137
119
  if let Some(ref key) = self.config.{{snakeCase configKey}} {
138
- req = req.header("{{paramName}}", key);
120
+ request = request.header("{{paramName}}", key);
139
121
  }
140
122
  {{/if}}
141
- {{#if (eq in "query")}}
142
- // API key via query parameter handled during URL construction
143
123
  {{/if}}
144
- {{/if}}
145
- {{#if (and (eq type "http") (eq scheme "bearer"))}}
146
- if let Some(ref token) = self.config.{{snakeCase configKey}} {
147
- req = req.header("Authorization", format!("Bearer {}", token));
148
- }
149
- {{/if}}
150
- {{#if (eq type "oauth2")}}
124
+ {{#if (or (and (eq type "http") (eq scheme "bearer")) (eq type "oauth2") (eq type "openIdConnect"))}}
151
125
  if let Some(ref token) = self.config.{{snakeCase configKey}} {
152
- req = req.header("Authorization", format!("Bearer {}", token));
153
- }
154
- {{/if}}
155
- {{#if (eq type "openIdConnect")}}
156
- if let Some(ref token) = self.config.{{snakeCase configKey}} {
157
- req = req.header("Authorization", format!("Bearer {}", token));
126
+ request = request.bearer_auth(token);
158
127
  }
159
128
  {{/if}}
160
129
  {{/each}}
161
- req
130
+ request
162
131
  }
163
132
 
164
133
  fn apply_headers(
165
134
  &self,
166
- mut req: RequestBuilder,
135
+ mut request: RequestBuilder,
167
136
  headers: Option<&[(String, String)]>,
168
137
  ) -> RequestBuilder {
169
- if let Some(hdrs) = headers {
170
- for (key, value) in hdrs {
171
- req = req.header(key.as_str(), value.as_str());
138
+ request = request.header("Accept", "application/json");
139
+ for (key, value) in &self.config.default_headers {
140
+ request = request.header(key, value);
141
+ }
142
+ if let Some(headers) = headers {
143
+ for (key, value) in headers {
144
+ request = request.header(key, value);
172
145
  }
173
146
  }
174
- req
147
+ request
175
148
  }
176
149
 
177
150
  async fn execute_with_retry(
178
151
  &self,
179
- req: RequestBuilder,
152
+ request: RequestBuilder,
153
+ method: &Method,
154
+ headers: Option<&[(String, String)]>,
180
155
  ) -> Result<Response, SdkError> {
181
- let max_retries = self.config.max_retries;
156
+ let can_retry = self.can_retry(method, headers);
182
157
  let mut attempt = 0u32;
183
158
 
184
159
  loop {
185
- let request = req
186
- .try_clone()
187
- .ok_or_else(|| SdkError::NetworkError("failed to clone request for retry".to_string()))?;
188
-
189
- match request.send().await {
160
+ let current = request.try_clone().ok_or_else(|| {
161
+ SdkError::NetworkError("request body cannot be cloned for execution".to_string())
162
+ })?;
163
+ match current.send().await {
190
164
  Ok(response) => {
191
165
  let status = response.status();
192
-
193
- // Retry on server errors (5xx) and 429 Too Many Requests
194
- if status.is_server_error() || status == StatusCode::TOO_MANY_REQUESTS {
195
- if attempt < max_retries {
196
- attempt += 1;
197
- let backoff = Duration::from_millis(
198
- 200u64 * 2u64.pow(attempt),
199
- );
200
- tokio::time::sleep(backoff).await;
201
- continue;
202
- }
166
+ if can_retry && is_retryable_status(status) && attempt < self.config.max_retries {
167
+ let delay = retry_delay(response.headers().get("retry-after"), attempt);
168
+ attempt += 1;
169
+ tokio::time::sleep(delay).await;
170
+ continue;
203
171
  }
204
172
  return Ok(response);
205
173
  }
206
- Err(e) => {
207
- if attempt < max_retries && e.is_connect() || e.is_timeout() {
174
+ Err(error) => {
175
+ if can_retry
176
+ && attempt < self.config.max_retries
177
+ && (error.is_connect() || error.is_timeout() || error.is_request())
178
+ {
179
+ let delay = retry_delay(None, attempt);
208
180
  attempt += 1;
209
- let backoff = Duration::from_millis(
210
- 200u64 * 2u64.pow(attempt),
211
- );
212
- tokio::time::sleep(backoff).await;
181
+ tokio::time::sleep(delay).await;
213
182
  continue;
214
183
  }
215
- return Err(SdkError::NetworkError(e.to_string()));
184
+ return Err(SdkError::NetworkError(error.to_string()));
216
185
  }
217
186
  }
218
187
  }
219
188
  }
220
189
 
221
- async fn handle_response<T: DeserializeOwned>(
222
- &self,
223
- response: Response,
224
- ) -> Result<T, SdkError> {
190
+ fn can_retry(&self, method: &Method, headers: Option<&[(String, String)]>) -> bool {
191
+ if self.config.retry_unsafe_requests
192
+ || method == Method::GET
193
+ || method == Method::HEAD
194
+ || method == Method::OPTIONS
195
+ || method == Method::PUT
196
+ || method == Method::DELETE
197
+ {
198
+ return true;
199
+ }
200
+
201
+ self.config
202
+ .default_headers
203
+ .iter()
204
+ .chain(headers.unwrap_or_default().iter())
205
+ .any(|(name, value)| name.eq_ignore_ascii_case("x-idempotency-key") && !value.is_empty())
206
+ }
207
+
208
+ async fn handle_response<T: DeserializeOwned>(&self, response: Response) -> Result<T, SdkError> {
225
209
  let status = response.status();
226
- let text = response.text().await.map_err(|e| {
227
- SdkError::DeserializationError(format!("failed to read response body: {}", e))
210
+ let request_id = response
211
+ .headers()
212
+ .get("x-request-id")
213
+ .or_else(|| response.headers().get("request-id"))
214
+ .and_then(|value| value.to_str().ok())
215
+ .map(String::from);
216
+ let text = response.text().await.map_err(|error| {
217
+ SdkError::DeserializationError(format!("failed to read response body: {}", error))
228
218
  })?;
229
-
230
219
  if status.is_success() {
231
- serde_json::from_str(&text).map_err(|e| {
232
- SdkError::DeserializationError(format!(
233
- "failed to deserialize response (status {}): {}",
234
- status, e
235
- ))
236
- })
220
+ if text.trim().is_empty() {
221
+ serde_json::from_str("null").map_err(|error| {
222
+ SdkError::DeserializationError(format!("empty response could not be decoded: {}", error))
223
+ })
224
+ } else {
225
+ serde_json::from_str(&text).map_err(|error| {
226
+ SdkError::DeserializationError(format!(
227
+ "failed to deserialize response (status {}): {}",
228
+ status, error
229
+ ))
230
+ })
231
+ }
237
232
  } else {
238
- Err(self.build_api_error(status, &text))
233
+ Err(Self::build_api_error(status, request_id, &text))
239
234
  }
240
235
  }
241
236
 
242
- fn build_api_error(&self, status: StatusCode, body: &str) -> SdkError {
243
- // Try to extract structured error info from the JSON body
244
- let code = serde_json::from_str::<serde_json::Value>(body)
245
- .ok()
246
- .and_then(|v| {
247
- v.get("code")
248
- .or_else(|| v.get("error"))
249
- .and_then(|c| c.as_str().map(String::from))
250
- .or_else(|| {
251
- v.get("error")
252
- .and_then(|e| e.get("code"))
253
- .and_then(|c| c.as_str().map(String::from))
254
- })
255
- });
256
-
257
- let message = serde_json::from_str::<serde_json::Value>(body)
258
- .ok()
259
- .and_then(|v| {
260
- v.get("message")
261
- .or_else(|| v.get("error"))
262
- .and_then(|m| {
263
- if m.is_string() {
264
- m.as_str().map(String::from)
265
- } else {
266
- m.get("message").and_then(|s| s.as_str().map(String::from))
267
- }
268
- })
269
- })
270
- .unwrap_or_else(|| body.to_string());
271
-
237
+ fn build_api_error(status: StatusCode, request_id: Option<String>, body: &str) -> SdkError {
238
+ let parsed = serde_json::from_str::<serde_json::Value>(body).ok();
239
+ let nested = parsed.as_ref().and_then(|value| value.get("error"));
240
+ let code = parsed
241
+ .as_ref()
242
+ .and_then(|value| value.get("code"))
243
+ .or_else(|| nested.and_then(|value| value.get("code")))
244
+ .or_else(|| nested.filter(|value| value.is_string()))
245
+ .and_then(|value| value.as_str())
246
+ .map(String::from);
247
+ let message = parsed
248
+ .as_ref()
249
+ .and_then(|value| value.get("message"))
250
+ .or_else(|| nested.and_then(|value| value.get("message")))
251
+ .or_else(|| nested.filter(|value| value.is_string()))
252
+ .and_then(|value| value.as_str())
253
+ .unwrap_or(body)
254
+ .to_string();
255
+ let body_request_id = parsed
256
+ .as_ref()
257
+ .and_then(|value| value.get("request_id"))
258
+ .and_then(|value| value.as_str())
259
+ .map(String::from);
272
260
  SdkError::ApiError {
273
261
  status: status.as_u16(),
274
262
  code,
275
263
  message,
264
+ request_id: body_request_id.or(request_id),
276
265
  body: body.to_string(),
277
266
  }
278
267
  }
279
268
  }
280
269
 
270
+ fn is_retryable_status(status: StatusCode) -> bool {
271
+ matches!(status.as_u16(), 408 | 425 | 429 | 500 | 502 | 503 | 504)
272
+ }
273
+
274
+ fn retry_delay(retry_after: Option<&reqwest::header::HeaderValue>, attempt: u32) -> Duration {
275
+ if let Some(value) = retry_after.and_then(|value| value.to_str().ok()) {
276
+ if let Ok(seconds) = value.parse::<f64>() {
277
+ return Duration::from_secs_f64(seconds.max(0.0).min(30.0));
278
+ }
279
+ if let Ok(time) = httpdate::parse_http_date(value) {
280
+ if let Ok(delay) = time.duration_since(SystemTime::now()) {
281
+ return delay.min(Duration::from_secs(30));
282
+ }
283
+ }
284
+ }
285
+ let base = (400u64.saturating_mul(2u64.saturating_pow(attempt))).min(30_000);
286
+ let jitter = SystemTime::now()
287
+ .duration_since(UNIX_EPOCH)
288
+ .unwrap_or_default()
289
+ .subsec_nanos() as u64
290
+ % (base / 2 + 1);
291
+ Duration::from_millis((base + jitter).min(30_000))
292
+ }
293
+
281
294
  impl std::fmt::Debug for Transport {
282
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283
- f.debug_struct("Transport")
284
- .field("config", &self.config)
285
- .finish()
295
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296
+ formatter.debug_struct("Transport").field("config", &self.config).finish()
286
297
  }
287
298
  }