use arma_rs::{FromArma, IntoArma}; use forge_shared::GarageValidationError; use serde::{Deserialize, Deserializer, Serialize}; use std::collections::HashMap; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Garage { pub vehicles: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Vehicle { pub plate: String, pub classname: String, pub fuel: f64, pub damage: f64, pub hit_points: HitPoints, } #[derive(Debug, Clone, Serialize)] pub struct HitPoints { pub names: Vec, pub selections: Vec, pub values: Vec, } #[derive(Deserialize)] struct HitPointsWire { #[serde(default)] names: Vec, #[serde(default)] selections: Vec, #[serde(default)] values: Vec, } impl HitPoints { pub fn new() -> Self { Self { names: Vec::new(), selections: Vec::new(), values: Vec::new(), } } fn normalize_legacy_fields(&mut self) { if self.names.is_empty() && !self.selections.is_empty() && self.selections.len() == self.values.len() { self.names = self.selections.clone(); } if self.selections.is_empty() && !self.names.is_empty() && self.names.len() == self.values.len() { self.selections = self.names.clone(); } } pub fn from_json_str(json_str: &str) -> Result { let hit_points: HitPoints = serde_json::from_str(json_str) .map_err(|e| format!("Failed to parse hit_points JSON: {}", e))?; let names_len = hit_points.names.len(); let selections_len = hit_points.selections.len(); let values_len = hit_points.values.len(); if names_len != selections_len || names_len != values_len { return Err(format!( "Hitpoint array length mismatch: names={}, selections={}, values={}", names_len, selections_len, values_len )); } Ok(hit_points) } } impl<'de> Deserialize<'de> for HitPoints { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let wire = HitPointsWire::deserialize(deserializer)?; let mut hit_points = Self { names: wire.names, selections: wire.selections, values: wire.values, }; hit_points.normalize_legacy_fields(); Ok(hit_points) } } impl Default for HitPoints { fn default() -> Self { Self::new() } } impl Vehicle { pub fn new>( plate: S, classname: S, fuel: f64, damage: f64, hit_points: HitPoints, ) -> Result { let vehicle = Self { plate: plate.into(), classname: classname.into(), fuel, damage, hit_points, }; vehicle.validate()?; Ok(vehicle) } pub fn validate(&self) -> Result<(), GarageValidationError> { if self.classname.trim().is_empty() { return Err(GarageValidationError::ClassnameEmpty); } if self.fuel < 0.0 || self.fuel > 1.0 { return Err(GarageValidationError::FuelInvalid); } if self.damage < 0.0 || self.damage > 1.0 { return Err(GarageValidationError::DamageInvalid); } let names_len = self.hit_points.names.len(); let selections_len = self.hit_points.selections.len(); let values_len = self.hit_points.values.len(); if names_len != selections_len || names_len != values_len { return Err(GarageValidationError::HitpointArrayLengthMismatch); } for (i, value) in self.hit_points.values.iter().enumerate() { if *value < 0.0 || *value > 1.0 { return Err(GarageValidationError::HitpointValueInvalid(i)); } } Ok(()) } } impl Garage { pub fn new() -> Result { let garage = Self { vehicles: HashMap::new(), }; garage.validate()?; Ok(garage) } pub fn validate(&self) -> Result<(), GarageValidationError> { for vehicle in self.vehicles.values() { vehicle.validate()?; } Ok(()) } pub fn add_vehicle(&mut self, vehicle: Vehicle) -> Result<(), GarageValidationError> { vehicle.validate()?; self.vehicles.insert(vehicle.plate.clone(), vehicle); Ok(()) } pub fn remove_vehicle(&mut self, plate: &str) -> Option { self.vehicles.remove(plate) } pub fn get_vehicle(&self, plate: &str) -> Option<&Vehicle> { self.vehicles.get(plate) } pub fn get_vehicle_mut(&mut self, plate: &str) -> Option<&mut Vehicle> { self.vehicles.get_mut(plate) } } impl FromArma for Vehicle { fn from_arma(s: String) -> Result { serde_json::from_str(&s) .map_err(|e| arma_rs::FromArmaError::InvalidPrimitive(format!("Invalid JSON: {}", e))) } } impl IntoArma for Vehicle { fn to_arma(&self) -> arma_rs::Value { let json_str = serde_json::to_string(self).unwrap_or_default(); arma_rs::Value::String(json_str) } } #[cfg(test)] mod tests { use super::HitPoints; #[test] fn deserializes_legacy_hit_points_missing_names() { let hit_points = HitPoints::from_json_str(r#"{"selections":["engine_hitpoint"],"values":[0.35]}"#) .expect("legacy hit points should deserialize"); assert_eq!(hit_points.names, vec!["engine_hitpoint"]); assert_eq!(hit_points.selections, vec!["engine_hitpoint"]); assert_eq!(hit_points.values, vec![0.35]); } #[test] fn deserializes_empty_legacy_hit_points_object() { let hit_points = HitPoints::from_json_str(r#"{}"#).expect("empty legacy hit points should deserialize"); assert!(hit_points.names.is_empty()); assert!(hit_points.selections.is_empty()); assert!(hit_points.values.is_empty()); } #[test] fn rejects_unbalanced_legacy_hit_points() { let error = HitPoints::from_json_str(r#"{"selections":["engine_hitpoint"],"values":[0.35,0.5]}"#) .expect_err("unbalanced hit points should be rejected"); assert!(error.contains("Hitpoint array length mismatch")); } }