Merge remote-tracking branch 'origin/master'

This commit is contained in:
TotallyNot 2023-07-29 17:40:10 +02:00
commit 4f3d62da95
14 changed files with 627 additions and 325 deletions

View file

@ -56,3 +56,108 @@ pub struct Territory {
#[serde(deserialize_with = "de_util::string_or_decimal")]
pub coordinate_y: rust_decimal::Decimal,
}
#[derive(Debug, Clone, Copy, Deserialize)]
pub enum AttackResult {
Attacked,
Mugged,
Hospitalized,
Lost,
Arrested,
Escape,
Interrupted,
Assist,
Timeout,
Stalemate,
Special,
Looted,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Attack<'a> {
pub code: &'a str,
#[serde(with = "ts_seconds")]
pub timestamp_started: DateTime<Utc>,
#[serde(with = "ts_seconds")]
pub timestamp_ended: DateTime<Utc>,
#[serde(deserialize_with = "de_util::empty_string_int_option")]
pub attacker_id: Option<i32>,
#[serde(deserialize_with = "de_util::empty_string_int_option")]
pub attacker_faction: Option<i32>,
pub defender_id: i32,
#[serde(deserialize_with = "de_util::empty_string_int_option")]
pub defender_faction: Option<i32>,
pub result: AttackResult,
#[serde(deserialize_with = "de_util::int_is_bool")]
pub stealthed: bool,
#[cfg(feature = "decimal")]
pub respect: rust_decimal::Decimal,
#[cfg(not(feature = "decimal"))]
pub respect: f32,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RespectModifiers {
pub fair_fight: f32,
pub war: f32,
pub retaliation: f32,
pub group_attack: f32,
pub overseas: f32,
pub chain_bonus: f32,
}
#[derive(Debug, Clone, Deserialize)]
pub struct AttackFull<'a> {
pub code: &'a str,
#[serde(with = "ts_seconds")]
pub timestamp_started: DateTime<Utc>,
#[serde(with = "ts_seconds")]
pub timestamp_ended: DateTime<Utc>,
#[serde(deserialize_with = "de_util::empty_string_int_option")]
pub attacker_id: Option<i32>,
#[serde(deserialize_with = "de_util::empty_string_is_none")]
pub attacker_name: Option<&'a str>,
#[serde(deserialize_with = "de_util::empty_string_int_option")]
pub attacker_faction: Option<i32>,
#[serde(
deserialize_with = "de_util::empty_string_is_none",
rename = "attacker_factionname"
)]
pub attacker_faction_name: Option<&'a str>,
pub defender_id: i32,
pub defender_name: &'a str,
#[serde(deserialize_with = "de_util::empty_string_int_option")]
pub defender_faction: Option<i32>,
#[serde(
deserialize_with = "de_util::empty_string_is_none",
rename = "defender_factionname"
)]
pub defender_faction_name: Option<&'a str>,
pub result: AttackResult,
#[serde(deserialize_with = "de_util::int_is_bool")]
pub stealthed: bool,
#[serde(deserialize_with = "de_util::int_is_bool")]
pub raid: bool,
#[serde(deserialize_with = "de_util::int_is_bool")]
pub ranked_war: bool,
#[cfg(feature = "decimal")]
pub respect: rust_decimal::Decimal,
#[cfg(feature = "decimal")]
pub respect_loss: rust_decimal::Decimal,
#[cfg(not(feature = "decimal"))]
pub respect: f32,
#[cfg(not(feature = "decimal"))]
pub respect_loss: f32,
pub modifiers: RespectModifiers,
}

View file

