milsymbol-rs 0.3.2

A Rust wrapper for the milsymbol JavaScript library to generate military symbols (MIL-STD-2525 and APP-6).
use eyre::{Result, WrapErr};
use serde::{Deserialize, Serialize};

#[cfg(feature = "cache")]
use crate::cache::METADATA_CACHE;

/// Represents a part of an SIDC (e.g., a specific identity or echelon).
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SidcPart {
    /// The numeric code for this part.
    pub code: String,
    /// The human-readable label for this part.
    pub label: String,
    /// Sub-parts of this part (e.g., specific types of a broader category).
    #[serde(default)]
    pub children: Vec<SidcPart>,
}

/// Contains entities and modifiers for a specific symbol set.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SidcEntitiesAndModifiers {
    /// Valid entities (icons).
    pub entities: Vec<SidcPart>,
    /// Valid entity modifier 1 (digits 17-18).
    pub modifiers1: Vec<SidcPart>,
    /// Valid entity modifier 2 (digits 19-20).
    pub modifiers2: Vec<SidcPart>,
}

/// Contains all valid parts for constructing an SIDC.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SidcMetadata {
    /// Valid contexts (Reality, Exercise, etc.)
    pub contexts: Vec<SidcPart>,
    /// Valid identities (Friend, Hostile, etc.)
    pub identities: Vec<SidcPart>,
    /// Valid statuses (Present, Planned, etc.)
    pub statuses: Vec<SidcPart>,
    /// Valid Headquarters/Task Force/Dummy modifiers.
    pub hqtffd: Vec<SidcPart>,
    /// Valid echelons.
    pub echelons: Vec<SidcPart>,
    /// Valid mobility modifiers.
    pub mobilities: Vec<SidcPart>,
    /// Valid symbol sets (Air, Land Unit, etc.)
    pub symbol_sets: Vec<SidcPart>,
}

/// Retrieves metadata about SIDC parts from the milsymbol library.
pub fn get_sidc_metadata() -> Result<SidcMetadata> {
    #[cfg(feature = "cache")]
    if let Some(metadata) = METADATA_CACHE.get() {
        return Ok(metadata.clone());
    }

    // This metadata is static; define it directly to avoid JS execution overhead.
    let json_data = r#"{
        "contexts": [
            { "code": "0", "label": "Reality" },
            { "code": "1", "label": "Exercise" },
            { "code": "2", "label": "Simulation" }
        ],
        "identities": [
            { "code": "0", "label": "Pending" },
            { "code": "1", "label": "Unknown" },
            { "code": "2", "label": "Assumed Friend" },
            { "code": "3", "label": "Friend" },
            { "code": "4", "label": "Neutral" },
            { "code": "5", "label": "Suspect/Joker" },
            { "code": "6", "label": "Hostile/Faker" }
        ],
        "statuses": [
            { "code": "0", "label": "Present" },
            { "code": "1", "label": "Planned/Anticipated" },
            { "code": "2", "label": "Present/Fully Capable" },
            { "code": "3", "label": "Present/Damaged" },
            { "code": "4", "label": "Present/Destroyed" },
            { "code": "5", "label": "Present/Full to Capacity" }
        ],
        "hqtffd": [
            { "code": "0", "label": "None" },
            { "code": "1", "label": "Feint/Dummy" },
            { "code": "2", "label": "Headquarters" },
            { "code": "3", "label": "Feint/Dummy Headquarters" },
            { "code": "4", "label": "Task Force" },
            { "code": "5", "label": "Feint/Dummy Task Force" },
            { "code": "6", "label": "Task Force Headquarters" },
            { "code": "7", "label": "Feint/Dummy Task Force Headquarters" }
        ],
        "echelons": [
            { "code": "00", "label": "None" },
            { "code": "11", "label": "Team/Crew" },
            { "code": "12", "label": "Squad" },
            { "code": "13", "label": "Section" },
            { "code": "14", "label": "Platoon/Detachment" },
            { "code": "15", "label": "Company/Battery/Troop" },
            { "code": "16", "label": "Battalion/Squadron" },
            { "code": "17", "label": "Regiment/Group" },
            { "code": "18", "label": "Brigade" },
            { "code": "21", "label": "Division" },
            { "code": "22", "label": "Corps/MEF" },
            { "code": "23", "label": "Army" },
            { "code": "24", "label": "Army Group/Front" },
            { "code": "25", "label": "Region/Theater" },
            { "code": "26", "label": "Command" }
        ],
        "mobilities": [
            { "code": "00", "label": "None" },
            { "code": "31", "label": "Wheeled limited cross country" },
            { "code": "32", "label": "Wheeled cross country" },
            { "code": "33", "label": "Tracked" },
            { "code": "34", "label": "Wheeled and tracked combination" },
            { "code": "35", "label": "Towed" },
            { "code": "36", "label": "Rail" },
            { "code": "37", "label": "Pack animals" },
            { "code": "41", "label": "Over snow (prime mover)" },
            { "code": "42", "label": "Sled" },
            { "code": "51", "label": "Barge" },
            { "code": "52", "label": "Amphibious" },
            { "code": "61", "label": "Short towed array" },
            { "code": "62", "label": "Long towed Array" }
        ],
        "symbol_sets": [
            { "code": "01", "label": "Air" },
            { "code": "02", "label": "Air Missile" },
            { "code": "05", "label": "Space" },
            { "code": "06", "label": "Space Missile" },
            { "code": "10", "label": "Land Unit" },
            { "code": "11", "label": "Land Civilian Unit" },
            { "code": "15", "label": "Land Equipment" },
            { "code": "20", "label": "Land Installation" },
            { "code": "25", "label": "Control Measure" },
            { "code": "27", "label": "Dismounted Individual" },
            { "code": "30", "label": "Sea Surface" },
            { "code": "35", "label": "Sea Subsurface" },
            { "code": "36", "label": "Mine Warfare" },
            { "code": "40", "label": "Activities" },
            { "code": "50", "label": "Signals Intelligence (Airborne)" },
            { "code": "51", "label": "Signals Intelligence (Unmanned Airborne)" },
            { "code": "52", "label": "Signals Intelligence (Ground)" },
            { "code": "53", "label": "Signals Intelligence (Sea Surface)" },
            { "code": "54", "label": "Signals Intelligence (Subsurface)" },
            { "code": "60", "label": "Cyberspace" }
        ]
    }"#;

    let metadata: SidcMetadata =
        serde_json::from_str(json_data).wrap_err("Failed to parse SIDC metadata")?;

    #[cfg(feature = "cache")]
    let _ = METADATA_CACHE.set(metadata.clone());

    Ok(metadata)
}

