1use serde::Serialize;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
14#[serde(rename_all = "camelCase")]
15pub enum EngineStatus {
16 Available,
18 Planned,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
29#[serde(rename_all = "camelCase")]
30pub struct ImportFormat {
31 pub label: &'static str,
33 pub extensions: &'static [&'static str],
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
45#[serde(rename_all = "camelCase")]
46pub struct EngineDescriptor {
47 pub key: &'static str,
50 pub label: &'static str,
52 pub pill: &'static str,
54 pub accent: &'static str,
56 pub summary: &'static str,
58 pub status: EngineStatus,
60 pub import: &'static [ImportFormat],
62}
63
64impl EngineDescriptor {
65 pub fn is_available(&self) -> bool {
67 matches!(self.status, EngineStatus::Available)
68 }
69}
70
71pub const ENGINES: &[EngineDescriptor] = &[
75 EngineDescriptor {
76 key: "wds",
77 label: "Water Distribution",
78 pill: "WD",
79 accent: "#4a90d9",
80 summary: "Pressurized water distribution network simulation — hydraulics, \
81 water quality, and energy on the EPANET data model.",
82 status: EngineStatus::Available,
83 import: &[ImportFormat {
84 label: "EPANET input file",
85 extensions: &["inp"],
86 }],
87 },
88 EngineDescriptor {
89 key: "uds",
90 label: "Urban Drainage",
91 pill: "UD",
92 accent: "#7a6ff0",
93 summary: "Stormwater and wastewater collection network simulation — \
94 runoff, routing, and water quality on the SWMM data model.",
95 status: EngineStatus::Planned,
96 import: &[ImportFormat {
97 label: "SWMM input file",
98 extensions: &["inp"],
99 }],
100 },
101 EngineDescriptor {
102 key: "och",
103 label: "Open Channel",
104 pill: "OC",
105 accent: "#3daf75",
106 summary: "River and open-channel hydraulics — steady and unsteady flow \
107 on the HEC-RAS data model.",
108 status: EngineStatus::Planned,
109 import: &[ImportFormat {
110 label: "HEC-RAS project archive",
111 extensions: &["zip", "7z", "tar", "gz", "tgz"],
112 }],
113 },
114];
115
116#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct UnknownEngineError {
123 pub key: String,
125}
126
127impl std::fmt::Display for UnknownEngineError {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 write!(f, "unknown engine key: {:?}", self.key)
130 }
131}
132
133impl std::error::Error for UnknownEngineError {}
134
135pub fn engine_by_key(key: &str) -> Result<&'static EngineDescriptor, UnknownEngineError> {
137 ENGINES
138 .iter()
139 .find(|e| e.key == key)
140 .ok_or_else(|| UnknownEngineError { key: key.into() })
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn registry_contains_wds_first() {
149 assert_eq!(ENGINES[0].key, "wds");
150 assert_eq!(ENGINES[0].label, "Water Distribution");
151 assert_eq!(ENGINES[0].pill, "WD");
152 }
153
154 #[test]
155 fn registry_lists_the_three_domain_engines_in_order() {
156 let keys: Vec<_> = ENGINES.iter().map(|e| e.key).collect();
157 assert_eq!(keys, ["wds", "uds", "och"]);
158 }
159
160 #[test]
161 fn wds_is_the_only_available_engine() {
162 let available: Vec<_> = ENGINES
167 .iter()
168 .filter(|e| e.is_available())
169 .map(|e| e.key)
170 .collect();
171 assert_eq!(available, ["wds"]);
172 assert_eq!(engine_by_key("uds").unwrap().status, EngineStatus::Planned);
173 assert_eq!(engine_by_key("och").unwrap().status, EngineStatus::Planned);
174 }
175
176 #[test]
177 fn every_engine_declares_a_usable_import_filter() {
178 for e in ENGINES {
179 assert!(
180 !e.import.is_empty(),
181 "engine {:?} declares no import format",
182 e.key
183 );
184 for fmt in e.import {
185 assert!(!fmt.label.is_empty());
186 assert!(
187 !fmt.extensions.is_empty(),
188 "format {:?} lists no extensions",
189 fmt.label
190 );
191 for ext in fmt.extensions {
192 assert!(
195 !ext.is_empty()
196 && ext
197 .chars()
198 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()),
199 "extension {ext:?} must be lowercase ASCII with no leading dot",
200 );
201 }
202 }
203 }
204 }
205
206 #[test]
207 fn the_inp_extension_is_shared_and_therefore_never_a_validity_test() {
208 let claimants: Vec<_> = ENGINES
213 .iter()
214 .filter(|e| e.import.iter().any(|f| f.extensions.contains(&"inp")))
215 .map(|e| e.key)
216 .collect();
217 assert_eq!(claimants, ["wds", "uds"]);
218 }
219
220 #[test]
221 fn descriptor_field_invariants_hold_for_every_engine() {
222 for e in ENGINES {
223 assert!(
224 e.key.chars().all(|c| c.is_ascii_lowercase()),
225 "key {:?} must be lowercase ASCII",
226 e.key
227 );
228 assert_eq!(
229 e.pill.chars().count(),
230 2,
231 "pill {:?} must be 2 chars",
232 e.pill
233 );
234 assert!(e.pill.chars().all(|c| c.is_ascii_uppercase()));
235 assert!(
236 e.accent.len() == 7 && e.accent.starts_with('#'),
237 "accent {:?} must be #rrggbb",
238 e.accent
239 );
240 assert!(!e.summary.is_empty());
241 }
242 }
243
244 #[test]
245 fn keys_are_unique() {
246 let mut keys: Vec<_> = ENGINES.iter().map(|e| e.key).collect();
247 keys.sort_unstable();
248 keys.dedup();
249 assert_eq!(keys.len(), ENGINES.len());
250 }
251
252 #[test]
253 fn lookup_resolves_and_rejects() {
254 assert_eq!(engine_by_key("wds").unwrap().pill, "WD");
255 let err = engine_by_key("nope").unwrap_err();
256 assert_eq!(err.key, "nope");
257 assert!(err.to_string().contains("nope"));
258 }
259
260 #[test]
261 fn a_planned_engine_resolves_rather_than_erroring() {
262 assert!(engine_by_key("uds").is_ok());
266 assert!(!engine_by_key("uds").unwrap().is_available());
267 }
268}