use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum EngineStatus {
Available,
Planned,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ImportFormat {
pub label: &'static str,
pub extensions: &'static [&'static str],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EngineDescriptor {
pub key: &'static str,
pub label: &'static str,
pub pill: &'static str,
pub accent: &'static str,
pub summary: &'static str,
pub status: EngineStatus,
pub import: &'static [ImportFormat],
}
impl EngineDescriptor {
pub fn is_available(&self) -> bool {
matches!(self.status, EngineStatus::Available)
}
}
pub const ENGINES: &[EngineDescriptor] = &[
EngineDescriptor {
key: "wds",
label: "Water Distribution",
pill: "WD",
accent: "#4a90d9",
summary: "Pressurized water distribution network simulation — hydraulics, \
water quality, and energy on the EPANET data model.",
status: EngineStatus::Available,
import: &[ImportFormat {
label: "EPANET input file",
extensions: &["inp"],
}],
},
EngineDescriptor {
key: "uds",
label: "Urban Drainage",
pill: "UD",
accent: "#7a6ff0",
summary: "Stormwater and wastewater collection network simulation — \
runoff, routing, and water quality on the SWMM data model.",
status: EngineStatus::Planned,
import: &[ImportFormat {
label: "SWMM input file",
extensions: &["inp"],
}],
},
EngineDescriptor {
key: "och",
label: "Open Channel",
pill: "OC",
accent: "#3daf75",
summary: "River and open-channel hydraulics — steady and unsteady flow \
on the HEC-RAS data model.",
status: EngineStatus::Planned,
import: &[ImportFormat {
label: "HEC-RAS project archive",
extensions: &["zip", "7z", "tar", "gz", "tgz"],
}],
},
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnknownEngineError {
pub key: String,
}
impl std::fmt::Display for UnknownEngineError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "unknown engine key: {:?}", self.key)
}
}
impl std::error::Error for UnknownEngineError {}
pub fn engine_by_key(key: &str) -> Result<&'static EngineDescriptor, UnknownEngineError> {
ENGINES
.iter()
.find(|e| e.key == key)
.ok_or_else(|| UnknownEngineError { key: key.into() })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn registry_contains_wds_first() {
assert_eq!(ENGINES[0].key, "wds");
assert_eq!(ENGINES[0].label, "Water Distribution");
assert_eq!(ENGINES[0].pill, "WD");
}
#[test]
fn registry_lists_the_three_domain_engines_in_order() {
let keys: Vec<_> = ENGINES.iter().map(|e| e.key).collect();
assert_eq!(keys, ["wds", "uds", "och"]);
}
#[test]
fn wds_is_the_only_available_engine() {
let available: Vec<_> = ENGINES
.iter()
.filter(|e| e.is_available())
.map(|e| e.key)
.collect();
assert_eq!(available, ["wds"]);
assert_eq!(engine_by_key("uds").unwrap().status, EngineStatus::Planned);
assert_eq!(engine_by_key("och").unwrap().status, EngineStatus::Planned);
}
#[test]
fn every_engine_declares_a_usable_import_filter() {
for e in ENGINES {
assert!(
!e.import.is_empty(),
"engine {:?} declares no import format",
e.key
);
for fmt in e.import {
assert!(!fmt.label.is_empty());
assert!(
!fmt.extensions.is_empty(),
"format {:?} lists no extensions",
fmt.label
);
for ext in fmt.extensions {
assert!(
!ext.is_empty()
&& ext
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()),
"extension {ext:?} must be lowercase ASCII with no leading dot",
);
}
}
}
}
#[test]
fn the_inp_extension_is_shared_and_therefore_never_a_validity_test() {
let claimants: Vec<_> = ENGINES
.iter()
.filter(|e| e.import.iter().any(|f| f.extensions.contains(&"inp")))
.map(|e| e.key)
.collect();
assert_eq!(claimants, ["wds", "uds"]);
}
#[test]
fn descriptor_field_invariants_hold_for_every_engine() {
for e in ENGINES {
assert!(
e.key.chars().all(|c| c.is_ascii_lowercase()),
"key {:?} must be lowercase ASCII",
e.key
);
assert_eq!(
e.pill.chars().count(),
2,
"pill {:?} must be 2 chars",
e.pill
);
assert!(e.pill.chars().all(|c| c.is_ascii_uppercase()));
assert!(
e.accent.len() == 7 && e.accent.starts_with('#'),
"accent {:?} must be #rrggbb",
e.accent
);
assert!(!e.summary.is_empty());
}
}
#[test]
fn keys_are_unique() {
let mut keys: Vec<_> = ENGINES.iter().map(|e| e.key).collect();
keys.sort_unstable();
keys.dedup();
assert_eq!(keys.len(), ENGINES.len());
}
#[test]
fn lookup_resolves_and_rejects() {
assert_eq!(engine_by_key("wds").unwrap().pill, "WD");
let err = engine_by_key("nope").unwrap_err();
assert_eq!(err.key, "nope");
assert!(err.to_string().contains("nope"));
}
#[test]
fn a_planned_engine_resolves_rather_than_erroring() {
assert!(engine_by_key("uds").is_ok());
assert!(!engine_by_key("uds").unwrap().is_available());
}
}