@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.
- package/dist/adapters/csharp/index.d.ts +12 -0
- package/dist/adapters/csharp/index.d.ts.map +1 -0
- package/dist/adapters/csharp/index.js +99 -0
- package/dist/adapters/csharp/index.js.map +1 -0
- package/dist/adapters/go/index.d.ts +12 -0
- package/dist/adapters/go/index.d.ts.map +1 -0
- package/dist/adapters/go/index.js +135 -0
- package/dist/adapters/go/index.js.map +1 -0
- package/dist/adapters/kotlin/index.d.ts +13 -0
- package/dist/adapters/kotlin/index.d.ts.map +1 -0
- package/dist/adapters/kotlin/index.js +186 -0
- package/dist/adapters/kotlin/index.js.map +1 -0
- package/dist/adapters/php/index.d.ts +20 -0
- package/dist/adapters/php/index.d.ts.map +1 -0
- package/dist/adapters/php/index.js +123 -0
- package/dist/adapters/php/index.js.map +1 -0
- package/dist/adapters/python/index.d.ts +12 -0
- package/dist/adapters/python/index.d.ts.map +1 -0
- package/dist/adapters/python/index.js +145 -0
- package/dist/adapters/python/index.js.map +1 -0
- package/dist/adapters/ruby/index.d.ts +12 -0
- package/dist/adapters/ruby/index.d.ts.map +1 -0
- package/dist/adapters/ruby/index.js +86 -0
- package/dist/adapters/ruby/index.js.map +1 -0
- package/dist/adapters/rust/index.d.ts +13 -0
- package/dist/adapters/rust/index.d.ts.map +1 -0
- package/dist/adapters/rust/index.js +213 -0
- package/dist/adapters/rust/index.js.map +1 -0
- package/dist/adapters/typescript/emitter/emitSchema.d.ts +1 -1
- package/dist/adapters/typescript/emitter/emitSchema.d.ts.map +1 -1
- package/dist/adapters/typescript/emitter/emitSchema.js +1 -1
- package/dist/adapters/typescript/emitter/emitSchema.js.map +1 -1
- package/dist/adapters/typescript/emitter/emitTypeScriptModels.d.ts +2 -2
- package/dist/adapters/typescript/emitter/emitTypeScriptModels.d.ts.map +1 -1
- package/dist/adapters/typescript/emitter/emitTypeScriptModels.js +2 -2
- package/dist/adapters/typescript/emitter/emitTypeScriptModels.js.map +1 -1
- package/dist/adapters/typescript/emitter/importCollector.d.ts +1 -1
- package/dist/adapters/typescript/emitter/importCollector.d.ts.map +1 -1
- package/dist/cli.js +2 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -1
- package/dist/pipeline/intake.js +23 -16
- package/dist/pipeline/intake.js.map +1 -1
- package/dist/types/ir.d.ts +1 -1
- package/dist/types/ir.d.ts.map +1 -1
- package/dist/types/sdk-module.d.ts +22 -2
- package/dist/types/sdk-module.d.ts.map +1 -1
- package/dist/types/sdk-module.js +1 -1
- package/package.json +1 -1
- package/templates/csharp/client.hbs +95 -0
- package/templates/csharp/csproj.hbs +18 -0
- package/templates/csharp/domain.hbs +183 -0
- package/templates/csharp/errors.hbs +45 -0
- package/templates/csharp/readme.hbs +76 -0
- package/templates/csharp/transport.hbs +291 -0
- package/templates/csharp/types.hbs +104 -0
- package/templates/go/client.hbs +158 -0
- package/templates/go/domain.hbs +89 -0
- package/templates/go/errors.hbs +38 -0
- package/templates/go/go-mod.hbs +6 -0
- package/templates/go/readme.hbs +73 -0
- package/templates/go/transport.hbs +379 -0
- package/templates/go/types.hbs +80 -0
- package/templates/go/utils.hbs +45 -0
- package/templates/kotlin/build-gradle.hbs +41 -0
- package/templates/kotlin/client.hbs +57 -0
- package/templates/kotlin/domain.hbs +73 -0
- package/templates/kotlin/errors.hbs +42 -0
- package/templates/kotlin/gradle-properties.hbs +2 -0
- package/templates/kotlin/readme.hbs +105 -0
- package/templates/kotlin/settings-gradle.hbs +1 -0
- package/templates/kotlin/transport.hbs +246 -0
- package/templates/kotlin/types.hbs +100 -0
- package/templates/php/client.hbs +74 -0
- package/templates/php/composer.hbs +19 -0
- package/templates/php/domain.hbs +173 -0
- package/templates/php/errors.hbs +131 -0
- package/templates/php/readme.hbs +89 -0
- package/templates/php/transport.hbs +285 -0
- package/templates/php/types.hbs +95 -0
- package/templates/python/client.hbs +102 -0
- package/templates/python/domain.hbs +139 -0
- package/templates/python/index.hbs +20 -0
- package/templates/python/pyproject.hbs +26 -0
- package/templates/python/readme.hbs +64 -0
- package/templates/python/resources-init.hbs +11 -0
- package/templates/python/transport.hbs +191 -0
- package/templates/python/types-errors.hbs +75 -0
- package/templates/python/types.hbs +79 -0
- package/templates/ruby/Gemfile.hbs +5 -0
- package/templates/ruby/client.hbs +86 -0
- package/templates/ruby/domain.hbs +110 -0
- package/templates/ruby/errors.hbs +150 -0
- package/templates/ruby/gemspec.hbs +30 -0
- package/templates/ruby/index.hbs +54 -0
- package/templates/ruby/readme.hbs +113 -0
- package/templates/ruby/transport.hbs +177 -0
- package/templates/ruby/types.hbs +126 -0
- package/templates/ruby/version.hbs +5 -0
- package/templates/rust/api-mod.hbs +3 -0
- package/templates/rust/cargo.hbs +22 -0
- package/templates/rust/client.hbs +114 -0
- package/templates/rust/domain.hbs +94 -0
- package/templates/rust/error.hbs +74 -0
- package/templates/rust/lib.hbs +85 -0
- package/templates/rust/readme.hbs +77 -0
- package/templates/rust/transport.hbs +287 -0
- package/templates/rust/types.hbs +142 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
use thiserror::Error;
|
|
2
|
+
|
|
3
|
+
/// Primary error type for the {{crateName}} SDK.
|
|
4
|
+
///
|
|
5
|
+
/// All API methods return `Result<T, SdkError>`.
|
|
6
|
+
#[derive(Error, Debug)]
|
|
7
|
+
pub enum SdkError {
|
|
8
|
+
/// The API returned an error response (4xx/5xx).
|
|
9
|
+
#[error("API error (status {status}): {message}")]
|
|
10
|
+
ApiError {
|
|
11
|
+
/// HTTP status code.
|
|
12
|
+
status: u16,
|
|
13
|
+
/// Machine-readable error code (if available).
|
|
14
|
+
code: Option<String>,
|
|
15
|
+
/// Human-readable error message.
|
|
16
|
+
message: String,
|
|
17
|
+
/// Raw response body for debugging.
|
|
18
|
+
body: String,
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
/// A network-level error occurred (DNS, TLS, connection refused, timeout).
|
|
22
|
+
#[error("network error: {0}")]
|
|
23
|
+
NetworkError(String),
|
|
24
|
+
|
|
25
|
+
/// Failed to deserialize the response body.
|
|
26
|
+
#[error("deserialization error: {0}")]
|
|
27
|
+
DeserializationError(String),
|
|
28
|
+
|
|
29
|
+
/// The request parameters failed client-side validation.
|
|
30
|
+
#[error("validation error: {0}")]
|
|
31
|
+
ValidationError(String),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
impl SdkError {
|
|
35
|
+
/// Returns the HTTP status code, if this is an `ApiError`.
|
|
36
|
+
pub fn status(&self) -> Option<u16> {
|
|
37
|
+
match self {
|
|
38
|
+
Self::ApiError { status, .. } => Some(*status),
|
|
39
|
+
_ => None,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Returns `true` if this error was caused by the server (5xx).
|
|
44
|
+
pub fn is_server_error(&self) -> bool {
|
|
45
|
+
matches!(self, Self::ApiError { status, .. } if *status >= 500)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Returns `true` if this error was caused by the client (4xx).
|
|
49
|
+
pub fn is_client_error(&self) -> bool {
|
|
50
|
+
matches!(self, Self::ApiError { status, .. } if *status >= 400 && *status < 500)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Returns `true` if this is a network error (should be retried safely).
|
|
54
|
+
pub fn is_retryable(&self) -> bool {
|
|
55
|
+
matches!(
|
|
56
|
+
self,
|
|
57
|
+
Self::NetworkError(_)
|
|
58
|
+
| Self::ApiError {
|
|
59
|
+
status: 429 | 502 | 503 | 504,
|
|
60
|
+
..
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Returns the error message as a string.
|
|
66
|
+
pub fn message(&self) -> &str {
|
|
67
|
+
match self {
|
|
68
|
+
Self::ApiError { message, .. } => message.as_str(),
|
|
69
|
+
Self::NetworkError(msg) => msg.as_str(),
|
|
70
|
+
Self::DeserializationError(msg) => msg.as_str(),
|
|
71
|
+
Self::ValidationError(msg) => msg.as_str(),
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
//! {{title}}
|
|
2
|
+
//!
|
|
3
|
+
//! {{#if description}}{{description}}{{else}}Auto-generated Rust SDK for the {{title}} API.{{/if}}
|
|
4
|
+
//!
|
|
5
|
+
//! # Quick Start
|
|
6
|
+
//!
|
|
7
|
+
//! Add to your `Cargo.toml`:
|
|
8
|
+
//!
|
|
9
|
+
//! ```toml
|
|
10
|
+
//! [dependencies]
|
|
11
|
+
//! {{crateName}} = "{{#if packageVersion}}{{packageVersion}}{{else}}0.1.0{{/if}}"
|
|
12
|
+
//! ```
|
|
13
|
+
//!
|
|
14
|
+
//! Then use it:
|
|
15
|
+
//!
|
|
16
|
+
//! ```rust,no_run
|
|
17
|
+
//! use {{crateName}}::Client;
|
|
18
|
+
//! use {{crateName}}::ClientConfig;
|
|
19
|
+
//!
|
|
20
|
+
//! #[tokio::main]
|
|
21
|
+
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
22
|
+
//! let mut config = ClientConfig::default();
|
|
23
|
+
{{#each securitySchemes}}
|
|
24
|
+
{{#if (eq type "apiKey")}}
|
|
25
|
+
//! config.{{snakeCase configKey}} = Some("your-api-key".to_string());
|
|
26
|
+
{{/if}}
|
|
27
|
+
{{#if (and (eq type "http") (eq scheme "bearer"))}}
|
|
28
|
+
//! config.{{snakeCase configKey}} = Some("your-bearer-token".to_string());
|
|
29
|
+
{{/if}}
|
|
30
|
+
{{/each}}
|
|
31
|
+
//!
|
|
32
|
+
//! let client = Client::new(config);
|
|
33
|
+
//!
|
|
34
|
+
//! // Use domain clients
|
|
35
|
+
//! // let result = client.{{#each groups}}{{#if @first}}{{snakeCase name}}{{/if}}{{/each}}.some_method().await?;
|
|
36
|
+
//!
|
|
37
|
+
//! Ok(())
|
|
38
|
+
//! }
|
|
39
|
+
//! ```
|
|
40
|
+
//!
|
|
41
|
+
//! ## API Reference
|
|
42
|
+
//!
|
|
43
|
+
{{#each groups}}
|
|
44
|
+
//! ### `{{interfaceName}}`
|
|
45
|
+
//! {{#if description}}{{description}}{{/if}}
|
|
46
|
+
//!
|
|
47
|
+
{{#each operations}}
|
|
48
|
+
//! - `{{snakeCase name}}()` — {{#if summary}}{{summary}}{{else}}No summary{{/if}}
|
|
49
|
+
{{/each}}
|
|
50
|
+
//!
|
|
51
|
+
{{/each}}
|
|
52
|
+
//!
|
|
53
|
+
//! ## Error Handling
|
|
54
|
+
//!
|
|
55
|
+
//! All API methods return `Result<T, SdkError>`. The `SdkError` enum covers:
|
|
56
|
+
//!
|
|
57
|
+
//! - **ApiError** — server returned an error response (4xx/5xx)
|
|
58
|
+
//! - **NetworkError** — request failed due to network issues
|
|
59
|
+
//! - **DeserializationError** — response body could not be parsed
|
|
60
|
+
//! - **ValidationError** — invalid request parameters
|
|
61
|
+
//!
|
|
62
|
+
//! ```rust
|
|
63
|
+
//! match client.some_domain.some_method().await {
|
|
64
|
+
//! Ok(response) => println!("Success: {:?}", response),
|
|
65
|
+
//! Err(SdkError::ApiError { status, .. }) => eprintln!("API error ({}): {}", status, error),
|
|
66
|
+
//! Err(SdkError::NetworkError(e)) => eprintln!("Network error: {}", e),
|
|
67
|
+
//! Err(e) => eprintln!("Other error: {}", e),
|
|
68
|
+
//! }
|
|
69
|
+
//! ```
|
|
70
|
+
|
|
71
|
+
pub mod client;
|
|
72
|
+
pub mod transport;
|
|
73
|
+
pub mod error;
|
|
74
|
+
pub mod types;
|
|
75
|
+
pub mod api;
|
|
76
|
+
|
|
77
|
+
pub use client::*;
|
|
78
|
+
pub use error::SdkError;
|
|
79
|
+
|
|
80
|
+
/// Re-export API resource modules
|
|
81
|
+
pub mod resources {
|
|
82
|
+
{{#each groups}}
|
|
83
|
+
pub use super::api::{{snakeCase fileName}}::*;
|
|
84
|
+
{{/each}}
|
|
85
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# {{title}}
|
|
2
|
+
|
|
3
|
+
{{#if description}}{{description}}{{/if}}
|
|
4
|
+
|
|
5
|
+
{{#unless description}}An auto-generated Rust SDK for the {{title}} API.{{/unless}}
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this to your `Cargo.toml`:
|
|
10
|
+
|
|
11
|
+
```toml
|
|
12
|
+
[dependencies]
|
|
13
|
+
{{crateName}} = "{{#if packageVersion}}{{packageVersion}}{{else}}0.1.0{{/if}}"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```rust
|
|
19
|
+
use {{crateName}}::Client;
|
|
20
|
+
use {{crateName}}::ClientConfig;
|
|
21
|
+
|
|
22
|
+
#[tokio::main]
|
|
23
|
+
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());
|
|
31
|
+
{{/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
|
+
Ok(())
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## API Reference
|
|
48
|
+
|
|
49
|
+
{{#each groups}}
|
|
50
|
+
### {{interfaceName}}
|
|
51
|
+
|
|
52
|
+
{{#if description}}
|
|
53
|
+
{{description}}
|
|
54
|
+
{{/if}}
|
|
55
|
+
|
|
56
|
+
| Method | HTTP | Description |
|
|
57
|
+
|--------|------|-------------|
|
|
58
|
+
{{#each operations}}
|
|
59
|
+
| `{{snakeCase name}}()` | `{{httpMethod}}` | {{#if summary}}{{summary}}{{else}}—{{/if}} |
|
|
60
|
+
{{/each}}
|
|
61
|
+
|
|
62
|
+
{{/each}}
|
|
63
|
+
|
|
64
|
+
## Error Handling
|
|
65
|
+
|
|
66
|
+
All API methods return `Result<T, SdkError>`. The `SdkError` enum has these variants:
|
|
67
|
+
|
|
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 |
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
use crate::error::SdkError;
|
|
2
|
+
use reqwest::{Client as ReqwestClient, Method, RequestBuilder, Response, StatusCode};
|
|
3
|
+
use serde::de::DeserializeOwned;
|
|
4
|
+
use serde::Serialize;
|
|
5
|
+
use std::sync::Arc;
|
|
6
|
+
use std::time::Duration;
|
|
7
|
+
use url::Url;
|
|
8
|
+
|
|
9
|
+
/// Internal transport configuration.
|
|
10
|
+
#[derive(Debug, Clone)]
|
|
11
|
+
pub struct TransportConfig {
|
|
12
|
+
pub base_url: String,
|
|
13
|
+
{{#each securitySchemes}}
|
|
14
|
+
pub {{snakeCase configKey}}: Option<String>,
|
|
15
|
+
{{/each}}
|
|
16
|
+
pub timeout: Duration,
|
|
17
|
+
pub max_retries: u32,
|
|
18
|
+
}
|
|
19
|
+
|
|
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
|
+
#[derive(Clone)]
|
|
26
|
+
pub struct Transport {
|
|
27
|
+
client: ReqwestClient,
|
|
28
|
+
config: Arc<TransportConfig>,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
impl Transport {
|
|
32
|
+
/// Create a new transport with the given configuration.
|
|
33
|
+
pub fn new(config: TransportConfig) -> Self {
|
|
34
|
+
let client = ReqwestClient::builder()
|
|
35
|
+
.timeout(config.timeout)
|
|
36
|
+
.build()
|
|
37
|
+
.expect("failed to build reqwest client");
|
|
38
|
+
|
|
39
|
+
Self {
|
|
40
|
+
client,
|
|
41
|
+
config: Arc::new(config),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Execute a request, deserializing the response body as JSON.
|
|
46
|
+
pub async fn execute<T: DeserializeOwned>(
|
|
47
|
+
&self,
|
|
48
|
+
method: Method,
|
|
49
|
+
path: &str,
|
|
50
|
+
query: Option<&[(String, String)]>,
|
|
51
|
+
body: Option<&(impl Serialize + ?Sized)>,
|
|
52
|
+
headers: Option<&[(String, String)]>,
|
|
53
|
+
) -> 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
|
+
|
|
67
|
+
self.handle_response(response).await
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// Execute a request that returns the raw response text.
|
|
71
|
+
pub async fn execute_raw(
|
|
72
|
+
&self,
|
|
73
|
+
method: Method,
|
|
74
|
+
path: &str,
|
|
75
|
+
query: Option<&[(String, String)]>,
|
|
76
|
+
body: Option<&(impl Serialize + ?Sized)>,
|
|
77
|
+
headers: Option<&[(String, String)]>,
|
|
78
|
+
) -> 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
|
+
|
|
92
|
+
let status = response.status();
|
|
93
|
+
let text = response.text().await.map_err(|e| {
|
|
94
|
+
SdkError::DeserializationError(format!("failed to read response body: {}", e))
|
|
95
|
+
})?;
|
|
96
|
+
|
|
97
|
+
if status.is_success() {
|
|
98
|
+
Ok(text)
|
|
99
|
+
} else {
|
|
100
|
+
Err(self.build_api_error(status, &text))
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── private helpers ─────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
fn build_url(
|
|
107
|
+
&self,
|
|
108
|
+
path: &str,
|
|
109
|
+
query: Option<&[(String, String)]>,
|
|
110
|
+
) -> 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
|
+
|
|
119
|
+
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
|
+
}
|
|
126
|
+
}
|
|
127
|
+
Ok(url)
|
|
128
|
+
} else {
|
|
129
|
+
Ok(joined)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
fn apply_auth(&self, mut req: RequestBuilder) -> RequestBuilder {
|
|
134
|
+
{{#each securitySchemes}}
|
|
135
|
+
{{#if (eq type "apiKey")}}
|
|
136
|
+
{{#if (eq in "header")}}
|
|
137
|
+
if let Some(ref key) = self.config.{{snakeCase configKey}} {
|
|
138
|
+
req = req.header("{{paramName}}", key);
|
|
139
|
+
}
|
|
140
|
+
{{/if}}
|
|
141
|
+
{{#if (eq in "query")}}
|
|
142
|
+
// API key via query parameter handled during URL construction
|
|
143
|
+
{{/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")}}
|
|
151
|
+
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));
|
|
158
|
+
}
|
|
159
|
+
{{/if}}
|
|
160
|
+
{{/each}}
|
|
161
|
+
req
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
fn apply_headers(
|
|
165
|
+
&self,
|
|
166
|
+
mut req: RequestBuilder,
|
|
167
|
+
headers: Option<&[(String, String)]>,
|
|
168
|
+
) -> RequestBuilder {
|
|
169
|
+
if let Some(hdrs) = headers {
|
|
170
|
+
for (key, value) in hdrs {
|
|
171
|
+
req = req.header(key.as_str(), value.as_str());
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
req
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async fn execute_with_retry(
|
|
178
|
+
&self,
|
|
179
|
+
req: RequestBuilder,
|
|
180
|
+
) -> Result<Response, SdkError> {
|
|
181
|
+
let max_retries = self.config.max_retries;
|
|
182
|
+
let mut attempt = 0u32;
|
|
183
|
+
|
|
184
|
+
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 {
|
|
190
|
+
Ok(response) => {
|
|
191
|
+
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
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return Ok(response);
|
|
205
|
+
}
|
|
206
|
+
Err(e) => {
|
|
207
|
+
if attempt < max_retries && e.is_connect() || e.is_timeout() {
|
|
208
|
+
attempt += 1;
|
|
209
|
+
let backoff = Duration::from_millis(
|
|
210
|
+
200u64 * 2u64.pow(attempt),
|
|
211
|
+
);
|
|
212
|
+
tokio::time::sleep(backoff).await;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
return Err(SdkError::NetworkError(e.to_string()));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async fn handle_response<T: DeserializeOwned>(
|
|
222
|
+
&self,
|
|
223
|
+
response: Response,
|
|
224
|
+
) -> Result<T, SdkError> {
|
|
225
|
+
let status = response.status();
|
|
226
|
+
let text = response.text().await.map_err(|e| {
|
|
227
|
+
SdkError::DeserializationError(format!("failed to read response body: {}", e))
|
|
228
|
+
})?;
|
|
229
|
+
|
|
230
|
+
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
|
+
})
|
|
237
|
+
} else {
|
|
238
|
+
Err(self.build_api_error(status, &text))
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
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
|
+
|
|
272
|
+
SdkError::ApiError {
|
|
273
|
+
status: status.as_u16(),
|
|
274
|
+
code,
|
|
275
|
+
message,
|
|
276
|
+
body: body.to_string(),
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
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()
|
|
286
|
+
}
|
|
287
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
use serde::{Deserialize, Serialize};
|
|
2
|
+
|
|
3
|
+
{{#each schemas}}
|
|
4
|
+
{{#if (and (not ownership) (not isEnum) (not isTypeAlias) (not isUnionType) (not isIntersectionType))}}
|
|
5
|
+
{{#if (or externalImport (eq ownership.kind "external"))}}
|
|
6
|
+
// {{name}} is imported from an external schema package.
|
|
7
|
+
{{else}}
|
|
8
|
+
{{#if description}}
|
|
9
|
+
/// {{description}}
|
|
10
|
+
{{/if}}
|
|
11
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
12
|
+
{{#if additionalPropertiesType}}
|
|
13
|
+
{{! Additional properties means we need to use a flattened map }}
|
|
14
|
+
#[serde(rename_all = "camelCase")]
|
|
15
|
+
pub struct {{name}} {
|
|
16
|
+
{{#each properties}}
|
|
17
|
+
{{#if description}}
|
|
18
|
+
/// {{description}}
|
|
19
|
+
{{/if}}
|
|
20
|
+
{{#if deprecated}}
|
|
21
|
+
#[deprecated]
|
|
22
|
+
{{/if}}
|
|
23
|
+
{{#if (and nullable (not required))}}
|
|
24
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
25
|
+
pub {{snakeCase name}}: Option<{{{rustType type}}}>,
|
|
26
|
+
{{else if nullable}}
|
|
27
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
28
|
+
pub {{snakeCase name}}: Option<{{{rustType type}}}>,
|
|
29
|
+
{{else if required}}
|
|
30
|
+
pub {{snakeCase name}}: {{{rustType type}}},
|
|
31
|
+
{{else}}
|
|
32
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
33
|
+
pub {{snakeCase name}}: Option<{{{rustType type}}}>,
|
|
34
|
+
{{/if}}
|
|
35
|
+
{{/each}}
|
|
36
|
+
}
|
|
37
|
+
{{else}}
|
|
38
|
+
#[serde(rename_all = "camelCase")]
|
|
39
|
+
pub struct {{name}} {
|
|
40
|
+
{{#each properties}}
|
|
41
|
+
{{#if description}}
|
|
42
|
+
/// {{description}}
|
|
43
|
+
{{/if}}
|
|
44
|
+
{{#if deprecated}}
|
|
45
|
+
#[deprecated]
|
|
46
|
+
{{/if}}
|
|
47
|
+
{{#if (and nullable (not required))}}
|
|
48
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
49
|
+
pub {{snakeCase name}}: Option<{{{rustType type}}}>,
|
|
50
|
+
{{else if nullable}}
|
|
51
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
52
|
+
pub {{snakeCase name}}: Option<{{{rustType type}}}>,
|
|
53
|
+
{{else if required}}
|
|
54
|
+
pub {{snakeCase name}}: {{{rustType type}}},
|
|
55
|
+
{{else}}
|
|
56
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
57
|
+
pub {{snakeCase name}}: Option<{{{rustType type}}}>,
|
|
58
|
+
{{/if}}
|
|
59
|
+
{{/each}}
|
|
60
|
+
}
|
|
61
|
+
{{/if}}
|
|
62
|
+
{{/if}}
|
|
63
|
+
{{/if}}
|
|
64
|
+
|
|
65
|
+
{{#if isEnum}}
|
|
66
|
+
{{#if description}}
|
|
67
|
+
/// {{description}}
|
|
68
|
+
{{/if}}
|
|
69
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
70
|
+
{{#if enumValues}}
|
|
71
|
+
#[serde(rename_all = "camelCase")]
|
|
72
|
+
{{/if}}
|
|
73
|
+
pub enum {{name}} {
|
|
74
|
+
{{#each enumValues}}
|
|
75
|
+
#[serde(rename = "{{this}}")]
|
|
76
|
+
{{pascalCase this}},
|
|
77
|
+
{{/each}}
|
|
78
|
+
}
|
|
79
|
+
{{/if}}
|
|
80
|
+
|
|
81
|
+
{{#if isTypeAlias}}
|
|
82
|
+
{{#if description}}
|
|
83
|
+
/// {{description}}
|
|
84
|
+
{{/if}}
|
|
85
|
+
{{#if properties.length}}
|
|
86
|
+
{{! Type alias with shape — emit as struct }}
|
|
87
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
88
|
+
#[serde(rename_all = "camelCase")]
|
|
89
|
+
pub struct {{name}} {
|
|
90
|
+
{{#each properties}}
|
|
91
|
+
{{#if description}}
|
|
92
|
+
/// {{description}}
|
|
93
|
+
{{/if}}
|
|
94
|
+
{{#if required}}
|
|
95
|
+
pub {{snakeCase name}}: {{{rustType type}}},
|
|
96
|
+
{{else}}
|
|
97
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
98
|
+
pub {{snakeCase name}}: Option<{{{rustType type}}}>,
|
|
99
|
+
{{/if}}
|
|
100
|
+
{{/each}}
|
|
101
|
+
}
|
|
102
|
+
{{else}}
|
|
103
|
+
{{! Simple type alias — skip, referenced by name }}
|
|
104
|
+
// {{name}} is a type alias — referenced inline.
|
|
105
|
+
{{/if}}
|
|
106
|
+
{{/if}}
|
|
107
|
+
|
|
108
|
+
{{#if isUnionType}}
|
|
109
|
+
{{#if description}}
|
|
110
|
+
/// {{description}}
|
|
111
|
+
{{/if}}
|
|
112
|
+
{{#if unionMembers.length}}
|
|
113
|
+
{{! Union type with named members — emit as enum }}
|
|
114
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
115
|
+
#[serde(untagged)]
|
|
116
|
+
pub enum {{name}} {
|
|
117
|
+
{{#each unionMembers}}
|
|
118
|
+
{{pascalCase this}}({{{rustType this}}}),
|
|
119
|
+
{{/each}}
|
|
120
|
+
}
|
|
121
|
+
{{/if}}
|
|
122
|
+
{{/if}}
|
|
123
|
+
|
|
124
|
+
{{#if isIntersectionType}}
|
|
125
|
+
{{#if description}}
|
|
126
|
+
/// {{description}}
|
|
127
|
+
{{/if}}
|
|
128
|
+
{{#if intersectionMembers.length}}
|
|
129
|
+
{{! Intersection type — flatten into a struct combining all members }}
|
|
130
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
131
|
+
#[serde(rename_all = "camelCase")]
|
|
132
|
+
pub struct {{name}} {
|
|
133
|
+
{{#each intersectionMembers}}
|
|
134
|
+
// Fields from {{this}} are expected to be inlined by the code generator.
|
|
135
|
+
{{/each}}
|
|
136
|
+
// Intersection types require manual field specification.
|
|
137
|
+
// TODO: inline fields from {{join intersectionMembers ", "}}
|
|
138
|
+
}
|
|
139
|
+
{{/if}}
|
|
140
|
+
{{/if}}
|
|
141
|
+
|
|
142
|
+
{{/each}}
|