use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Deserializer, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Fit {
pub name: Option<String>,
pub ship: Ship,
pub items: Vec<FitItem>,
#[serde(default)]
pub character: Character,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Ship {
pub type_id: i32,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct FitItem {
pub type_id: i32,
pub slot: Slot,
#[serde(default = "one")]
pub quantity: u32,
pub state: State,
pub charge: Option<Charge>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fighter_abilities: Option<BTreeSet<i32>>,
}
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[serde(tag = "type", content = "index", rename_all = "snake_case")]
pub enum Slot {
High(u8),
Medium(u8),
Low(u8),
Rig(u8),
Subsystem(u8),
Service(u8),
FighterTube(u8),
FighterBay,
DroneBay,
Cargo,
}
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum State {
Offline,
Online,
Active,
Overload,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Charge {
pub type_id: i32,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct Character {
#[serde(default, deserialize_with = "id_map")]
pub skills: BTreeMap<i32, u8>,
}
fn one() -> u32 {
1
}
fn id_map<'de, D, V>(deserializer: D) -> Result<BTreeMap<i32, V>, D::Error>
where
D: Deserializer<'de>,
V: Deserialize<'de>,
{
BTreeMap::<String, V>::deserialize(deserializer)?
.into_iter()
.map(|(key, value)| {
let key = key.parse().map_err(|_| {
serde::de::Error::custom(format!("expected an identifier, found {key:?}"))
})?;
Ok((key, value))
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn minimal_fit_fills_defaults() {
let fit: Fit = serde_json::from_str(
r#"{
"ship": {"type_id": 587},
"items": [
{"type_id": 2873, "slot": {"type": "high", "index": 0}, "state": "active"},
{"type_id": 2488, "slot": {"type": "drone_bay"}, "quantity": 5, "state": "active"}
],
"character": {"skills": {"3300": 5}}
}"#,
)
.unwrap();
assert_eq!(fit.items[0].slot, Slot::High(0));
assert_eq!(fit.items[0].quantity, 1);
assert_eq!(fit.items[0].fighter_abilities, None);
assert_eq!(fit.items[1].slot, Slot::DroneBay);
assert_eq!(fit.items[1].quantity, 5);
assert_eq!(fit.character.skills[&3300], 5);
}
#[test]
fn reads_fighters() {
let fit: Fit = serde_json::from_str(
r#"{
"ship": {"type_id": 23911},
"items": [
{"type_id": 40556, "slot": {"type": "fighter_tube", "index": 1}, "quantity": 6, "state": "active", "fighter_abilities": [6465, 6431]},
{"type_id": 40556, "slot": {"type": "fighter_bay"}, "quantity": 3, "state": "offline", "fighter_abilities": []}
]
}"#,
)
.unwrap();
assert_eq!(fit.items[0].slot, Slot::FighterTube(1));
assert_eq!(
fit.items[0].fighter_abilities,
Some(BTreeSet::from([6431, 6465]))
);
assert_eq!(fit.items[1].slot, Slot::FighterBay);
assert_eq!(fit.items[1].fighter_abilities, Some(BTreeSet::new()));
}
#[test]
fn round_trips_through_json() {
let fit = Fit {
name: Some("Rifter".to_string()),
ship: Ship { type_id: 587 },
items: vec![FitItem {
type_id: 47408,
slot: Slot::Medium(2),
quantity: 1,
state: State::Overload,
charge: None,
fighter_abilities: None,
}],
character: Character::default(),
};
let json = serde_json::to_string(&fit).unwrap();
let parsed: Fit = serde_json::from_str(&json).unwrap();
assert!(!json.contains("fighter_abilities"));
assert_eq!(parsed.items[0].slot, Slot::Medium(2));
assert_eq!(parsed.items[0].state, State::Overload);
}
}