uapi_sdk_rust/
client.rs

1use crate::errors::{ApiErrorBody, Error};
2use crate::services::{
3    ClipzyZaiXianJianTieBanService, ConvertService, DailyService, GameService, ImageService,
4    MinGanCiShiBieService, MiscService, NetworkService, PoemService, RandomService, SocialService,
5    StatusService, TextService, TranslateService, WebparseService, ZhiNengSouSuoService,
6};
7use crate::Result;
8use once_cell::sync::Lazy;
9use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, RETRY_AFTER, USER_AGENT};
10use reqwest::StatusCode;
11use std::time::Duration;
12use tracing::{debug, instrument};
13use url::Url;
14
15static DEFAULT_BASE: &str = "https://uapis.cn/api/v1";
16static DEFAULT_UA: &str = "uapi-sdk-rust/0.1.2";
17static DEFAULT_BASE_URL: Lazy<Url> =
18    Lazy::new(|| Url::parse(DEFAULT_BASE).expect("valid default base"));
19
20#[derive(Clone, Debug)]
21pub struct Client {
22    pub(crate) http: reqwest::Client,
23    pub(crate) base_url: Url,
24    pub(crate) api_key: Option<String>,
25    pub(crate) user_agent: String,
26}
27
28impl Client {
29    pub fn new<T: Into<String>>(api_key: T) -> Self {
30        let http = reqwest::Client::builder()
31            .timeout(Duration::from_secs(20))
32            .build()
33            .expect("reqwest client");
34        Self {
35            http,
36            base_url: DEFAULT_BASE_URL.clone(),
37            api_key: Some(api_key.into()),
38            user_agent: DEFAULT_UA.to_string(),
39        }
40    }
41
42    pub fn from_env() -> Option<Self> {
43        let token = std::env::var("UAPI_TOKEN").ok()?;
44        let mut cli = Self::new(token);
45        if let Ok(base) = std::env::var("UAPI_BASE_URL") {
46            if let Ok(url) = Url::parse(&base) {
47                cli.base_url = url;
48            }
49        }
50        Some(cli)
51    }
52
53    pub fn builder() -> ClientBuilder {
54        ClientBuilder::default()
55    }
56    pub fn clipzy_zai_xian_jian_tie_ban(&self) -> ClipzyZaiXianJianTieBanService<'_> {
57        ClipzyZaiXianJianTieBanService { client: self }
58    }
59    pub fn convert(&self) -> ConvertService<'_> {
60        ConvertService { client: self }
61    }
62    pub fn daily(&self) -> DailyService<'_> {
63        DailyService { client: self }
64    }
65    pub fn game(&self) -> GameService<'_> {
66        GameService { client: self }
67    }
68    pub fn image(&self) -> ImageService<'_> {
69        ImageService { client: self }
70    }
71    pub fn misc(&self) -> MiscService<'_> {
72        MiscService { client: self }
73    }
74    pub fn network(&self) -> NetworkService<'_> {
75        NetworkService { client: self }
76    }
77    pub fn poem(&self) -> PoemService<'_> {
78        PoemService { client: self }
79    }
80    pub fn random(&self) -> RandomService<'_> {
81        RandomService { client: self }
82    }
83    pub fn social(&self) -> SocialService<'_> {
84        SocialService { client: self }
85    }
86    pub fn status(&self) -> StatusService<'_> {
87        StatusService { client: self }
88    }
89    pub fn text(&self) -> TextService<'_> {
90        TextService { client: self }
91    }
92    pub fn translate(&self) -> TranslateService<'_> {
93        TranslateService { client: self }
94    }
95    pub fn webparse(&self) -> WebparseService<'_> {
96        WebparseService { client: self }
97    }
98    pub fn min_gan_ci_shi_bie(&self) -> MinGanCiShiBieService<'_> {
99        MinGanCiShiBieService { client: self }
100    }
101    pub fn zhi_neng_sou_suo(&self) -> ZhiNengSouSuoService<'_> {
102        ZhiNengSouSuoService { client: self }
103    }
104
105    #[instrument(skip(self, headers, query), fields(method=%method, path=%path))]
106    pub(crate) async fn request_json<T: serde::de::DeserializeOwned>(
107        &self,
108        method: reqwest::Method,
109        path: &str,
110        headers: Option<HeaderMap>,
111        query: Option<Vec<(String, String)>>,
112        json_body: Option<serde_json::Value>,
113    ) -> Result<T> {
114        let url = self.base_url.join(path)?;
115        let mut req = self.http.request(method.clone(), url.clone());
116
117        let mut merged = HeaderMap::new();
118        merged.insert(USER_AGENT, HeaderValue::from_static(DEFAULT_UA));
119        if let Some(t) = &self.api_key {
120            let value = format!("Bearer {}", t);
121            if let Ok(h) = HeaderValue::from_str(&value) {
122                merged.insert(AUTHORIZATION, h);
123            }
124        }
125        if let Some(h) = headers {
126            merged.extend(h);
127        }
128        req = req.headers(merged);
129
130        if let Some(q) = query {
131            req = req.query(&q);
132        }
133        if let Some(body) = json_body {
134            req = req.json(&body);
135        }
136
137        debug!("request {}", url);
138        let resp = req.send().await?;
139        self.handle_json_response(resp).await
140    }
141
142    async fn handle_json_response<T: serde::de::DeserializeOwned>(
143        &self,
144        resp: reqwest::Response,
145    ) -> Result<T> {
146        let status = resp.status();
147        let req_id = find_request_id(resp.headers());
148        let retry_after = parse_retry_after(resp.headers());
149        if status.is_success() {
150            return Ok(resp.json::<T>().await?);
151        }
152        let text = resp.text().await.unwrap_or_default();
153        let parsed = serde_json::from_str::<ApiErrorBody>(&text).ok();
154        let msg = parsed
155            .as_ref()
156            .and_then(|b| b.message.clone())
157            .or_else(|| non_empty(text.clone()));
158        let code = parsed.as_ref().and_then(|b| b.code.clone());
159        let details = parsed.as_ref().and_then(|b| b.details.clone());
160        Err(map_status_to_error(
161            status,
162            code,
163            msg,
164            details,
165            req_id,
166            retry_after,
167        ))
168    }
169}
170
171#[derive(Default)]
172pub struct ClientBuilder {
173    api_key: Option<String>,
174    base_url: Option<Url>,
175    timeout: Option<Duration>,
176    client: Option<reqwest::Client>,
177    user_agent: Option<String>,
178}
179
180impl ClientBuilder {
181    pub fn api_key<T: Into<String>>(mut self, api_key: T) -> Self {
182        self.api_key = Some(api_key.into());
183        self
184    }
185    pub fn base_url(mut self, base: Url) -> Self {
186        self.base_url = Some(base);
187        self
188    }
189    pub fn timeout(mut self, secs: u64) -> Self {
190        self.timeout = Some(Duration::from_secs(secs));
191        self
192    }
193    pub fn user_agent<T: Into<String>>(mut self, ua: T) -> Self {
194        self.user_agent = Some(ua.into());
195        self
196    }
197    pub fn http_client(mut self, cli: reqwest::Client) -> Self {
198        self.client = Some(cli);
199        self
200    }
201
202    pub fn build(self) -> Result<Client> {
203        let http = if let Some(cli) = self.client {
204            cli
205        } else {
206            reqwest::Client::builder()
207                .timeout(self.timeout.unwrap_or(Duration::from_secs(20)))
208                .build()?
209        };
210        Ok(Client {
211            http,
212            base_url: self.base_url.unwrap_or_else(|| DEFAULT_BASE_URL.clone()),
213            api_key: self.api_key,
214            user_agent: self.user_agent.unwrap_or_else(|| DEFAULT_UA.to_string()),
215        })
216    }
217}
218
219fn find_request_id(headers: &HeaderMap) -> Option<String> {
220    const CANDIDATES: &[&str] = &["x-request-id", "x-amzn-requestid", "traceparent"];
221    for key in CANDIDATES {
222        if let Some(v) = headers.get(*key) {
223            if let Ok(text) = v.to_str() {
224                return Some(text.to_string());
225            }
226        }
227    }
228    None
229}
230
231fn parse_retry_after(headers: &HeaderMap) -> Option<u64> {
232    headers
233        .get(RETRY_AFTER)
234        .and_then(|v| v.to_str().ok())
235        .and_then(|s| s.trim().parse::<u64>().ok())
236}
237
238fn non_empty(s: String) -> Option<String> {
239    let trimmed = s.trim();
240    if trimmed.is_empty() {
241        None
242    } else {
243        Some(trimmed.to_owned())
244    }
245}
246
247fn map_status_to_error(
248    status: StatusCode,
249    code: Option<String>,
250    message: Option<String>,
251    details: Option<serde_json::Value>,
252    request_id: Option<String>,
253    retry_after: Option<u64>,
254) -> Error {
255    let s = status.as_u16();
256    match status {
257        StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => Error::AuthenticationError {
258            status: s,
259            message,
260            request_id,
261        },
262        StatusCode::TOO_MANY_REQUESTS => Error::RateLimitError {
263            status: s,
264            message,
265            retry_after_seconds: retry_after,
266            request_id,
267        },
268        StatusCode::NOT_FOUND => Error::NotFound {
269            status: s,
270            message,
271            request_id,
272        },
273        StatusCode::BAD_REQUEST => Error::ValidationError {
274            status: s,
275            message,
276            details,
277            request_id,
278        },
279        _ if status.is_server_error() => Error::ServerError {
280            status: s,
281            message,
282            request_id,
283        },
284        _ if status.is_client_error() => Error::ApiError {
285            status: s,
286            code,
287            message,
288            details,
289            request_id,
290        },
291        _ => Error::ApiError {
292            status: s,
293            code,
294            message,
295            details,
296            request_id,
297        },
298    }
299}