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.3";
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 clean_path = path.trim_start_matches('/');
115 let url = self.base_url.join(clean_path)?;
116 let mut req = self.http.request(method.clone(), url.clone());
117
118 let mut merged = HeaderMap::new();
119 merged.insert(USER_AGENT, HeaderValue::from_static(DEFAULT_UA));
120 if let Some(t) = &self.api_key {
121 let value = format!("Bearer {}", t);
122 if let Ok(h) = HeaderValue::from_str(&value) {
123 merged.insert(AUTHORIZATION, h);
124 }
125 }
126 if let Some(h) = headers {
127 merged.extend(h);
128 }
129 req = req.headers(merged);
130
131 if let Some(q) = query {
132 req = req.query(&q);
133 }
134 if let Some(body) = json_body {
135 req = req.json(&body);
136 }
137
138 debug!("request {}", url);
139 let resp = req.send().await?;
140 self.handle_json_response(resp).await
141 }
142
143 async fn handle_json_response<T: serde::de::DeserializeOwned>(
144 &self,
145 resp: reqwest::Response,
146 ) -> Result<T> {
147 let status = resp.status();
148 let req_id = find_request_id(resp.headers());
149 let retry_after = parse_retry_after(resp.headers());
150 if status.is_success() {
151 return Ok(resp.json::<T>().await?);
152 }
153 let text = resp.text().await.unwrap_or_default();
154 let parsed = serde_json::from_str::<ApiErrorBody>(&text).ok();
155 let msg = parsed
156 .as_ref()
157 .and_then(|b| b.message.clone())
158 .or_else(|| non_empty(text.clone()));
159 let code = parsed.as_ref().and_then(|b| b.code.clone());
160 let details = parsed.as_ref().and_then(|b| b.details.clone());
161 Err(map_status_to_error(
162 status,
163 code,
164 msg,
165 details,
166 req_id,
167 retry_after,
168 ))
169 }
170}
171
172#[derive(Default)]
173pub struct ClientBuilder {
174 api_key: Option<String>,
175 base_url: Option<Url>,
176 timeout: Option<Duration>,
177 client: Option<reqwest::Client>,
178 user_agent: Option<String>,
179}
180
181impl ClientBuilder {
182 pub fn api_key<T: Into<String>>(mut self, api_key: T) -> Self {
183 self.api_key = Some(api_key.into());
184 self
185 }
186 pub fn base_url(mut self, base: Url) -> Self {
187 self.base_url = Some(base);
188 self
189 }
190 pub fn timeout(mut self, secs: u64) -> Self {
191 self.timeout = Some(Duration::from_secs(secs));
192 self
193 }
194 pub fn user_agent<T: Into<String>>(mut self, ua: T) -> Self {
195 self.user_agent = Some(ua.into());
196 self
197 }
198 pub fn http_client(mut self, cli: reqwest::Client) -> Self {
199 self.client = Some(cli);
200 self
201 }
202
203 pub fn build(self) -> Result<Client> {
204 let http = if let Some(cli) = self.client {
205 cli
206 } else {
207 reqwest::Client::builder()
208 .timeout(self.timeout.unwrap_or(Duration::from_secs(20)))
209 .build()?
210 };
211 Ok(Client {
212 http,
213 base_url: self.base_url.unwrap_or_else(|| DEFAULT_BASE_URL.clone()),
214 api_key: self.api_key,
215 user_agent: self.user_agent.unwrap_or_else(|| DEFAULT_UA.to_string()),
216 })
217 }
218}
219
220fn find_request_id(headers: &HeaderMap) -> Option<String> {
221 const CANDIDATES: &[&str] = &["x-request-id", "x-amzn-requestid", "traceparent"];
222 for key in CANDIDATES {
223 if let Some(v) = headers.get(*key) {
224 if let Ok(text) = v.to_str() {
225 return Some(text.to_string());
226 }
227 }
228 }
229 None
230}
231
232fn parse_retry_after(headers: &HeaderMap) -> Option<u64> {
233 headers
234 .get(RETRY_AFTER)
235 .and_then(|v| v.to_str().ok())
236 .and_then(|s| s.trim().parse::<u64>().ok())
237}
238
239fn non_empty(s: String) -> Option<String> {
240 let trimmed = s.trim();
241 if trimmed.is_empty() {
242 None
243 } else {
244 Some(trimmed.to_owned())
245 }
246}
247
248fn map_status_to_error(
249 status: StatusCode,
250 code: Option<String>,
251 message: Option<String>,
252 details: Option<serde_json::Value>,
253 request_id: Option<String>,
254 retry_after: Option<u64>,
255) -> Error {
256 let s = status.as_u16();
257 match status {
258 StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => Error::AuthenticationError {
259 status: s,
260 message,
261 request_id,
262 },
263 StatusCode::TOO_MANY_REQUESTS => Error::RateLimitError {
264 status: s,
265 message,
266 retry_after_seconds: retry_after,
267 request_id,
268 },
269 StatusCode::NOT_FOUND => Error::NotFound {
270 status: s,
271 message,
272 request_id,
273 },
274 StatusCode::BAD_REQUEST => Error::ValidationError {
275 status: s,
276 message,
277 details,
278 request_id,
279 },
280 _ if status.is_server_error() => Error::ServerError {
281 status: s,
282 message,
283 request_id,
284 },
285 _ if status.is_client_error() => Error::ApiError {
286 status: s,
287 code,
288 message,
289 details,
290 request_id,
291 },
292 _ => Error::ApiError {
293 status: s,
294 code,
295 message,
296 details,
297 request_id,
298 },
299 }
300}