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}