added faction->chain, changed selections() signature

This commit is contained in:
TotallyNot 2023-11-17 11:34:38 +01:00
parent c367c24606
commit 3df2d8e882
9 changed files with 323 additions and 45 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "torn-api"
version = "0.5.28"
version = "0.6.0"
edition = "2021"
authors = ["Pyrit [2111649]"]
license = "MIT"
@ -37,7 +37,7 @@ reqwest = { version = "0.11", default-features = false, features = [ "json" ], o
awc = { version = "3", default-features = false, optional = true }
rust_decimal = { version = "1", default-features = false, optional = true, features = [ "serde" ] }
torn-api-macros = { path = "../torn-api-macros", version = "0.1.2" }
torn-api-macros = { path = "../torn-api-macros", version = "0.2" }
[dev-dependencies]
actix-rt = { version = "2.7.0" }

View file

@ -1,7 +1,10 @@
use std::collections::{BTreeMap, HashMap};
use chrono::{DateTime, Utc};
use serde::Deserialize;
use chrono::{DateTime, TimeZone, Utc};
use serde::{
de::{Error, Unexpected, Visitor},
Deserialize, Deserializer,
};
use torn_api_macros::{ApiCategory, IntoOwned};
@ -28,6 +31,9 @@ pub enum FactionSelection {
with = "null_is_empty_dict"
)]
Territory,
#[api(type = "Option<Chain>", field = "chain", with = "deserialize_chain")]
Chain,
}
pub type Selection = FactionSelection;
@ -80,6 +86,128 @@ pub struct Basic<'a> {
pub territory_wars: Vec<FactionTerritoryWar<'a>>,
}
#[derive(Debug)]
pub struct Chain {
pub current: i32,
pub max: i32,
#[cfg(feature = "decimal")]
pub modifier: rust_decimal::Decimal,
pub timeout: Option<i32>,
pub cooldown: Option<i32>,
pub start: DateTime<Utc>,
pub end: DateTime<Utc>,
}
fn deserialize_chain<'de, D>(deserializer: D) -> Result<Option<Chain>, D::Error>
where
D: Deserializer<'de>,
{
struct ChainVisitor;
impl<'de> Visitor<'de> for ChainVisitor {
type Value = Option<Chain>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("struct Chain")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
#[derive(Deserialize)]
#[serde(rename_all = "snake_case")]
enum Fields {
Current,
Max,
Modifier,
Timeout,
Cooldown,
Start,
End,
#[serde(other)]
Ignore,
}
let mut current = None;
let mut max = None;
#[cfg(feature = "decimal")]
let mut modifier = None;
let mut timeout = None;
let mut cooldown = None;
let mut start = None;
let mut end = None;
while let Some(key) = map.next_key()? {
match key {
Fields::Current => {
let value = map.next_value()?;
if value != 0 {
current = Some(value);
}
}
Fields::Max => {
max = Some(map.next_value()?);
}
Fields::Modifier => {
#[cfg(feature = "decimal")]
{
modifier = Some(map.next_value()?);
}
}
Fields::Timeout => {
match map.next_value()? {
0 => timeout = Some(None),
val => timeout = Some(Some(val)),
};
}
Fields::Cooldown => {
match map.next_value()? {
0 => cooldown = Some(None),
val => cooldown = Some(Some(val)),
};
}
Fields::Start => {
let ts: i64 = map.next_value()?;
start = Some(Utc.timestamp_opt(ts, 0).single().ok_or_else(|| {
A::Error::invalid_value(Unexpected::Signed(ts), &"Epoch timestamp")
})?);
}
Fields::End => {
let ts: i64 = map.next_value()?;
end = Some(Utc.timestamp_opt(ts, 0).single().ok_or_else(|| {
A::Error::invalid_value(Unexpected::Signed(ts), &"Epoch timestamp")
})?);
}
Fields::Ignore => (),
}
}
let Some(current) = current else {
return Ok(None);
};
let max = max.ok_or_else(|| A::Error::missing_field("max"))?;
let timeout = timeout.ok_or_else(|| A::Error::missing_field("timeout"))?;
let cooldown = cooldown.ok_or_else(|| A::Error::missing_field("cooldown"))?;
let start = start.ok_or_else(|| A::Error::missing_field("start"))?;
let end = end.ok_or_else(|| A::Error::missing_field("end"))?;
Ok(Some(Chain {
current,
max,
#[cfg(feature = "decimal")]
modifier: modifier.ok_or_else(|| A::Error::missing_field("modifier"))?,
timeout,
cooldown,
start,
end,
}))
}
}
deserializer.deserialize_map(ChainVisitor)
}
#[cfg(test)]
mod tests {
use super::*;
@ -92,7 +220,12 @@ mod tests {
let response = Client::default()
.torn_api(key)
.faction(|b| {
b.selections(&[Selection::Basic, Selection::Attacks, Selection::Territory])
b.selections([
Selection::Basic,
Selection::Attacks,
Selection::Territory,
Selection::Chain,
])
})
.await
.unwrap();
@ -101,6 +234,7 @@ mod tests {
response.attacks().unwrap();
response.attacks_full().unwrap();
response.territory().unwrap();
response.chain().unwrap();
}
#[async_test]
@ -111,13 +245,14 @@ mod tests {
.torn_api(key)
.faction(|b| {
b.id(7049)
.selections(&[Selection::Basic, Selection::Territory])
.selections([Selection::Basic, Selection::Territory, Selection::Chain])
})
.await
.unwrap();
response.basic().unwrap();
response.territory().unwrap();
response.chain().unwrap();
}
#[async_test]
@ -128,12 +263,13 @@ mod tests {
.torn_api(key)
.faction(|b| {
b.id(8981)
.selections(&[Selection::Basic, Selection::Territory])
.selections([Selection::Basic, Selection::Territory, Selection::Chain])
})
.await
.unwrap();
response.basic().unwrap();
response.territory().unwrap();
assert!(response.chain().unwrap().is_none());
}
}

View file

@ -247,7 +247,7 @@ mod tests {
let response = Client::default()
.torn_api(key)
.key(|b| b.selections(&[Selection::Info]))
.key(|b| b.selections([Selection::Info]))
.await
.unwrap();

View file

@ -45,7 +45,16 @@ pub enum ResponseError {
Api { code: u8, reason: String },
#[error(transparent)]
Parsing(#[from] serde_json::Error),
MalformedResponse(#[from] serde_json::Error),
}
impl ResponseError {
pub fn api_code(&self) -> Option<u8> {
match self {
Self::Api { code, .. } => Some(*code),
_ => None,
}
}
}
impl ApiResponse {
@ -100,7 +109,7 @@ impl ApiResponse {
}
pub trait ApiSelection: Send + Sync {
fn raw_value(&self) -> &'static str;
fn raw_value(self) -> &'static str;
fn category() -> &'static str;
}
@ -137,6 +146,18 @@ where
Response(#[from] ResponseError),
}
impl<C> ApiClientError<C>
where
C: std::error::Error,
{
pub fn api_code(&self) -> Option<u8> {
match self {
Self::Response(err) => err.api_code(),
_ => None,
}
}
}
#[derive(Debug)]
pub struct ApiRequest<A>
where
@ -218,10 +239,13 @@ where
A: ApiSelection,
{
#[must_use]
pub fn selections(mut self, selections: &[A]) -> Self {
self.request
.selections
.append(&mut selections.iter().map(ApiSelection::raw_value).collect());
pub fn selections(mut self, selections: impl IntoIterator<Item = A>) -> Self {
self.request.selections.append(
&mut selections
.into_iter()
.map(ApiSelection::raw_value)
.collect(),
);
self
}

View file

@ -222,7 +222,7 @@ mod tests {
let response = Client::default()
.torn_api(key)
.torn(|b| {
b.selections(&[
b.selections([
TornSelection::Competition,
TornSelection::TerritoryWars,
TornSelection::Rackets,
@ -242,7 +242,7 @@ mod tests {
let response = Client::default()
.torn_api(key)
.torn(|b| b.selections(&[Selection::Territory]).id("NSC"))
.torn(|b| b.selections([Selection::Territory]).id("NSC"))
.await
.unwrap();
@ -256,7 +256,7 @@ mod tests {
let response = Client::default()
.torn_api(key)
.torn(|b| b.selections(&[Selection::Territory]).id("AAA"))
.torn(|b| b.selections([Selection::Territory]).id("AAA"))
.await
.unwrap();
@ -269,7 +269,7 @@ mod tests {
let response = Client::default()
.torn_api(&key)
.torn(|b| b.selections(&[Selection::TerritoryWarReport]).id(37403))
.torn(|b| b.selections([Selection::TerritoryWarReport]).id(37403))
.await
.unwrap();
@ -280,7 +280,7 @@ mod tests {
let response = Client::default()
.torn_api(&key)
.torn(|b| b.selections(&[Selection::TerritoryWarReport]).id(37502))
.torn(|b| b.selections([Selection::TerritoryWarReport]).id(37502))
.await
.unwrap();
@ -291,7 +291,7 @@ mod tests {
let response = Client::default()
.torn_api(&key)
.torn(|b| b.selections(&[Selection::TerritoryWarReport]).id(37860))
.torn(|b| b.selections([Selection::TerritoryWarReport]).id(37860))
.await
.unwrap();
@ -302,7 +302,7 @@ mod tests {
let response = Client::default()
.torn_api(&key)
.torn(|b| b.selections(&[Selection::TerritoryWarReport]).id(23757))
.torn(|b| b.selections([Selection::TerritoryWarReport]).id(23757))
.await
.unwrap();

View file

@ -145,8 +145,11 @@ pub struct Basic<'a> {
#[derive(Debug, Clone, IntoOwned, PartialEq, Eq, Deserialize)]
#[into_owned(identity)]
pub struct Discord {
#[serde(rename = "userID")]
pub user_id: i32,
#[serde(
rename = "userID",
deserialize_with = "de_util::empty_string_int_option"
)]
pub user_id: Option<i32>,
#[serde(rename = "discordID", deserialize_with = "de_util::string_is_long")]
pub discord_id: Option<i64>,
}
@ -342,6 +345,7 @@ pub struct Profile<'a> {
pub last_action: LastAction,
#[serde(deserialize_with = "deserialize_faction")]
pub faction: Option<Faction<'a>>,
pub job: EmploymentStatus,
pub status: Status<'a>,
#[serde(deserialize_with = "deserialize_comp")]
@ -484,6 +488,104 @@ impl<'de> Deserialize<'de> for Icon {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[non_exhaustive]
pub enum Job {
Director,
Employee,
Education,
Army,
Law,
Casino,
Medical,
Grocer,
#[serde(other)]
Other,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Company {
PlayerRun {
name: String,
id: i32,
company_type: u8,
},
CityJob,
}
impl<'de> Deserialize<'de> for Company {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct CompanyVisitor;
impl<'de> Visitor<'de> for CompanyVisitor {
type Value = Company;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("enum Company")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
#[allow(clippy::enum_variant_names)]
#[derive(Deserialize)]
#[serde(rename_all = "snake_case")]
enum Field {
CompanyId,
CompanyName,
CompanyType,
#[serde(other)]
Other,
}
let mut id = None;
let mut name = None;
let mut company_type = None;
while let Some(key) = map.next_key()? {
match key {
Field::CompanyId => {
id = Some(map.next_value()?);
if id == Some(0) {
return Ok(Company::CityJob);
}
}
Field::CompanyType => company_type = Some(map.next_value()?),
Field::CompanyName => {
name = Some(map.next_value()?);
}
Field::Other => (),
}
}
let id = id.ok_or_else(|| de::Error::missing_field("company_id"))?;
let name = name.ok_or_else(|| de::Error::missing_field("company_name"))?;
let company_type =
company_type.ok_or_else(|| de::Error::missing_field("company_type"))?;
Ok(Company::PlayerRun {
name,
id,
company_type,
})
}
}
deserializer.deserialize_map(CompanyVisitor)
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct EmploymentStatus {
pub job: Job,
#[serde(flatten)]
pub company: Company,
}
#[cfg(test)]
mod tests {
use super::*;
@ -496,7 +598,7 @@ mod tests {
let response = Client::default()
.torn_api(key)
.user(|b| {
b.selections(&[
b.selections([
Selection::Basic,
Selection::Discord,
Selection::Profile,
@ -523,7 +625,7 @@ mod tests {
let response = Client::default()
.torn_api(key)
.user(|b| b.id(28).selections(&[Selection::Profile]))
.user(|b| b.id(28).selections([Selection::Profile]))
.await
.unwrap();
@ -539,7 +641,7 @@ mod tests {
let response = Client::default()
.torn_api(key)
.users([1, 2111649, 374272176892674048i64], |b| {
b.selections(&[Selection::Basic])
b.selections([Selection::Basic])
})
.await;
@ -553,7 +655,7 @@ mod tests {
let response = Client::default()
.torn_api(key)
.user(|b| b.id(374272176892674048i64).selections(&[Selection::Basic]))
.user(|b| b.id(374272176892674048i64).selections([Selection::Basic]))
.await
.unwrap();
@ -566,7 +668,7 @@ mod tests {
let response = Client::default()
.torn_api(key)
.user(|b| b.id(1900654).selections(&[Selection::Icons]))
.user(|b| b.id(1900654).selections([Selection::Icons]))
.await
.unwrap();