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}