torn_api/
faction.rs

1use std::collections::{BTreeMap, HashMap};
2
3use chrono::{DateTime, TimeZone, Utc};
4use serde::{
5    de::{Error, Unexpected, Visitor},
6    Deserialize, Deserializer,
7};
8
9use torn_api_macros::{ApiCategory, IntoOwned};
10
11use crate::de_util::{self, null_is_empty_dict};
12
13pub use crate::common::{Attack, AttackFull, LastAction, Status, Territory};
14
15#[derive(Debug, Clone, Copy, ApiCategory)]
16#[api(category = "faction")]
17#[non_exhaustive]
18pub enum FactionSelection {
19    #[api(type = "Basic", flatten)]
20    Basic,
21
22    #[api(type = "BTreeMap<i32, Attack>", field = "attacks")]
23    AttacksFull,
24
25    #[api(type = "BTreeMap<i32, AttackFull>", field = "attacks")]
26    Attacks,
27
28    #[api(
29        type = "HashMap<String, Territory>",
30        field = "territory",
31        with = "null_is_empty_dict"
32    )]
33    Territory,
34
35    #[api(type = "Option<Chain>", field = "chain", with = "deserialize_chain")]
36    Chain,
37}
38
39pub type Selection = FactionSelection;
40
41#[derive(Debug, IntoOwned, Deserialize)]
42pub struct Member<'a> {
43    pub name: &'a str,
44    pub level: i16,
45    pub days_in_faction: i16,
46    pub position: &'a str,
47    pub status: Status<'a>,
48    pub last_action: LastAction,
49}
50
51#[derive(Debug, IntoOwned, Deserialize)]
52pub struct FactionTerritoryWar<'a> {
53    pub territory_war_id: i32,
54    pub territory: &'a str,
55    pub assaulting_faction: i32,
56    pub defending_faction: i32,
57    pub score: i32,
58    pub required_score: i32,
59
60    #[serde(with = "chrono::serde::ts_seconds")]
61    pub start_time: DateTime<Utc>,
62
63    #[serde(with = "chrono::serde::ts_seconds")]
64    pub end_time: DateTime<Utc>,
65}
66
67#[derive(Debug, IntoOwned, Deserialize)]
68pub struct Basic<'a> {
69    #[serde(rename = "ID")]
70    pub id: i32,
71    pub name: &'a str,
72    pub leader: i32,
73
74    pub respect: i32,
75    pub age: i16,
76    pub capacity: i16,
77    pub best_chain: i32,
78
79    #[serde(deserialize_with = "de_util::empty_string_is_none")]
80    pub tag_image: Option<&'a str>,
81
82    #[serde(borrow)]
83    pub members: BTreeMap<i32, Member<'a>>,
84
85    #[serde(deserialize_with = "de_util::datetime_map")]
86    pub peace: BTreeMap<i32, DateTime<Utc>>,
87
88    #[serde(borrow, deserialize_with = "de_util::empty_dict_is_empty_array")]
89    pub territory_wars: Vec<FactionTerritoryWar<'a>>,
90}
91
92#[derive(Debug)]
93pub struct Chain {
94    pub current: i32,
95    pub max: i32,
96    #[cfg(feature = "decimal")]
97    pub modifier: rust_decimal::Decimal,
98    pub timeout: Option<i32>,
99    pub cooldown: Option<i32>,
100    pub start: DateTime<Utc>,
101    pub end: DateTime<Utc>,
102}
103
104fn deserialize_chain<'de, D>(deserializer: D) -> Result<Option<Chain>, D::Error>
105where
106    D: Deserializer<'de>,
107{
108    struct ChainVisitor;
109
110    impl<'de> Visitor<'de> for ChainVisitor {
111        type Value = Option<Chain>;
112
113        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
114            formatter.write_str("struct Chain")
115        }
116
117        fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
118        where
119            A: serde::de::MapAccess<'de>,
120        {
121            #[derive(Deserialize)]
122            #[serde(rename_all = "snake_case")]
123            enum Fields {
124                Current,
125                Max,
126                Modifier,
127                Timeout,
128                Cooldown,
129                Start,
130                End,
131                #[serde(other)]
132                Ignore,
133            }
134
135            let mut current = None;
136            let mut max = None;
137            #[cfg(feature = "decimal")]
138            let mut modifier = None;
139            let mut timeout = None;
140            let mut cooldown = None;
141            let mut start = None;
142            let mut end = None;
143
144            while let Some(key) = map.next_key()? {
145                match key {
146                    Fields::Current => {
147                        let value = map.next_value()?;
148                        if value != 0 {
149                            current = Some(value);
150                        }
151                    }
152                    Fields::Max => {
153                        max = Some(map.next_value()?);
154                    }
155                    Fields::Modifier => {
156                        #[cfg(feature = "decimal")]
157                        {
158                            modifier = Some(map.next_value()?);
159                        }
160                    }
161                    Fields::Timeout => {
162                        match map.next_value()? {
163                            0 => timeout = Some(None),
164                            val => timeout = Some(Some(val)),
165                        };
166                    }
167                    Fields::Cooldown => {
168                        match map.next_value()? {
169                            0 => cooldown = Some(None),
170                            val => cooldown = Some(Some(val)),
171                        };
172                    }
173                    Fields::Start => {
174                        let ts: i64 = map.next_value()?;
175                        start = Some(Utc.timestamp_opt(ts, 0).single().ok_or_else(|| {
176                            A::Error::invalid_value(Unexpected::Signed(ts), &"Epoch timestamp")
177                        })?);
178                    }
179                    Fields::End => {
180                        let ts: i64 = map.next_value()?;
181                        end = Some(Utc.timestamp_opt(ts, 0).single().ok_or_else(|| {
182                            A::Error::invalid_value(Unexpected::Signed(ts), &"Epoch timestamp")
183                        })?);
184                    }
185                    Fields::Ignore => (),
186                }
187            }
188
189            let Some(current) = current else {
190                return Ok(None);
191            };
192            let max = max.ok_or_else(|| A::Error::missing_field("max"))?;
193            let timeout = timeout.ok_or_else(|| A::Error::missing_field("timeout"))?;
194            let cooldown = cooldown.ok_or_else(|| A::Error::missing_field("cooldown"))?;
195            let start = start.ok_or_else(|| A::Error::missing_field("start"))?;
196            let end = end.ok_or_else(|| A::Error::missing_field("end"))?;
197
198            Ok(Some(Chain {
199                current,
200                max,
201                #[cfg(feature = "decimal")]
202                modifier: modifier.ok_or_else(|| A::Error::missing_field("modifier"))?,
203                timeout,
204                cooldown,
205                start,
206                end,
207            }))
208        }
209    }
210
211    deserializer.deserialize_map(ChainVisitor)
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use crate::tests::{async_test, setup, Client, ClientTrait};
218
219    #[async_test]
220    async fn faction() {
221        let key = setup();
222
223        let response = Client::default()
224            .torn_api(key)
225            .faction(|b| {
226                b.selections([
227                    Selection::Basic,
228                    Selection::Attacks,
229                    Selection::Territory,
230                    Selection::Chain,
231                ])
232            })
233            .await
234            .unwrap();
235
236        response.basic().unwrap();
237        response.attacks().unwrap();
238        response.attacks_full().unwrap();
239        response.territory().unwrap();
240        response.chain().unwrap();
241    }
242
243    #[async_test]
244    async fn faction_public() {
245        let key = setup();
246
247        let response = Client::default()
248            .torn_api(key)
249            .faction(|b| {
250                b.id(7049)
251                    .selections([Selection::Basic, Selection::Territory, Selection::Chain])
252            })
253            .await
254            .unwrap();
255
256        response.basic().unwrap();
257        response.territory().unwrap();
258        response.chain().unwrap();
259    }
260
261    #[async_test]
262    async fn destroyed_faction() {
263        let key = setup();
264
265        let response = Client::default()
266            .torn_api(key)
267            .faction(|b| {
268                b.id(8981)
269                    .selections([Selection::Basic, Selection::Territory, Selection::Chain])
270            })
271            .await
272            .unwrap();
273
274        response.basic().unwrap();
275        response.territory().unwrap();
276        assert!(response.chain().unwrap().is_none());
277    }
278}