pub mod response_types;
use thiserror::Error;
use reqwest::Error as ReqwestError;
use response_types::{MapDataResponse, MapNameResponse, WarDataResponse, WarReportResponse};
use serde::de::DeserializeOwned;
const MAP_NAME: &str = "/worldconquest/maps";
const WAR_DATA: &str = "/worldconquest/war";
#[derive(Error, Debug)]
pub enum FoxholeApiError {
#[error("Error fetching data from the war API")]
FetchError(#[from] ReqwestError),
}
pub enum Shard {
Live1,
Live2,
}
pub struct Client {
web_client: reqwest::Client,
#[allow(dead_code)]
shard: Shard,
}
impl Client {
pub fn new(shard: Shard) -> Self {
let web_client = reqwest::Client::new();
Self { web_client, shard }
}
pub async fn war_data(&self) -> Result<WarDataResponse, FoxholeApiError> {
let war_data: WarDataResponse = self.get_response(WAR_DATA.to_string()).await?;
Ok(war_data)
}
pub async fn map_names(&self) -> Result<MapNameResponse, FoxholeApiError> {
let maps: Vec<String> = self.get_response(MAP_NAME.to_string()).await?;
let map_data = MapNameResponse { maps };
Ok(map_data)
}
pub async fn map_war_report(
&self,
map_name: &str,
) -> Result<WarReportResponse, FoxholeApiError> {
let endpoint_string = format!("/worldconquest/warReport/{}", map_name);
let war_report: WarReportResponse = self.get_response(endpoint_string).await?;
Ok(war_report)
}
pub async fn map_data_static(
&self,
map_name: &str,
) -> Result<MapDataResponse, FoxholeApiError> {
let endpoint_string = format!("/worldconquest/maps/{}/static", map_name);
let map_data: MapDataResponse = self.get_response(endpoint_string).await?;
Ok(map_data)
}
pub async fn map_data_dynamic(
&self,
map_name: &str,
) -> Result<MapDataResponse, FoxholeApiError> {
let endpoint_string = format!("/worldconquest/maps/{}/dynamic/public", map_name);
let map_data: MapDataResponse = self.get_response(endpoint_string).await?;
Ok(map_data)
}
async fn get_response<T>(&self, endpoint: String) -> Result<T, FoxholeApiError>
where
T: DeserializeOwned,
{
let request_string = self.build_request(endpoint);
let response = self.web_client.get(request_string.clone()).send().await?;
let response = response.json::<T>().await?;
Ok(response)
}
fn build_request(&self, endpoint: String) -> String {
#[cfg(not(test))]
let mut request_string = self.get_shard_url();
#[cfg(test)]
let mut request_string = mockito::server_url();
request_string.push_str(endpoint.as_str());
request_string
}
#[cfg(not(test))]
fn get_shard_url(&self) -> String {
let shard = match self.shard {
Shard::Live1 => "live",
Shard::Live2 => "live-2",
};
format!("https://war-service-{}.foxholeservices.com/api", shard)
}
}
impl Default for Client {
fn default() -> Self {
Client::new(Shard::Live1)
}
}
#[cfg(test)]
mod test {
use crate::response_types::{IconType, MapItem, MapMarkerType, MapTextItem, TeamId};
use super::*;
use mockito::{mock, Mock};
fn build_mock(endpoint: &str, body: &'static str) -> Mock {
mock("GET", endpoint)
.with_status(200)
.with_header("content-type", "application/json")
.with_body(body)
.create()
}
#[tokio::test]
async fn test_war_data() {
let war_data_string = r#"{
"warId" : "1e82269a-d82b-4350-b1b1-06a98c983503",
"warNumber" : 83,
"winner" : "NONE",
"conquestStartTime" : 1632326703205,
"conquestEndTime" : null,
"resistanceStartTime" : null,
"requiredVictoryTowns" : 32
}"#;
let expected_response = WarDataResponse {
war_id: "1e82269a-d82b-4350-b1b1-06a98c983503".to_string(),
war_number: 83,
winner: TeamId::None,
conquest_start_time: 1632326703205,
conquest_end_time: None,
resistance_start_time: None,
required_victory_towns: 32,
};
let _m = build_mock(WAR_DATA, war_data_string);
let client = Client::default();
let response = client.war_data().await.unwrap();
assert_eq!(expected_response, response);
}
#[tokio::test]
async fn test_map_name() {
let map_name_string = r#"[
"TheFingersHex",
"GreatMarchHex",
"TempestIslandHex"
]"#;
let _m = build_mock(MAP_NAME, map_name_string);
let maps = vec![
"TheFingersHex".to_string(),
"GreatMarchHex".to_string(),
"TempestIslandHex".to_string(),
];
let expected_response = MapNameResponse { maps };
let client = Client::default();
let response = client.map_names().await.unwrap();
assert_eq!(expected_response, response);
}
#[tokio::test]
async fn test_map_data_static() {
let map_data_string = r#"{
"regionId": 38,
"scorchedVictoryTowns": 0,
"mapItems": [],
"mapTextItems": [
{
"text": "Captain's Dread",
"x": 0.8643478,
"y": 0.4387644,
"mapMarkerType": "Minor"
},
{
"text": "Cavitatis",
"x": 0.43523252,
"y": 0.6119927,
"mapMarkerType": "Major"
}
],
"lastUpdated": 1635388391413,
"version": 3
}"#;
let map_text_items = vec![
MapTextItem {
text: "Captain's Dread".to_string(),
x: 0.8643478,
y: 0.4387644,
map_marker_type: MapMarkerType::Minor,
},
MapTextItem {
text: "Cavitatis".to_string(),
x: 0.43523252,
y: 0.6119927,
map_marker_type: MapMarkerType::Major,
},
];
let expected_response = MapDataResponse {
region_id: 38,
scorched_victory_towns: 0,
map_items: Vec::new(),
map_text_items,
last_updated: 1635388391413,
version: 3,
};
let map_string = "TheFingersHex".to_string();
let endpoint_string = format!("/worldconquest/maps/{}/static", map_string);
let _m = build_mock(endpoint_string.as_str(), map_data_string);
let client = Client::default();
let response = client.map_data_static(&map_string).await.unwrap();
assert_eq!(expected_response, response);
}
#[tokio::test]
async fn test_map_data_dynamic() {
let map_data_string = r#"{
"regionId" : 38,
"scorchedVictoryTowns" : 0,
"mapItems" : [ {
"teamId" : "NONE",
"iconType" : 20,
"x" : 0.43503433,
"y" : 0.83201146,
"flags" : 0
}, {
"teamId" : "COLONIALS",
"iconType" : 20,
"x" : 0.83840775,
"y" : 0.45411408,
"flags" : 0
}, {
"teamId" : "WARDENS",
"iconType" : 20,
"x" : 0.83840775,
"y" : 0.45411408,
"flags" : 0
} ],
"mapTextItems" : [ ],
"lastUpdated" : 1635534670643,
"version" : 5
}"#;
let map_items = vec![
MapItem {
team_id: TeamId::None,
icon_type: IconType::SalvageField,
x: 0.43503433,
y: 0.83201146,
flags: 0,
},
MapItem {
team_id: TeamId::Colonials,
icon_type: IconType::SalvageField,
x: 0.83840775,
y: 0.45411408,
flags: 0,
},
MapItem {
team_id: TeamId::Wardens,
icon_type: IconType::SalvageField,
x: 0.83840775,
y: 0.45411408,
flags: 0,
},
];
let expected_response = MapDataResponse {
region_id: 38,
scorched_victory_towns: 0,
map_items,
map_text_items: Vec::new(),
last_updated: 1635534670643,
version: 5,
};
let map_string = "TheFingersHex".to_string();
let endpoint_string = format!("/worldconquest/maps/{}/dynamic/public", map_string);
let _m = build_mock(endpoint_string.as_str(), map_data_string);
let client = Client::default();
let response = client.map_data_dynamic(&map_string).await.unwrap();
assert_eq!(expected_response, response);
}
#[tokio::test]
async fn test_map_war_report() {
let war_report_string = r#"{
"totalEnlistments" : 1000,
"colonialCasualties" : 2000,
"wardenCasualties" : 2000,
"dayOfWar" : 355
}"#;
let expected_response = WarReportResponse {
total_enlistments: 1000,
colonial_casualties: 2000,
warden_casualties: 2000,
day_of_war: 355,
};
let _m = build_mock("/worldconquest/warReport/TheFingersHex", war_report_string);
let client = Client::default();
let response = client.map_war_report("TheFingersHex").await.unwrap();
assert_eq!(expected_response, response);
}
}