From 4bb4c8ff5e35eebc4dea9e5e44cfd15b21917c73 Mon Sep 17 00:00:00 2001 From: Jacob Schmidt Date: Mon, 20 Apr 2026 17:09:32 -0500 Subject: [PATCH] feat(garage): enhance hit points structure with legacy field normalization and deserialization tests --- arma/server/extension/src/schema/garage.surql | 3 + lib/models/src/garage.rs | 81 ++++++++++++++++++- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/arma/server/extension/src/schema/garage.surql b/arma/server/extension/src/schema/garage.surql index 9a679e7..374db84 100644 --- a/arma/server/extension/src/schema/garage.surql +++ b/arma/server/extension/src/schema/garage.surql @@ -10,6 +10,9 @@ DEFINE FIELD IF NOT EXISTS classname ON garage_vehicle TYPE string; DEFINE FIELD IF NOT EXISTS fuel ON garage_vehicle TYPE number; DEFINE FIELD IF NOT EXISTS damage ON garage_vehicle TYPE number; DEFINE FIELD IF NOT EXISTS hit_points ON garage_vehicle TYPE object; +DEFINE FIELD IF NOT EXISTS hit_points.names ON garage_vehicle TYPE array; +DEFINE FIELD IF NOT EXISTS hit_points.selections ON garage_vehicle TYPE array; +DEFINE FIELD IF NOT EXISTS hit_points.values ON garage_vehicle TYPE array; DEFINE FIELD OVERWRITE updated_at ON garage_vehicle TYPE option; DEFINE INDEX IF NOT EXISTS garage_vehicle_owner ON garage_vehicle COLUMNS uid; DEFINE INDEX IF NOT EXISTS garage_vehicle_unique ON garage_vehicle COLUMNS uid, plate UNIQUE; diff --git a/lib/models/src/garage.rs b/lib/models/src/garage.rs index 578fe79..aa78101 100644 --- a/lib/models/src/garage.rs +++ b/lib/models/src/garage.rs @@ -1,6 +1,6 @@ use arma_rs::{FromArma, IntoArma}; use forge_shared::GarageValidationError; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use std::collections::HashMap; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -17,13 +17,23 @@ pub struct Vehicle { pub hit_points: HitPoints, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[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 { @@ -33,6 +43,22 @@ impl HitPoints { } } + 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))?; @@ -52,6 +78,22 @@ impl HitPoints { } } +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() @@ -159,3 +201,38 @@ impl IntoArma for Vehicle { 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")); + } +}