uapi_sdk_rust/
client.rs

1use crate::errors::{ApiErrorBody, Error};
2use crate::services::{ClipzyZaiXianJianTieBanService,ConvertService,DailyService,GameService,ImageService,MiscService,NetworkService,PoemService,RandomService,SocialService,StatusService,TextService,TranslateService,WebparseService,MinGanCiShiBieService,ZhiNengSouSuoService
3};
4use crate::Result;
5use once_cell::sync::Lazy;
6use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, RETRY_AFTER, USER_AGENT};
7use reqwest::StatusCode;
8use std::time::Duration;
9use tracing::{debug, instrument};
10use url::Url;
11
12static DEFAULT_BASE: &str = "https://uapis.cn/api/v1/";
13static DEFAULT_UA: &str = "uapi-sdk-rust/0.1.0";
14static DEFAULT_BASE_URL: Lazy<Url> = Lazy::new(|| Url::parse(DEFAULT_BASE).expect("valid default base"));
15
16#[derive(Clone, Debug)]
17pub 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
24impl 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    pub fn clipzy_zai_xian_jian_tie_ban(&self) -> ClipzyZaiXianJianTieBanService<'_> {
53        ClipzyZaiXianJianTieBanService { client: self }
54    }
55    pub fn convert(&self) -> ConvertService<'_> {
56        ConvertService { client: self }
57    }
58    pub fn daily(&self) -> DailyService<'_> {
59        DailyService { client: self }
60    }
61    pub fn game(&self) -> GameService<'_> {
62        GameService { client: self }
63    }
64    pub fn image(&self) -> ImageService<'_> {
65        ImageService { client: self }
66    }
67    pub fn misc(&self) -> MiscService<'_> {
68        MiscService { client: self }
69    }
70    pub fn network(&self) -> NetworkService<'_> {
71        NetworkService { client: self }
72    }
73    pub fn poem(&self) -> PoemService<'_> {
74        PoemService { client: self }
75    }
76    pub fn random(&self) -> RandomService<'_> {
77        RandomService { client: self }
78    }
79    pub fn social(&self) -> SocialService<'_> {
80        SocialService { client: self }
81    }
82    pub fn status(&self) -> StatusService<'_> {
83        StatusService { client: self }
84    }
85    pub fn text(&self) -> TextService<'_> {
86        TextService { client: self }
87    }
88    pub fn translate(&self) -> TranslateService<'_> {
89        TranslateService { client: self }
90    }
91    pub fn webparse(&self) -> WebparseService<'_> {
92        WebparseService { client: self }
93    }
94    pub fn min_gan_ci_shi_bie(&self) -> MinGanCiShiBieService<'_> {
95        MinGanCiShiBieService { client: self }
96    }
97    pub fn zhi_neng_sou_suo(&self) -> ZhiNengSouSuoService<'_> {
98        ZhiNengSouSuoService { client: self }
99    }
100
101    #[instrument(skip(self, headers, query), fields(method=%method, path=%path))]
102    pub(crate) async fn request_json<T: serde::de::DeserializeOwned>(
103        &self,
104        method: reqwest::Method,
105        path: &str,
106        headers: Option<HeaderMap>,
107        query: Option<Vec<(String, String)>>,
108        json_body: Option<serde_json::Value>,
109    ) -> Result<T> {
110        let clean_path = path.trim_start_matches('/');
111        let url = self.base_url.join(clean_path)?;
112        let mut req = self.http.request(method.clone(), url.clone());
113
114        let mut merged = HeaderMap::new();
115        merged.insert(USER_AGENT, HeaderValue::from_static(DEFAULT_UA));
116        if let Some(t) = &self.api_key {
117            let value = format!("Bearer {}", t);
118            if let Ok(h) = HeaderValue::from_str(&value) {
119                merged.insert(AUTHORIZATION, h);
120            }
121        }
122        if let Some(h) = headers {
123            merged.extend(h);
124        }
125        req = req.headers(merged);
126
127        if let Some(q) = query {
128            req = req.query(&q);
129        }
130        if let Some(body) = json_body {
131            req = req.json(&body);
132        }
133
134        debug!("request {}", url);
135        let resp = req.send().await?;
136        self.handle_json_response(resp).await
137    }
138
139    async fn handle_json_response<T: serde::de::DeserializeOwned>(&self, resp: reqwest::Response) -> Result<T> {
140        let status = resp.status();
141        let req_id = find_request_id(resp.headers());
142        let retry_after = parse_retry_after(resp.headers());
143        if status.is_success() {
144            return Ok(resp.json::<T>().await?);
145        }
146        let text = resp.text().await.unwrap_or_default();
147        let parsed = serde_json::from_str::<ApiErrorBody>(&text).ok();
148        let msg = parsed.as_ref().and_then(|b| b.message.clone()).or_else(|| non_empty(text.clone()));
149        let code = parsed.as_ref().and_then(|b| b.code.clone());
150        let details = parsed.as_ref().and_then(|b| b.details.clone());
151        Err(map_status_to_error(status, code, msg, details, req_id, retry_after))
152    }
153}
154
155#[derive(Default)]
156pub struct ClientBuilder {
157    api_key: Option<String>,
158    base_url: Option<Url>,
159    timeout: Option<Duration>,
160    client: Option<reqwest::Client>,
161    user_agent: Option<String>,
162}
163
164impl ClientBuilder {
165    pub fn api_key<T: Into<String>>(mut self, api_key: T) -> Self { self.api_key = Some(api_key.into()); self }
166    pub fn base_url(mut self, base: Url) -> Self { self.base_url = Some(base); self }
167    pub fn timeout(mut self, secs: u64) -> Self { self.timeout = Some(Duration::from_secs(secs)); self }
168    pub fn user_agent<T: Into<String>>(mut self, ua: T) -> Self { self.user_agent = Some(ua.into()); self }
169    pub fn http_client(mut self, cli: reqwest::Client) -> Self { self.client = Some(cli); self }
170
171    pub fn build(self) -> Result<Client> {
172        let http = if let Some(cli) = self.client {
173            cli
174        } else {
175            reqwest::Client::builder()
176                .timeout(self.timeout.unwrap_or(Duration::from_secs(20)))
177                .build()?
178        };
179        Ok(Client {
180            http,
181            base_url: self.base_url.unwrap_or_else(|| DEFAULT_BASE_URL.clone()),
182            api_key: self.api_key,
183            user_agent: self.user_agent.unwrap_or_else(|| DEFAULT_UA.to_string()),
184        })
185    }
186}
187
188fn find_request_id(headers: &HeaderMap) -> Option<String> {
189    const CANDIDATES: &[&str] = &["x-request-id", "x-amzn-requestid", "traceparent"];
190    for key in CANDIDATES {
191        if let Some(v) = headers.get(*key) {
192            if let Ok(text) = v.to_str() {
193                return Some(text.to_string());
194            }
195        }
196    }
197    None
198}
199
200fn parse_retry_after(headers: &HeaderMap) -> Option<u64> {
201    headers
202        .get(RETRY_AFTER)
203        .and_then(|v| v.to_str().ok())
204        .and_then(|s| s.trim().parse::<u64>().ok())
205}
206
207fn non_empty(s: String) -> Option<String> {
208    let trimmed = s.trim();
209    if trimmed.is_empty() { None } else { Some(trimmed.to_owned()) }
210}
211
212fn map_status_to_error(
213    status: StatusCode,
214    code: Option<String>,
215    message: Option<String>,
216    details: Option<serde_json::Value>,
217    request_id: Option<String>,
218    retry_after: Option<u64>,
219) -> Error {
220    let s = status.as_u16();
221    match status {
222        StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => Error::AuthenticationError { status: s, message, request_id },
223        StatusCode::TOO_MANY_REQUESTS => Error::RateLimitError { status: s, message, retry_after_seconds: retry_after, request_id },
224        StatusCode::NOT_FOUND => Error::NotFound { status: s, message, request_id },
225        StatusCode::BAD_REQUEST => Error::ValidationError { status: s, message, details, request_id },
226        _ if status.is_server_error() => Error::ServerError { status: s, message, request_id },
227        _ if status.is_client_error() => Error::ApiError { status: s, code, message, details, request_id },
228        _ => Error::ApiError { status: s, code, message, details, request_id },
229    }
230}