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}