use std::collections::HashMap;
use crate::types::Sparte;
#[derive(Debug, Default, Clone)]
pub struct PidRouter {
table: HashMap<u32, Box<str>>,
commodity_table: HashMap<(u32, Sparte), Box<str>>,
registered_by: HashMap<u32, Box<str>>,
}
impl PidRouter {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, pid: u32, workflow_name: impl Into<Box<str>>) {
let wf = workflow_name.into();
self.table.insert(pid, wf);
}
pub fn register_with_module(
&mut self,
pid: u32,
workflow_name: impl Into<Box<str>>,
module: &str,
) {
let wf = workflow_name.into();
if let Some(existing_wf) = self.table.get(&pid)
&& *existing_wf != wf
{
let existing_mod = self
.registered_by
.get(&pid)
.map_or("<unknown>", Box::as_ref);
panic!(
"PID {pid} routing conflict:\n \
module '{module}' tried to register PID {pid} → '{wf}'\n \
but it was already registered → '{existing_wf}' by module '{existing_mod}'\n \
Hint: use DeploymentRoles to prevent conflicting modules from \
both registering shared PIDs (e.g. 19001/19002 are claimed by \
gpke-konfiguration for NB role and wim-geraeteubernahme for nMSB role).\n \
Set EngineBuilder::with_deployment_roles(DeploymentRoles::nb()) to keep \
only the NB-role registration."
);
}
self.table.insert(pid, wf);
self.registered_by.insert(pid, module.into());
}
pub fn register_with_sparte(
&mut self,
pid: u32,
sparte: Sparte,
workflow_name: impl Into<Box<str>>,
) {
self.commodity_table
.insert((pid, sparte), workflow_name.into());
}
#[must_use]
pub fn route_with_sparte(&self, pid: u32, sparte: Sparte) -> Option<&str> {
self.commodity_table
.get(&(pid, sparte))
.or_else(|| self.table.get(&pid))
.map(Box::as_ref)
}
#[must_use]
pub fn route(&self, pid: u32) -> Option<&str> {
self.table.get(&pid).map(Box::as_ref)
}
pub fn registered_pids(&self) -> impl Iterator<Item = u32> + '_ {
self.table.keys().copied()
}
#[must_use]
pub fn len(&self) -> usize {
self.table.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.table.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn route_registered_pid() {
let mut r = PidRouter::new();
r.register(55001, "GpkeSupplierChange");
assert_eq!(r.route(55001), Some("GpkeSupplierChange"));
}
#[test]
fn route_unregistered_pid_returns_none() {
let r = PidRouter::new();
assert_eq!(r.route(55001), None);
}
#[test]
fn register_overwrites_previous_mapping() {
let mut r = PidRouter::new();
r.register(55001, "OldWorkflow");
r.register(55001, "NewWorkflow");
assert_eq!(r.route(55001), Some("NewWorkflow"));
assert_eq!(r.len(), 1);
}
#[test]
fn registered_pids_covers_all_entries() {
let mut r = PidRouter::new();
r.register(55001, "A");
r.register(55002, "B");
r.register(11001, "C");
let mut pids: Vec<u32> = r.registered_pids().collect();
pids.sort_unstable();
assert_eq!(pids, [11001, 55001, 55002]);
}
#[test]
fn multiple_pids_same_workflow() {
let mut r = PidRouter::new();
r.register(55001, "GpkeSupplierChange");
r.register(55002, "GpkeSupplierChange");
r.register(55003, "GpkeSupplierChange");
assert_eq!(r.len(), 3);
for pid in [55001, 55002, 55003] {
assert_eq!(r.route(pid), Some("GpkeSupplierChange"));
}
}
#[test]
fn is_empty_and_len() {
let mut r = PidRouter::new();
assert!(r.is_empty());
r.register(55001, "W");
assert!(!r.is_empty());
assert_eq!(r.len(), 1);
}
#[test]
fn route_with_sparte_prefers_commodity_entry() {
use crate::types::Sparte;
let mut r = PidRouter::new();
r.register(23001, "wim-insrpt"); r.register_with_sparte(23001, Sparte::Strom, "wim-insrpt");
r.register_with_sparte(23001, Sparte::Gas, "wim-gas-insrpt");
assert_eq!(
r.route_with_sparte(23001, Sparte::Strom),
Some("wim-insrpt")
);
assert_eq!(
r.route_with_sparte(23001, Sparte::Gas),
Some("wim-gas-insrpt")
);
assert_eq!(r.route(23001), Some("wim-insrpt"));
}
#[test]
fn route_with_sparte_falls_back_to_unambiguous() {
use crate::types::Sparte;
let mut r = PidRouter::new();
r.register(55001, "GpkeSupplierChange");
assert_eq!(
r.route_with_sparte(55001, Sparte::Strom),
Some("GpkeSupplierChange")
);
assert_eq!(
r.route_with_sparte(55001, Sparte::Gas),
Some("GpkeSupplierChange")
);
}
#[test]
fn route_with_sparte_returns_none_for_unregistered() {
use crate::types::Sparte;
let r = PidRouter::new();
assert_eq!(r.route_with_sparte(23001, Sparte::Strom), None);
assert_eq!(r.route_with_sparte(23001, Sparte::Gas), None);
}
#[test]
fn route_with_sparte_gas_only_deployment() {
use crate::types::Sparte;
let mut r = PidRouter::new();
r.register(23001, "wim-gas-insrpt"); r.register_with_sparte(23001, Sparte::Gas, "wim-gas-insrpt");
assert_eq!(
r.route_with_sparte(23001, Sparte::Gas),
Some("wim-gas-insrpt")
);
assert_eq!(
r.route_with_sparte(23001, Sparte::Strom),
Some("wim-gas-insrpt")
);
}
#[test]
fn route_with_sparte_combined_deployment_all_shared_insrpt_pids() {
use crate::types::Sparte;
let mut r = PidRouter::new();
for pid in [23001_u32, 23003, 23004, 23008] {
r.register(pid, "wim-insrpt");
r.register_with_sparte(pid, Sparte::Strom, "wim-insrpt");
}
for pid in [23001_u32, 23003, 23004, 23008] {
r.register_with_sparte(pid, Sparte::Gas, "wim-gas-insrpt");
}
for pid in [23001_u32, 23003, 23004, 23008] {
assert_eq!(
r.route_with_sparte(pid, Sparte::Strom),
Some("wim-insrpt"),
"PID {pid} Strom should route to wim-insrpt"
);
assert_eq!(
r.route_with_sparte(pid, Sparte::Gas),
Some("wim-gas-insrpt"),
"PID {pid} Gas should route to wim-gas-insrpt"
);
}
}
}