1#![warn(clippy::all, clippy::perf, clippy::style, clippy::suspicious)]
2
3pub mod into_owned;
4pub mod local;
5pub mod send;
6
7#[cfg(feature = "user")]
8pub mod user;
9
10#[cfg(feature = "faction")]
11pub mod faction;
12
13#[cfg(feature = "market")]
14pub mod market;
15
16#[cfg(feature = "torn")]
17pub mod torn;
18
19#[cfg(feature = "key")]
20pub mod key;
21
22#[cfg(feature = "awc")]
23pub mod awc;
24
25#[cfg(feature = "reqwest")]
26pub mod reqwest;
27
28#[cfg(feature = "__common")]
29pub mod common;
30
31mod de_util;
32
33use std::fmt::Write;
34
35use chrono::{DateTime, Utc};
36use serde::{de::Error as DeError, Deserialize};
37use thiserror::Error;
38
39pub use into_owned::IntoOwned;
40
41pub struct ApiResponse {
42 pub value: serde_json::Value,
43}
44
45#[derive(Error, Debug)]
46pub enum ResponseError {
47 #[error("API: {reason}")]
48 Api { code: u8, reason: String },
49
50 #[error(transparent)]
51 MalformedResponse(#[from] serde_json::Error),
52}
53
54impl ResponseError {
55 pub fn api_code(&self) -> Option<u8> {
56 match self {
57 Self::Api { code, .. } => Some(*code),
58 _ => None,
59 }
60 }
61}
62
63impl ApiResponse {
64 pub fn from_value(mut value: serde_json::Value) -> Result<Self, ResponseError> {
65 #[derive(serde::Deserialize)]
66 struct ApiErrorDto {
67 code: u8,
68 #[serde(rename = "error")]
69 reason: String,
70 }
71 match value.get_mut("error") {
72 Some(error) => {
73 let dto: ApiErrorDto = serde_json::from_value(error.take())?;
74 Err(ResponseError::Api {
75 code: dto.code,
76 reason: dto.reason,
77 })
78 }
79 None => Ok(Self { value }),
80 }
81 }
82
83 #[allow(dead_code)]
84 fn decode<'de, D>(&'de self) -> serde_json::Result<D>
85 where
86 D: Deserialize<'de>,
87 {
88 D::deserialize(&self.value)
89 }
90
91 #[allow(dead_code)]
92 fn decode_field<'de, D>(&'de self, field: &'static str) -> serde_json::Result<D>
93 where
94 D: Deserialize<'de>,
95 {
96 self.value
97 .get(field)
98 .ok_or_else(|| serde_json::Error::missing_field(field))
99 .and_then(D::deserialize)
100 }
101
102 #[allow(dead_code)]
103 fn decode_field_with<'de, V, F>(&'de self, field: &'static str, fun: F) -> serde_json::Result<V>
104 where
105 F: FnOnce(&'de serde_json::Value) -> serde_json::Result<V>,
106 {
107 self.value
108 .get(field)
109 .ok_or_else(|| serde_json::Error::missing_field(field))
110 .and_then(fun)
111 }
112}
113
114pub trait ApiSelectionResponse: Send + Sync + From<ApiResponse> + 'static {
115 fn into_inner(self) -> ApiResponse;
116}
117
118pub trait ApiSelection: Send + Sync + 'static {
119 type Response: ApiSelectionResponse;
120
121 fn raw_value(self) -> &'static str;
122
123 fn category() -> &'static str;
124}
125
126pub struct DirectExecutor<C> {
127 key: String,
128 _marker: std::marker::PhantomData<C>,
129}
130
131impl<C> DirectExecutor<C> {
132 fn new(key: String) -> Self {
133 Self {
134 key,
135 _marker: Default::default(),
136 }
137 }
138}
139
140#[derive(Error, Debug)]
141pub enum ApiClientError<C>
142where
143 C: std::error::Error,
144{
145 #[error(transparent)]
146 Client(C),
147
148 #[error(transparent)]
149 Response(#[from] ResponseError),
150}
151
152impl<C> ApiClientError<C>
153where
154 C: std::error::Error,
155{
156 pub fn api_code(&self) -> Option<u8> {
157 match self {
158 Self::Response(err) => err.api_code(),
159 _ => None,
160 }
161 }
162}
163
164#[derive(Debug)]
165pub struct ApiRequest<A>
166where
167 A: ApiSelection,
168{
169 pub selections: Vec<&'static str>,
170 pub query_items: Vec<(&'static str, String)>,
171 pub comment: Option<String>,
172 phantom: std::marker::PhantomData<A>,
173}
174
175impl<A> std::default::Default for ApiRequest<A>
176where
177 A: ApiSelection,
178{
179 fn default() -> Self {
180 Self {
181 selections: Vec::default(),
182 query_items: Vec::default(),
183 comment: None,
184 phantom: Default::default(),
185 }
186 }
187}
188
189impl<A> ApiRequest<A>
190where
191 A: ApiSelection,
192{
193 fn add_query_item(&mut self, name: &'static str, value: impl ToString) {
194 if let Some((_, old)) = self.query_items.iter_mut().find(|(n, _)| *n == name) {
195 *old = value.to_string();
196 } else {
197 self.query_items.push((name, value.to_string()));
198 }
199 }
200
201 pub fn url(&self, key: &str, id: Option<&str>) -> String {
202 let mut url = format!("https://api.torn.com/{}/", A::category());
203
204 if let Some(id) = id {
205 write!(url, "{}", id).unwrap();
206 }
207
208 write!(url, "?selections={}&key={}", self.selections.join(","), key).unwrap();
209
210 for (name, value) in &self.query_items {
211 write!(url, "&{name}={value}").unwrap();
212 }
213
214 if let Some(comment) = &self.comment {
215 write!(url, "&comment={}", comment).unwrap();
216 }
217
218 url
219 }
220}
221
222pub struct ApiRequestBuilder<A>
223where
224 A: ApiSelection,
225{
226 pub request: ApiRequest<A>,
227 pub id: Option<String>,
228}
229
230impl<A> Default for ApiRequestBuilder<A>
231where
232 A: ApiSelection,
233{
234 fn default() -> Self {
235 Self {
236 request: Default::default(),
237 id: None,
238 }
239 }
240}
241
242impl<A> ApiRequestBuilder<A>
243where
244 A: ApiSelection,
245{
246 #[must_use]
247 pub fn selections(mut self, selections: impl IntoIterator<Item = A>) -> Self {
248 self.request.selections.append(
249 &mut selections
250 .into_iter()
251 .map(ApiSelection::raw_value)
252 .collect(),
253 );
254 self
255 }
256
257 #[must_use]
258 pub fn from(mut self, from: DateTime<Utc>) -> Self {
259 self.request.add_query_item("from", from.timestamp());
260 self
261 }
262
263 #[must_use]
264 pub fn from_timestamp(mut self, from: i64) -> Self {
265 self.request.add_query_item("from", from);
266 self
267 }
268
269 #[must_use]
270 pub fn to(mut self, to: DateTime<Utc>) -> Self {
271 self.request.add_query_item("to", to.timestamp());
272 self
273 }
274
275 #[must_use]
276 pub fn to_timestamp(mut self, to: i64) -> Self {
277 self.request.add_query_item("to", to);
278 self
279 }
280
281 #[must_use]
282 pub fn stats_timestamp(mut self, ts: i64) -> Self {
283 self.request.add_query_item("timestamp", ts);
284 self
285 }
286
287 #[must_use]
288 pub fn stats_datetime(mut self, dt: DateTime<Utc>) -> Self {
289 self.request.add_query_item("timestamp", dt.timestamp());
290 self
291 }
292
293 #[must_use]
294 pub fn comment(mut self, comment: String) -> Self {
295 self.request.comment = Some(comment);
296 self
297 }
298
299 #[must_use]
300 pub fn id<I>(mut self, id: I) -> Self
301 where
302 I: ToString,
303 {
304 self.id = Some(id.to_string());
305 self
306 }
307}
308
309#[cfg(test)]
310#[allow(unused)]
311pub(crate) mod tests {
312 use std::sync::Once;
313
314 #[cfg(all(not(feature = "reqwest"), feature = "awc"))]
315 pub use ::awc::Client;
316 #[cfg(feature = "reqwest")]
317 pub use ::reqwest::Client;
318
319 #[cfg(all(not(feature = "reqwest"), feature = "awc"))]
320 pub use crate::local::ApiClient as ClientTrait;
321 #[cfg(feature = "reqwest")]
322 pub use crate::send::ApiClient as ClientTrait;
323
324 #[cfg(all(not(feature = "reqwest"), feature = "awc"))]
325 pub use actix_rt::test as async_test;
326 #[cfg(feature = "reqwest")]
327 pub use tokio::test as async_test;
328
329 use super::*;
330
331 static INIT: Once = Once::new();
332
333 pub(crate) fn setup() -> String {
334 INIT.call_once(|| {
335 dotenv::dotenv().ok();
336 });
337 std::env::var("APIKEY").expect("api key")
338 }
339
340 #[cfg(feature = "user")]
341 #[test]
342 fn selection_raw_value() {
343 assert_eq!(user::Selection::Basic.raw_value(), "basic");
344 }
345
346 #[cfg(all(feature = "reqwest", feature = "user"))]
347 #[tokio::test]
348 async fn reqwest() {
349 let key = setup();
350
351 Client::default().torn_api(key).user(|b| b).await.unwrap();
352 }
353
354 #[cfg(all(feature = "awc", feature = "user"))]
355 #[actix_rt::test]
356 async fn awc() {
357 let key = setup();
358
359 Client::default().torn_api(key).user(|b| b).await.unwrap();
360 }
361
362 #[test]
363 fn url_builder_from_dt() {
364 let url = ApiRequestBuilder::<user::Selection>::default()
365 .from(DateTime::default())
366 .request
367 .url("", None);
368
369 assert_eq!("https://api.torn.com/user/?selections=&key=&from=0", url);
370 }
371
372 #[test]
373 fn url_builder_from_ts() {
374 let url = ApiRequestBuilder::<user::Selection>::default()
375 .from_timestamp(12345)
376 .request
377 .url("", None);
378
379 assert_eq!(
380 "https://api.torn.com/user/?selections=&key=&from=12345",
381 url
382 );
383 }
384
385 #[test]
386 fn url_builder_to_dt() {
387 let url = ApiRequestBuilder::<user::Selection>::default()
388 .to(DateTime::default())
389 .request
390 .url("", None);
391
392 assert_eq!("https://api.torn.com/user/?selections=&key=&to=0", url);
393 }
394
395 #[test]
396 fn url_builder_to_ts() {
397 let url = ApiRequestBuilder::<user::Selection>::default()
398 .to_timestamp(12345)
399 .request
400 .url("", None);
401
402 assert_eq!("https://api.torn.com/user/?selections=&key=&to=12345", url);
403 }
404
405 #[test]
406 fn url_builder_timestamp_dt() {
407 let url = ApiRequestBuilder::<user::Selection>::default()
408 .stats_datetime(DateTime::default())
409 .request
410 .url("", None);
411
412 assert_eq!(
413 "https://api.torn.com/user/?selections=&key=×tamp=0",
414 url
415 );
416 }
417
418 #[test]
419 fn url_builder_timestamp_ts() {
420 let url = ApiRequestBuilder::<user::Selection>::default()
421 .stats_timestamp(12345)
422 .request
423 .url("", None);
424
425 assert_eq!(
426 "https://api.torn.com/user/?selections=&key=×tamp=12345",
427 url
428 );
429 }
430
431 #[test]
432 fn url_builder_duplicate() {
433 let url = ApiRequestBuilder::<user::Selection>::default()
434 .from(DateTime::default())
435 .from_timestamp(12345)
436 .request
437 .url("", None);
438
439 assert_eq!(
440 "https://api.torn.com/user/?selections=&key=&from=12345",
441 url
442 );
443 }
444
445 #[test]
446 fn url_builder_many_options() {
447 let url = ApiRequestBuilder::<user::Selection>::default()
448 .from(DateTime::default())
449 .to_timestamp(60)
450 .stats_timestamp(12345)
451 .selections([user::Selection::PersonalStats])
452 .request
453 .url("KEY", Some("1"));
454
455 assert_eq!(
456 "https://api.torn.com/user/1?selections=personalstats&key=KEY&from=0&to=60×tamp=12345",
457 url
458 );
459 }
460}