// These functions have been moved to `Milsymbol` since they require V8 runtime evaluation.

#[cfg(test)]
mod tests {
    use super::*;
    use crate::milsymbol::MilsymbolBuilder;

    #[test]
    fn test_get_sidc_metadata() -> Result<()> {
        let metadata = get_sidc_metadata()?;
        assert!(!metadata.contexts.is_empty());
        assert!(!metadata.identities.is_empty());
        assert!(
            metadata
                .identities
                .iter()
                .any(|p| p.code == "3" && p.label == "Friend")
        );
        Ok(())
    }

    #[test]
    fn test_get_sidc_entities_and_modifiers() -> Result<()> {
        let mut ms = MilsymbolBuilder::new().build().unwrap();
        let data = ms.get_sidc_entities_and_modifiers("10")?; // Land Unit
        assert!(!data.entities.is_empty());
        assert!(!data.modifiers1.is_empty());
        assert!(!data.modifiers2.is_empty());

        fn find_recursive(entities: &[SidcPart], code: &str) -> bool {
            for e in entities {
                if e.code == code {
                    return true;
                }
                if find_recursive(&e.children, code) {
                    return true;
                }
            }
            false
        }

        assert!(find_recursive(&data.entities, "121100")); // Infantry
        Ok(())
    }

    #[test]
    fn test_get_sidc_entities_convenience() -> Result<()> {
        let mut ms = MilsymbolBuilder::new().build().unwrap();
        let entities = ms.get_sidc_entities("10")?;
        assert!(!entities.is_empty());
        Ok(())
    }

    #[test]
    fn test_invalid_symbol_set() -> Result<()> {
        // Milsymbol might return an empty or mostly empty object for invalid symbol sets
        let mut ms = MilsymbolBuilder::new().build().unwrap();
        let data = ms.get_sidc_entities_and_modifiers("99")?;
        assert!(
            data.entities.is_empty(),
            "Invalid symbol set should return no entities"
        );
        Ok(())
    }
}