Skip to main content

hydra_common/
identity.rs

1//! Engine identity: descriptors and the registry (spec §2).
2
3use serde::Serialize;
4
5/// Immutable identity of one Hydra engine (spec §2.1).
6///
7/// `key` and the `label`/`pill` pair are two deliberately separate naming
8/// systems: the key carries the accurate domain umbrella and never changes
9/// once released (it is persisted in project metadata and report
10/// templates); the label carries the familiar practitioner term and may be
11/// revised between releases.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
13#[serde(rename_all = "camelCase")]
14pub struct EngineDescriptor {
15    /// Stable machine identifier: lowercase ASCII domain-umbrella
16    /// abbreviation (`wds`, `uds`, `och`).
17    pub key: &'static str,
18    /// Human-facing product name (e.g. "Water Distribution").
19    pub label: &'static str,
20    /// Two-character uppercase badge (e.g. "WD").
21    pub pill: &'static str,
22    /// Brand color for this engine, `#rrggbb`.
23    pub accent: &'static str,
24    /// One-sentence description of the engine's domain. Plain text.
25    pub summary: &'static str,
26}
27
28/// Every engine compiled into this distribution, in presentation order
29/// (spec §2.2).
30pub const ENGINES: &[EngineDescriptor] = &[EngineDescriptor {
31    key: "wds",
32    label: "Water Distribution",
33    pill: "WD",
34    accent: "#4a90d9",
35    summary: "Pressurized water distribution network simulation — hydraulics, \
36              water quality, and energy on the EPANET data model.",
37}];
38
39/// Lookup failure for [`engine_by_key`].
40///
41/// Applications must treat this as an explicit unsupported state (e.g. a
42/// project created by a newer Hydra carrying an engine this build lacks) —
43/// never as a fallback to a default engine (spec §2.2).
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct UnknownEngineError {
46    /// The key that failed to resolve.
47    pub key: String,
48}
49
50impl std::fmt::Display for UnknownEngineError {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        write!(f, "unknown engine key: {:?}", self.key)
53    }
54}
55
56impl std::error::Error for UnknownEngineError {}
57
58/// Resolve an engine key to its descriptor (spec §2.2).
59pub fn engine_by_key(key: &str) -> Result<&'static EngineDescriptor, UnknownEngineError> {
60    ENGINES
61        .iter()
62        .find(|e| e.key == key)
63        .ok_or_else(|| UnknownEngineError { key: key.into() })
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn registry_contains_wds_first() {
72        assert_eq!(ENGINES[0].key, "wds");
73        assert_eq!(ENGINES[0].label, "Water Distribution");
74        assert_eq!(ENGINES[0].pill, "WD");
75    }
76
77    #[test]
78    fn descriptor_field_invariants_hold_for_every_engine() {
79        for e in ENGINES {
80            assert!(
81                e.key.chars().all(|c| c.is_ascii_lowercase()),
82                "key {:?} must be lowercase ASCII",
83                e.key
84            );
85            assert_eq!(
86                e.pill.chars().count(),
87                2,
88                "pill {:?} must be 2 chars",
89                e.pill
90            );
91            assert!(e.pill.chars().all(|c| c.is_ascii_uppercase()));
92            assert!(
93                e.accent.len() == 7 && e.accent.starts_with('#'),
94                "accent {:?} must be #rrggbb",
95                e.accent
96            );
97            assert!(!e.summary.is_empty());
98        }
99    }
100
101    #[test]
102    fn keys_are_unique() {
103        let mut keys: Vec<_> = ENGINES.iter().map(|e| e.key).collect();
104        keys.sort_unstable();
105        keys.dedup();
106        assert_eq!(keys.len(), ENGINES.len());
107    }
108
109    #[test]
110    fn lookup_resolves_and_rejects() {
111        assert_eq!(engine_by_key("wds").unwrap().pill, "WD");
112        let err = engine_by_key("och").unwrap_err();
113        assert_eq!(err.key, "och");
114        assert!(err.to_string().contains("och"));
115    }
116}