torn_api/
torn.rs

1use std::collections::{BTreeMap, HashMap};
2
3use chrono::{DateTime, Utc};
4use serde::{
5    de::{self, MapAccess, Visitor},
6    Deserialize, Deserializer,
7};
8
9use torn_api_macros::ApiCategory;
10
11use crate::{de_util, user};
12
13#[derive(Debug, Clone, Copy, ApiCategory)]
14#[api(category = "torn")]
15#[non_exhaustive]
16pub enum TornSelection {
17    #[api(
18        field = "competition",
19        with = "decode_competition",
20        type = "Option<Competition>"
21    )]
22    Competition,
23
24    #[api(
25        type = "HashMap<String, TerritoryWar>",
26        with = "decode_territory_wars",
27        field = "territorywars"
28    )]
29    TerritoryWars,
30
31    #[api(type = "HashMap<String, Racket>", field = "rackets")]
32    Rackets,
33
34    #[api(
35        type = "HashMap<String, Territory>",
36        with = "decode_territory",
37        field = "territory"
38    )]
39    Territory,
40
41    #[api(type = "TerritoryWarReport", field = "territorywarreport")]
42    TerritoryWarReport,
43
44    #[api(type = "BTreeMap<i32, Item>", field = "items")]
45    Items,
46}
47
48pub type Selection = TornSelection;
49
50#[derive(Debug, Clone, Deserialize)]
51pub struct EliminationLeaderboard {
52    pub position: i16,
53    pub team: user::EliminationTeam,
54    pub score: i16,
55    pub lives: i16,
56    pub participants: Option<i16>,
57    pub wins: Option<i32>,
58    pub losses: Option<i32>,
59}
60
61#[derive(Debug, Clone)]
62pub enum Competition {
63    Elimination { teams: Vec<EliminationLeaderboard> },
64    Unkown(String),
65}
66
67fn decode_territory_wars<'de, D>(deserializer: D) -> Result<HashMap<String, TerritoryWar>, D::Error>
68where
69    D: serde::Deserializer<'de>,
70{
71    let map: Option<_> = serde::Deserialize::deserialize(deserializer)?;
72
73    Ok(map.unwrap_or_default())
74}
75
76fn decode_competition<'de, D>(deserializer: D) -> Result<Option<Competition>, D::Error>
77where
78    D: serde::Deserializer<'de>,
79{
80    struct CompetitionVisitor;
81
82    impl<'de> Visitor<'de> for CompetitionVisitor {
83        type Value = Option<Competition>;
84
85        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
86            formatter.write_str("struct Competition")
87        }
88
89        fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
90        where
91            D: serde::Deserializer<'de>,
92        {
93            deserializer.deserialize_map(self)
94        }
95
96        fn visit_none<E>(self) -> Result<Self::Value, E>
97        where
98            E: de::Error,
99        {
100            Ok(None)
101        }
102
103        fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
104        where
105            V: MapAccess<'de>,
106        {
107            let mut name = None;
108            let mut teams = None;
109
110            while let Some(key) = map.next_key()? {
111                match key {
112                    "name" => {
113                        name = Some(map.next_value()?);
114                    }
115                    "teams" => {
116                        teams = Some(map.next_value()?);
117                    }
118                    _ => (),
119                };
120            }
121
122            let name = name.ok_or_else(|| de::Error::missing_field("name"))?;
123
124            match name {
125                "Elimination" => Ok(Some(Competition::Elimination {
126                    teams: teams.ok_or_else(|| de::Error::missing_field("teams"))?,
127                })),
128                "" => Ok(None),
129                v => Ok(Some(Competition::Unkown(v.to_owned()))),
130            }
131        }
132    }
133
134    deserializer.deserialize_option(CompetitionVisitor)
135}
136
137#[derive(Debug, Clone, Deserialize)]
138pub struct TerritoryWar {
139    pub territory_war_id: i32,
140    pub assaulting_faction: i32,
141    pub defending_faction: i32,
142
143    #[serde(with = "chrono::serde::ts_seconds")]
144    pub started: DateTime<Utc>,
145    #[serde(with = "chrono::serde::ts_seconds")]
146    pub ends: DateTime<Utc>,
147}
148
149#[derive(Debug, Clone, Deserialize)]
150pub struct Racket {
151    pub name: String,
152    pub level: i16,
153    pub reward: String,
154
155    #[serde(with = "chrono::serde::ts_seconds")]
156    pub created: DateTime<Utc>,
157    #[serde(with = "chrono::serde::ts_seconds")]
158    pub changed: DateTime<Utc>,
159
160    #[serde(rename = "faction")]
161    pub faction_id: Option<i32>,
162}
163
164#[derive(Debug, Clone, Deserialize)]
165pub struct Territory {
166    pub sector: i16,
167    pub size: i16,
168    pub slots: i16,
169    pub daily_respect: i16,
170    pub faction: i32,
171
172    pub neighbors: Vec<String>,
173    pub war: Option<TerritoryWar>,
174    pub racket: Option<Racket>,
175}
176
177fn decode_territory<'de, D>(deserializer: D) -> Result<HashMap<String, Territory>, D::Error>
178where
179    D: Deserializer<'de>,
180{
181    Ok(Option::deserialize(deserializer)?.unwrap_or_default())
182}
183
184#[derive(Clone, Debug, Deserialize)]
185pub struct TerritoryWarReportTerritory {
186    pub name: String,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
190#[serde(rename_all = "snake_case")]
191pub enum TerritoryWarOutcome {
192    EndWithPeaceTreaty,
193    EndWithDestroyDefense,
194    FailAssault,
195    SuccessAssault,
196}
197
198#[derive(Clone, Debug, Deserialize)]
199pub struct TerritoryWarReportWar {
200    #[serde(with = "chrono::serde::ts_seconds")]
201    pub start: DateTime<Utc>,
202    #[serde(with = "chrono::serde::ts_seconds")]
203    pub end: DateTime<Utc>,
204
205    pub result: TerritoryWarOutcome,
206}
207
208#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Hash)]
209#[serde(rename_all = "snake_case")]
210pub enum TerritoryWarReportRole {
211    Aggressor,
212    Defender,
213}
214
215#[derive(Debug, Clone, Deserialize)]
216pub struct TerritoryWarReportFaction {
217    pub name: String,
218    pub score: i32,
219    pub joins: i32,
220    pub clears: i32,
221    #[serde(rename = "type")]
222    pub role: TerritoryWarReportRole,
223}
224
225#[derive(Clone, Debug, Deserialize)]
226pub struct TerritoryWarReport {
227    pub territory: TerritoryWarReportTerritory,
228    pub war: TerritoryWarReportWar,
229    pub factions: HashMap<i32, TerritoryWarReportFaction>,
230}
231
232#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Hash)]
233#[non_exhaustive]
234pub enum ItemType {
235    Primary,
236    Secondary,
237    Melee,
238    Temporary,
239    Defensive,
240    Collectible,
241    Medical,
242    Drug,
243    Booster,
244    #[serde(rename = "Energy Drink")]
245    EnergyDrink,
246    Alcohol,
247    Book,
248    Candy,
249    Car,
250    Clothing,
251    Electronic,
252    Enhancer,
253    Flower,
254    Jewelry,
255    Other,
256    Special,
257    #[serde(rename = "Supply Pack")]
258    SupplyPack,
259    Virus,
260}
261
262#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Hash)]
263#[non_exhaustive]
264//Missing hand to hand because it is not possible as a weapon
265pub enum WeaponType {
266    Slashing,
267    Rifle,
268    SMG,
269    Piercing,
270    Clubbing,
271    Pistol,
272    #[serde(rename = "Machine gun")]
273    MachineGun,
274    Mechanical,
275    Temporary,
276    Heavy,
277    Shotgun,
278}
279
280#[derive(Debug, Clone, Deserialize)]
281pub struct Item<'a> {
282    pub name: String,
283    pub description: String,
284    #[serde(deserialize_with = "de_util::empty_string_is_none")]
285    pub effect: Option<&'a str>,
286    #[serde(deserialize_with = "de_util::empty_string_is_none")]
287    pub requirement: Option<&'a str>,
288    #[serde(rename = "type")]
289    pub item_type: ItemType,
290    pub weapon_type: Option<WeaponType>,
291    #[serde(deserialize_with = "de_util::zero_is_none")]
292    pub buy_price: Option<u64>,
293    #[serde(deserialize_with = "de_util::zero_is_none")]
294    pub sell_price: Option<u64>,
295    #[serde(deserialize_with = "de_util::zero_is_none")]
296    pub market_value: Option<u64>,
297    #[serde(deserialize_with = "de_util::zero_is_none")]
298    pub circulation: Option<u32>,
299    pub image: String,
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305    use crate::tests::{async_test, setup, Client, ClientTrait};
306
307    #[async_test]
308    async fn competition() {
309        let key = setup();
310
311        let response = Client::default()
312            .torn_api(key)
313            .torn(|b| {
314                b.selections([
315                    TornSelection::Competition,
316                    TornSelection::TerritoryWars,
317                    TornSelection::Rackets,
318                ])
319            })
320            .await
321            .unwrap();
322
323        response.competition().unwrap();
324        response.territory_wars().unwrap();
325        response.rackets().unwrap();
326    }
327
328    #[async_test]
329    async fn territory() {
330        let key = setup();
331
332        let response = Client::default()
333            .torn_api(key)
334            .torn(|b| b.selections([Selection::Territory]).id("NSC"))
335            .await
336            .unwrap();
337
338        let territory = response.territory().unwrap();
339        assert!(territory.contains_key("NSC"));
340    }
341
342    #[async_test]
343    async fn invalid_territory() {
344        let key = setup();
345
346        let response = Client::default()
347            .torn_api(key)
348            .torn(|b| b.selections([Selection::Territory]).id("AAA"))
349            .await
350            .unwrap();
351
352        assert!(response.territory().unwrap().is_empty());
353    }
354
355    #[async_test]
356    async fn territory_war_report() {
357        let key = setup();
358
359        let response = Client::default()
360            .torn_api(&key)
361            .torn(|b| b.selections([Selection::TerritoryWarReport]).id(37403))
362            .await
363            .unwrap();
364
365        assert_eq!(
366            response.territory_war_report().unwrap().war.result,
367            TerritoryWarOutcome::SuccessAssault
368        );
369
370        let response = Client::default()
371            .torn_api(&key)
372            .torn(|b| b.selections([Selection::TerritoryWarReport]).id(37502))
373            .await
374            .unwrap();
375
376        assert_eq!(
377            response.territory_war_report().unwrap().war.result,
378            TerritoryWarOutcome::FailAssault
379        );
380
381        let response = Client::default()
382            .torn_api(&key)
383            .torn(|b| b.selections([Selection::TerritoryWarReport]).id(37860))
384            .await
385            .unwrap();
386
387        assert_eq!(
388            response.territory_war_report().unwrap().war.result,
389            TerritoryWarOutcome::EndWithPeaceTreaty
390        );
391
392        let response = Client::default()
393            .torn_api(&key)
394            .torn(|b| b.selections([Selection::TerritoryWarReport]).id(23757))
395            .await
396            .unwrap();
397
398        assert_eq!(
399            response.territory_war_report().unwrap().war.result,
400            TerritoryWarOutcome::EndWithDestroyDefense
401        );
402    }
403
404    #[async_test]
405    async fn item() {
406        let key = setup();
407
408        let response = Client::default()
409            .torn_api(key)
410            .torn(|b| b.selections([Selection::Items]).id(837))
411            .await
412            .unwrap();
413
414        let item_list = response.items().unwrap();
415        assert!(item_list.contains_key(&837));
416    }
417}