@ -1,8 +1,8 @@
#![allow(unused)]
use std::collections::BTreeMap;
use std::collections::{BTreeMap, HashMap};
use chrono::{DateTime, NaiveDateTime, Utc};
use chrono::{serde::ts_nanoseconds::deserialize, DateTime, NaiveDateTime, Utc};
use serde::de::{Deserialize, Deserializer, Error, Unexpected, Visitor};
pub(crate) fn empty_string_is_none<'de, D>(deserializer: D) -> Result<Option<&'de str>, D::Error>
@ -135,6 +135,62 @@ where
deserializer.deserialize_map(MapVisitor)
}
pub(crate) fn empty_dict_is_empty_array<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de>,
{
struct ArrayVisitor<T>(std::marker::PhantomData<T>);
impl<'de, T> Visitor<'de> for ArrayVisitor<T>
where
T: Deserialize<'de>,
{
type Value = Vec<T>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "vec or empty object")
}
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
match map.size_hint() {
Some(0) | None => Ok(Vec::default()),
Some(len) => Err(A::Error::invalid_length(len, &"empty dict")),
}
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut result = match seq.size_hint() {
Some(len) => Vec::with_capacity(len),
None => Vec::default(),
};
while let Some(element) = seq.next_element()? {
result.push(element);
}
Ok(result)
}
}
deserializer.deserialize_any(ArrayVisitor(std::marker::PhantomData))
}
pub(crate) fn null_is_empty_dict<'de, D, K, V>(deserializer: D) -> Result<HashMap<K, V>, D::Error>
where
D: Deserializer<'de>,
K: std::hash::Hash + std::cmp::Eq + Deserialize<'de>,
V: Deserialize<'de>,
{
Ok(Option::deserialize(deserializer)?.unwrap_or_default())
}
#[cfg(feature = "decimal")]
pub(crate) fn string_or_decimal<'de, D>(deserializer: D) -> Result<rust_decimal::Decimal, D::Error>
where

View file

