Skip to content

Commit be57e6f

Browse files
author
AxT-Team Bot
committed
chore: regenerate sanitized Facades
0 parents  commit be57e6f

File tree

14 files changed

+487
-0
lines changed

14 files changed

+487
-0
lines changed

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
target/
2+
Cargo.lock
3+
.idea/
4+
.vscode/
5+
.DS_Store
6+
**/*.rs.bk
7+
.*.swp

Cargo.toml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[package]
2+
name = "uapi"
3+
version = "0.1.0"
4+
edition = "2021"
5+
license = "MIT OR Apache-2.0"
6+
description = "UAPI Rust SDK - idiomatic, typed, domain-driven API client."
7+
repository = "https://github.com/uapis/uapi-sdk-rust"
8+
readme = "README.md"
9+
keywords = ["uapi", "sdk", "api", "client"]
10+
categories = ["api-bindings", "web-programming::http-client"]
11+
12+
[features]
13+
default = ["rustls-tls"]
14+
rustls-tls = ["reqwest/rustls-tls"]
15+
native-tls = ["reqwest/native-tls"]
16+
17+
[dependencies]
18+
once_cell = "1.19"
19+
reqwest = { version = "0.12", default-features = false, features = ["json", "gzip", "brotli", "stream"] }
20+
serde = { version = "1.0", features = ["derive"] }
21+
serde_json = "1.0"
22+
thiserror = "1.0"
23+
tracing = "0.1"
24+
url = "2.5"
25+
time = { version = "0.3", features = ["parsing", "formatting"] }
26+
27+
[dev-dependencies]
28+
tokio = { version = "1.39", features = ["macros", "rt-multi-thread"] }
29+
30+
[package.metadata.docs.rs]
31+
all-features = true
32+
rustdoc-args = ["--cfg", "docsrs"]

README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# uapi-sdk-rust
2+
3+
![Banner](./banner.png)
4+
5+
[![Rust](https://img.shields.io/badge/Rust-1.75+-DEA584?style=flat-square&logo=rust&logoColor=white)](https://www.rust-lang.org/)
6+
[![Docs](https://img.shields.io/badge/Docs-uapis.cn-2EAE5D?style=flat-square)](https://uapis.cn/)
7+
8+
> [!NOTE]
9+
> 所有接口的 Rust 示例都可以在 [UApi](https://uapis.cn/docs/introduction) 的接口文档页面,向下滚动至 **快速启动** 区块后直接复制。
10+
11+
## 快速开始
12+
13+
```bash
14+
cargo add uapi-sdk-rust
15+
```
16+
17+
```rust
18+
use uapi::Client;
19+
20+
#[tokio::main]
21+
async fn main() -> Result<(), uapi::Error> {
22+
let client = Client::new("");
23+
let result = client.social().get_social_qq_userinfo("10001").await?;
24+
println!("{result:?}");
25+
Ok(())
26+
}
27+
```
28+
29+
> 未发布到 crates.io 时,可用 `uapi-sdk-rust = { path = "sdks/rust/uapi-sdk-rust" }` 拉取本地版本。
30+
31+
## 特性
32+
33+
现在你不再需要反反复复的查阅文档了。
34+
35+
只需在 IDE 中键入 `client.`,所有核心模块——如 `social()``game()``image()`——即刻同步展现。进一步输入即可直接定位到 `get_social_qq_userinfo` 这样的具体方法,其名称与文档的 `operationId` 严格保持一致,确保了开发过程的直观与高效。
36+
37+
所有方法签名只接受真实且必需的参数。当你在构建请求时,IDE 会即时提示 `qq``username` 等键名,这彻底杜绝了在 `&str` / `serde_json::Value` 等常规类型中因键名拼写错误而导致的运行时错误。
38+
39+
针对 401、404、429 等标准 HTTP 响应,SDK 已将其统一映射为具名的错误类型(`Error::AuthenticationError``Error::NotFound``Error::RateLimitError` 等)。这些错误均附带 `status()``request_id()``details()` 等关键上下文信息,确保你在日志中能第一时间准确、快速地诊断问题。
40+
41+
`Client::builder()` / `Client::new()` / `Client::from_env()` 允许你在保持默认 15 秒超时与 `Authorization` 头的同时,自由覆盖 Base URL、Token、代理乃至注入自定义的 `reqwest::Client`
42+
43+
如果你需要查看字段细节或内部逻辑,仓库中的 `./internal` 目录同步保留了由 `openapi-generator` 生成的完整结构体,随时可供参考。
44+
45+
## 错误模型概览
46+
47+
| HTTP 状态码 | SDK 错误类型 | 附加信息 |
48+
|-------------|-------------------------|---------------------------------------------------|
49+
| 401/403 | `Error::AuthenticationError` | `status``request_id` |
50+
| 404 | `Error::NotFound` | `status``request_id` |
51+
| 400 | `Error::ValidationError`| `status``details``request_id` |
52+
| 429 | `Error::RateLimitError` | `status``retry_after_seconds``request_id` |
53+
| 5xx | `Error::ServerError` | `status``request_id` |
54+
| 其他 4xx/5xx| `Error::ApiError` | `code``status``details``request_id` |
55+
56+
## 其他 SDK
57+
58+
| 语言 | 仓库地址 |
59+
|-------------|--------------------------------------------------------------|
60+
| Go | https://github.com/AxT-Team/uapi-go-sdk |
61+
| Python | https://github.com/AxT-Team/uapi-python-sdk |
62+
| TypeScript| https://github.com/AxT-Team/uapi-typescript-sdk |
63+
| Browser (TypeScript/JavaScript)| https://github.com/AxT-Team/uapi-browser-sdk |
64+
| Java | https://github.com/AxT-Team/uapi-java-sdk |
65+
| PHP | https://github.com/AxT-Team/uapi-php-sdk |
66+
| C# | https://github.com/AxT-Team/uapi-csharp-sdk |
67+
| C++ | https://github.com/AxT-Team/uapi-cpp-sdk |
68+
| Rust(当前) | https://github.com/AxT-Team/uapi-rust-sdk |
69+
70+
## 文档
71+
72+
访问 [UApi文档首页](https://uapis.cn/docs/introduction) 并选择任意接口,向下滚动到 **快速启动** 区块即可看到最新的 Rust 示例代码。

banner.png

2.31 MB
Loading

examples/quickstart.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
use uapi::{Client, Result};
2+
3+
#[tokio::main]
4+
async fn main() -> Result<()> {
5+
tracing_subscriber::fmt::init();
6+
7+
let api = Client::from_env().unwrap_or_else(|| Client::new("<TOKEN>"));
8+
let user = api.game().get_minecraft_user_info("Notch").await?;
9+
println!("{} -> {}", user.username, user.uuid);
10+
Ok(())
11+
}

src/client.rs

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
use crate::errors::{ApiErrorBody, Error};
2+
use crate::services::game::GameService;
3+
use crate::services::social::SocialService;
4+
use crate::Result;
5+
use once_cell::sync::Lazy;
6+
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, RETRY_AFTER, USER_AGENT};
7+
use reqwest::StatusCode;
8+
use std::time::Duration;
9+
use tracing::{debug, instrument};
10+
use url::Url;
11+
12+
static DEFAULT_BASE: &str = "https://uapis.cn/api/v1";
13+
static DEFAULT_UA: &str = "uapi-sdk-rust/0.1.0";
14+
static DEFAULT_BASE_URL: Lazy<Url> = Lazy::new(|| Url::parse(DEFAULT_BASE).expect("valid default base"));
15+
16+
#[derive(Clone)]
17+
pub struct Client {
18+
pub(crate) http: reqwest::Client,
19+
pub(crate) base_url: Url,
20+
pub(crate) api_key: Option<String>,
21+
pub(crate) user_agent: String,
22+
}
23+
24+
impl Client {
25+
pub fn new<T: Into<String>>(api_key: T) -> Self {
26+
let http = reqwest::Client::builder()
27+
.timeout(Duration::from_secs(20))
28+
.build()
29+
.expect("reqwest client");
30+
Self {
31+
http,
32+
base_url: DEFAULT_BASE_URL.clone(),
33+
api_key: Some(api_key.into()),
34+
user_agent: DEFAULT_UA.to_string(),
35+
}
36+
}
37+
38+
pub fn from_env() -> Option<Self> {
39+
let token = std::env::var("UAPI_TOKEN").ok()?;
40+
let mut cli = Self::new(token);
41+
if let Ok(base) = std::env::var("UAPI_BASE_URL") {
42+
if let Ok(url) = Url::parse(&base) {
43+
cli.base_url = url;
44+
}
45+
}
46+
Some(cli)
47+
}
48+
49+
pub fn builder() -> ClientBuilder {
50+
ClientBuilder::default()
51+
}
52+
53+
pub fn game(&self) -> GameService<'_> {
54+
GameService { client: self }
55+
}
56+
57+
pub fn social(&self) -> SocialService<'_> {
58+
SocialService { client: self }
59+
}
60+
61+
#[instrument(skip(self, headers, query), fields(method=%method, path=%path))]
62+
pub(crate) async fn request_json<T: serde::de::DeserializeOwned>(
63+
&self,
64+
method: reqwest::Method,
65+
path: &str,
66+
headers: Option<HeaderMap>,
67+
query: Option<&[(&str, &str)]>,
68+
json_body: Option<serde_json::Value>,
69+
) -> Result<T> {
70+
let url = self.base_url.join(path)?;
71+
let mut req = self.http.request(method.clone(), url.clone());
72+
73+
let mut merged = HeaderMap::new();
74+
merged.insert(USER_AGENT, HeaderValue::from_static(DEFAULT_UA));
75+
if let Some(t) = &self.api_key {
76+
let value = format!("Bearer {}", t);
77+
if let Ok(h) = HeaderValue::from_str(&value) {
78+
merged.insert(AUTHORIZATION, h);
79+
}
80+
}
81+
if let Some(h) = headers {
82+
merged.extend(h);
83+
}
84+
req = req.headers(merged);
85+
86+
if let Some(q) = query {
87+
req = req.query(q);
88+
}
89+
if let Some(body) = json_body {
90+
req = req.json(&body);
91+
}
92+
93+
debug!("request {}", url);
94+
let resp = req.send().await?;
95+
self.handle_json_response(resp).await
96+
}
97+
98+
async fn handle_json_response<T: serde::de::DeserializeOwned>(&self, resp: reqwest::Response) -> Result<T> {
99+
let status = resp.status();
100+
let req_id = find_request_id(resp.headers());
101+
let retry_after = parse_retry_after(resp.headers());
102+
if status.is_success() {
103+
return Ok(resp.json::<T>().await?);
104+
}
105+
let text = resp.text().await.unwrap_or_default();
106+
let parsed = serde_json::from_str::<ApiErrorBody>(&text).ok();
107+
let msg = parsed.as_ref().and_then(|b| b.message.clone()).or_else(|| non_empty(text.clone()));
108+
let code = parsed.as_ref().and_then(|b| b.code.clone());
109+
let details = parsed.as_ref().and_then(|b| b.details.clone());
110+
Err(map_status_to_error(status, code, msg, details, req_id, retry_after))
111+
}
112+
}
113+
114+
#[derive(Default)]
115+
pub struct ClientBuilder {
116+
api_key: Option<String>,
117+
base_url: Option<Url>,
118+
timeout: Option<Duration>,
119+
client: Option<reqwest::Client>,
120+
user_agent: Option<String>,
121+
}
122+
123+
impl ClientBuilder {
124+
pub fn api_key<T: Into<String>>(mut self, api_key: T) -> Self { self.api_key = Some(api_key.into()); self }
125+
pub fn base_url(mut self, base: Url) -> Self { self.base_url = Some(base); self }
126+
pub fn timeout(mut self, secs: u64) -> Self { self.timeout = Some(Duration::from_secs(secs)); self }
127+
pub fn user_agent<T: Into<String>>(mut self, ua: T) -> Self { self.user_agent = Some(ua.into()); self }
128+
pub fn http_client(mut self, cli: reqwest::Client) -> Self { self.client = Some(cli); self }
129+
130+
pub fn build(self) -> Result<Client> {
131+
let http = if let Some(cli) = self.client {
132+
cli
133+
} else {
134+
reqwest::Client::builder()
135+
.timeout(self.timeout.unwrap_or(Duration::from_secs(20)))
136+
.build()?
137+
};
138+
Ok(Client {
139+
http,
140+
base_url: self.base_url.unwrap_or_else(|| DEFAULT_BASE_URL.clone()),
141+
api_key: self.api_key,
142+
user_agent: self.user_agent.unwrap_or_else(|| DEFAULT_UA.to_string()),
143+
})
144+
}
145+
}
146+
147+
fn find_request_id(headers: &HeaderMap) -> Option<String> {
148+
const CANDIDATES: &[&str] = &["x-request-id", "x-amzn-requestid", "traceparent"];
149+
for key in CANDIDATES {
150+
if let Some(v) = headers.get(*key) {
151+
if let Ok(text) = v.to_str() {
152+
return Some(text.to_string());
153+
}
154+
}
155+
}
156+
None
157+
}
158+
159+
fn parse_retry_after(headers: &HeaderMap) -> Option<u64> {
160+
headers
161+
.get(RETRY_AFTER)
162+
.and_then(|v| v.to_str().ok())
163+
.and_then(|s| s.trim().parse::<u64>().ok())
164+
}
165+
166+
fn non_empty(s: String) -> Option<String> {
167+
let trimmed = s.trim();
168+
if trimmed.is_empty() { None } else { Some(trimmed.to_owned()) }
169+
}
170+
171+
fn map_status_to_error(
172+
status: StatusCode,
173+
code: Option<String>,
174+
message: Option<String>,
175+
details: Option<serde_json::Value>,
176+
request_id: Option<String>,
177+
retry_after: Option<u64>,
178+
) -> Error {
179+
use StatusCode::*;
180+
let s = status.as_u16();
181+
match status {
182+
UNAUTHORIZED | FORBIDDEN => Error::AuthenticationError { status: s, message, request_id },
183+
TOO_MANY_REQUESTS => Error::RateLimitError { status: s, message, retry_after_seconds: retry_after, request_id },
184+
NOT_FOUND => Error::NotFound { status: s, message, request_id },
185+
BAD_REQUEST => Error::ValidationError { status: s, message, details, request_id },
186+
_ if status.is_server_error() => Error::ServerError { status: s, message, request_id },
187+
_ if status.is_client_error() => Error::ApiError { status: s, code, message, details, request_id },
188+
_ => Error::ApiError { status: s, code, message, details, request_id },
189+
}
190+
}

src/errors.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use thiserror::Error;
2+
3+
#[derive(Debug, Error)]
4+
pub enum Error {
5+
#[error("authentication failed (status {status})")]
6+
AuthenticationError { status: u16, message: Option<String>, request_id: Option<String> },
7+
8+
#[error("rate limited (status {status})")]
9+
RateLimitError {
10+
status: u16,
11+
message: Option<String>,
12+
retry_after_seconds: Option<u64>,
13+
request_id: Option<String>,
14+
},
15+
16+
#[error("validation failed (status {status})")]
17+
ValidationError { status: u16, message: Option<String>, details: Option<serde_json::Value>, request_id: Option<String> },
18+
19+
#[error("resource not found (status {status})")]
20+
NotFound { status: u16, message: Option<String>, request_id: Option<String> },
21+
22+
#[error("server error (status {status})")]
23+
ServerError { status: u16, message: Option<String>, request_id: Option<String> },
24+
25+
#[error("api error (status {status})")]
26+
ApiError {
27+
status: u16,
28+
code: Option<String>,
29+
message: Option<String>,
30+
details: Option<serde_json::Value>,
31+
request_id: Option<String>,
32+
},
33+
34+
#[error(transparent)]
35+
Transport(#[from] reqwest::Error),
36+
37+
#[error(transparent)]
38+
Json(#[from] serde_json::Error),
39+
40+
#[error(transparent)]
41+
Url(#[from] url::ParseError),
42+
}
43+
44+
impl Error {
45+
pub fn status(&self) -> Option<u16> {
46+
use Error::*;
47+
match self {
48+
AuthenticationError { status, .. }
49+
| RateLimitError { status, .. }
50+
| ValidationError { status, .. }
51+
| NotFound { status, .. }
52+
| ServerError { status, .. }
53+
| ApiError { status, .. } => Some(*status),
54+
_ => None,
55+
}
56+
}
57+
58+
pub fn request_id(&self) -> Option<&str> {
59+
use Error::*;
60+
match self {
61+
AuthenticationError { request_id, .. }
62+
| RateLimitError { request_id, .. }
63+
| ValidationError { request_id, .. }
64+
| NotFound { request_id, .. }
65+
| ServerError { request_id, .. }
66+
| ApiError { request_id, .. } => request_id.as_deref(),
67+
_ => None,
68+
}
69+
}
70+
}
71+
72+
#[derive(Debug, serde::Deserialize)]
73+
pub struct ApiErrorBody {
74+
pub code: Option<String>,
75+
pub message: Option<String>,
76+
pub details: Option<serde_json::Value>,
77+
}

0 commit comments

Comments
 (0)