use serde::{Deserialize, Serialize};
use crate::agent::AgentName;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Join {
All,
Any,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Mode {
#[default]
Async,
Spawn,
}
impl Mode {
#[must_use]
pub fn is_spawn(self) -> bool {
matches!(self, Self::Spawn)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Route {
#[serde(deserialize_with = "one_or_many")]
pub from: Vec<AgentName>,
#[serde(deserialize_with = "one_or_many")]
pub to: Vec<AgentName>,
#[serde(default)]
pub mode: Mode,
#[serde(default)]
pub join: Option<Join>,
#[serde(default)]
pub timeout_sec: Option<u64>,
}
impl Route {
#[must_use]
pub fn is_spawn(&self) -> bool {
self.mode.is_spawn()
}
#[must_use]
pub fn is_join(&self) -> bool {
self.join.is_some()
}
}
fn one_or_many<'de, D>(deserializer: D) -> Result<Vec<AgentName>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Raw {
One(AgentName),
Many(Vec<AgentName>),
}
Ok(match Raw::deserialize(deserializer)? {
Raw::One(name) => vec![name],
Raw::Many(names) => names,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn route(body: &str) -> Route {
toml::from_str(body).expect("route parses")
}
#[test]
fn from_and_to_accept_a_string_or_a_list() {
let route = route(
r#"
from = "planner"
to = ["probe_a", "probe_b"]
"#,
);
assert_eq!(route.from, vec![AgentName::from("planner")]);
assert_eq!(route.to.len(), 2);
assert!(!route.is_join());
}
#[test]
fn a_join_is_recognised_with_its_timeout() {
let route = route(
r#"
from = ["probe_a", "probe_b"]
to = "collector"
join = "all"
timeout_sec = 60
"#,
);
assert!(route.is_join());
assert_eq!(route.join, Some(Join::All));
assert_eq!(route.timeout_sec, Some(60));
}
#[test]
fn the_only_supported_mode_is_async() {
let route = route(
r#"
from = "a"
to = "b"
mode = "async"
"#,
);
assert_eq!(route.mode, Mode::Async);
}
#[test]
fn a_spawn_edge_is_recognised() {
let route = route(
r#"
from = "scanner"
to = "reviewer"
mode = "spawn"
"#,
);
assert!(route.is_spawn());
assert_eq!(route.mode, Mode::Spawn);
}
#[test]
fn a_deferred_mode_is_rejected_with_a_clear_error() {
let error = toml::from_str::<Route>(
r#"
from = "a"
to = "b"
mode = "request_response"
"#,
)
.expect_err("request_response was superseded by rendezvous joins");
assert!(error.to_string().contains("request_response"));
}
}