@ -1,13 +1,13 @@
use std::collections::{BTreeMap, HashMap};
use chrono::{serde::ts_seconds, DateTime, Utc};
use chrono::{DateTime, Utc};
use serde::Deserialize;
use torn_api_macros::ApiCategory;
use crate::de_util;
use crate::de_util::{self, null_is_empty_dict};
pub use crate::common::{LastAction, Status, Territory};
pub use crate::common::{Attack, AttackFull, LastAction, Status, Territory};
#[derive(Debug, Clone, Copy, ApiCategory)]
#[api(category = "faction")]
@ -21,7 +21,11 @@ pub enum Selection {
#[api(type = "BTreeMap<i32, AttackFull>", field = "attacks")]
Attacks,
#[api(type = "HashMap<String, Territory>", field = "territory")]
#[api(
type = "HashMap<String, Territory>",
field = "territory",
with = "null_is_empty_dict"
)]
Territory,
}
@ -68,115 +72,10 @@ pub struct Basic<'a> {
#[serde(deserialize_with = "de_util::datetime_map")]
pub peace: BTreeMap<i32, DateTime<Utc>>,
#[serde(borrow)]
#[serde(borrow, deserialize_with = "de_util::empty_dict_is_empty_array")]
pub territory_wars: Vec<FactionTerritoryWar<'a>>,
}
#[derive(Debug, Clone, Copy, Deserialize)]
pub enum AttackResult {
Attacked,
Mugged,
Hospitalized,
Lost,
Arrested,
Escape,
Interrupted,
Assist,
Timeout,
Stalemate,
Special,
Looted,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Attack<'a> {
pub code: &'a str,
#[serde(with = "ts_seconds")]
pub timestamp_started: DateTime<Utc>,
#[serde(with = "ts_seconds")]
pub timestamp_ended: DateTime<Utc>,
#[serde(deserialize_with = "de_util::empty_string_int_option")]
pub attacker_id: Option<i32>,
#[serde(deserialize_with = "de_util::empty_string_int_option")]
pub attacker_faction: Option<i32>,
pub defender_id: i32,
#[serde(deserialize_with = "de_util::empty_string_int_option")]
pub defender_faction: Option<i32>,
pub result: AttackResult,
#[serde(deserialize_with = "de_util::int_is_bool")]
pub stealthed: bool,
#[cfg(feature = "decimal")]
pub respect: rust_decimal::Decimal,
#[cfg(not(feature = "decimal"))]
pub respect: f32,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RespectModifiers {
pub fair_fight: f32,
pub war: f32,
pub retaliation: f32,
pub group_attack: f32,
pub overseas: f32,
pub chain_bonus: f32,
}
#[derive(Debug, Clone, Deserialize)]
pub struct AttackFull<'a> {
pub code: &'a str,
#[serde(with = "ts_seconds")]
pub timestamp_started: DateTime<Utc>,
#[serde(with = "ts_seconds")]
pub timestamp_ended: DateTime<Utc>,
#[serde(deserialize_with = "de_util::empty_string_int_option")]
pub attacker_id: Option<i32>,
#[serde(deserialize_with = "de_util::empty_string_is_none")]
pub attacker_name: Option<&'a str>,
#[serde(deserialize_with = "de_util::empty_string_int_option")]
pub attacker_faction: Option<i32>,
#[serde(
deserialize_with = "de_util::empty_string_is_none",
rename = "attacker_factionname"
)]
pub attacker_faction_name: Option<&'a str>,
pub defender_id: i32,
pub defender_name: &'a str,
#[serde(deserialize_with = "de_util::empty_string_int_option")]
pub defender_faction: Option<i32>,
#[serde(
deserialize_with = "de_util::empty_string_is_none",
rename = "defender_factionname"
)]
pub defender_faction_name: Option<&'a str>,
pub result: AttackResult,
#[serde(deserialize_with = "de_util::int_is_bool")]
pub stealthed: bool,
#[serde(deserialize_with = "de_util::int_is_bool")]
pub raid: bool,
#[serde(deserialize_with = "de_util::int_is_bool")]
pub ranked_war: bool,
#[cfg(feature = "decimal")]
pub respect: rust_decimal::Decimal,
#[cfg(feature = "decimal")]
pub respect_loss: rust_decimal::Decimal,
#[cfg(not(feature = "decimal"))]
pub respect: f32,
#[cfg(not(feature = "decimal"))]
pub respect_loss: f32,
pub modifiers: RespectModifiers,
}
#[cfg(test)]
mod tests {
use super::*;
@ -199,4 +98,21 @@ mod tests {
response.attacks_full().unwrap();
response.territory().unwrap();
}
#[async_test]
async fn destroyed_faction() {
let key = setup();
let response = Client::default()
.torn_api(key)
.faction(|b| {
b.id(8981)
.selections(&[Selection::Basic, Selection::Territory])
})
.await
.unwrap();
response.basic().unwrap();
response.territory().unwrap();
}
}

View file

@ -122,6 +122,8 @@ pub enum FactionSelection {
Upgrades,
Weapons,
Lookup,
Caches,
CrimeExp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
@ -171,6 +173,8 @@ pub enum TornSelection {
Timestamp,
Lookup,
CityShops,
ItemDetails,
TerritoryNames,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]

View file

@ -22,7 +22,7 @@ pub mod awc;
pub mod reqwest;
#[cfg(feature = "__common")]
mod common;
pub mod common;
mod de_util;

View file

@ -22,6 +22,12 @@ pub enum Selection {
#[api(type = "HashMap<String, TerritoryWar>", field = "territorywars")]
TerritoryWars,
#[api(type = "HashMap<String, Racket>", field = "rackets")]
Rackets,
#[api(type = "HashMap<String, Territory>", field = "territory")]
Territory,
}
#[derive(Deserialize)]
@ -111,6 +117,31 @@ pub struct TerritoryWar {
pub ends: DateTime<Utc>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Racket {
pub name: String,
pub level: i16,
pub reward: String,
#[serde(with = "chrono::serde::ts_seconds")]
pub created: DateTime<Utc>,
#[serde(with = "chrono::serde::ts_seconds")]
pub changed: DateTime<Utc>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Territory {
pub sector: i16,
pub size: i16,
pub slots: i16,
pub daily_respect: i16,
pub faction: i32,
pub neighbors: Vec<String>,
pub war: Option<TerritoryWar>,
pub racket: Option<Racket>,
}
#[cfg(test)]
mod tests {
use super::*;
@ -122,11 +153,32 @@ mod tests {
let response = Client::default()
.torn_api(key)
.torn(|b| b.selections(&[Selection::Competition, Selection::TerritoryWars]))
.torn(|b| {
b.selections(&[
Selection::Competition,
Selection::TerritoryWars,
Selection::Rackets,
])
})
.await
.unwrap();
response.competition().unwrap();
response.territory_wars().unwrap();
response.rackets().unwrap();
}
#[async_test]
async fn territory() {
let key = setup();
let response = Client::default()
.torn_api(key)
.torn(|b| b.selections(&[Selection::Territory]).id("NSC"))
.await
.unwrap();
let territory = response.territory().unwrap();
assert!(territory.contains_key("NSC"));
}
}

View file

@ -2,12 +2,13 @@ use serde::{
de::{self, MapAccess, Visitor},
Deserialize, Deserializer,
};
use std::collections::BTreeMap;
use torn_api_macros::ApiCategory;
use crate::de_util;
pub use crate::common::{LastAction, Status};
pub use crate::common::{Attack, AttackFull, LastAction, Status};
#[derive(Debug, Clone, Copy, ApiCategory)]
#[api(category = "user")]
@ -22,6 +23,10 @@ pub enum Selection {
PersonalStats,
#[api(type = "CriminalRecord", field = "criminalrecord")]
Crimes,
#[api(type = "BTreeMap<i32, Attack>", field = "attacks")]
AttacksFull,
#[api(type = "BTreeMap<i32, AttackFull>", field = "attacks")]
Attacks,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
@ -168,10 +173,14 @@ pub enum EliminationTeam {
#[derive(Debug, Clone)]
pub enum Competition {
Elimination {
score: i16,
score: i32,
attacks: i16,
team: EliminationTeam,
},
DogTags {
score: i32,
position: Option<i32>,
},
Unknown,
}
@ -187,6 +196,7 @@ where
Team,
Attacks,
TeamName,
Position,
#[serde(other)]
Ignore,
}
@ -194,6 +204,8 @@ where
#[derive(Deserialize)]
enum CompetitionName {
Elimination,
#[serde(rename = "Dog Tags")]
DogTags,
#[serde(other)]
Unknown,
}
@ -229,6 +241,7 @@ where
let mut score = None;
let mut attacks = None;
let mut name = None;
let mut position = None;
while let Some(key) = map.next_key()? {
match key {
@ -241,6 +254,9 @@ where
Field::Attacks => {
attacks = Some(map.next_value()?);
}
Field::Position => {
position = Some(map.next_value()?);
}
Field::Team => {
let team_raw: &str = map.next_value()?;
team = if team_raw.is_empty() {
@ -299,6 +315,12 @@ where
Ok(None)
}
}
CompetitionName::DogTags => {
let score = score.ok_or_else(|| de::Error::missing_field("score"))?;
let position = position.ok_or_else(|| de::Error::missing_field("position"))?;
Ok(Some(Competition::DogTags { score, position }))
}
CompetitionName::Unknown => Ok(Some(Competition::Unknown)),
}
}
@ -416,6 +438,7 @@ mod tests {
Selection::Profile,
Selection::PersonalStats,
Selection::Crimes,
Selection::Attacks,
])
})
.await
@ -426,6 +449,8 @@ mod tests {
response.profile().unwrap();
response.personal_stats().unwrap();
response.crimes().unwrap();
response.attacks().unwrap();
response.attacks_full().unwrap();
}
#[async_test]