1use serde::{Deserialize, Serialize};
8
9use crate::agent::AgentName;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
13#[serde(rename_all = "lowercase")]
14pub enum Join {
15 All,
17 Any,
19}
20
21#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
27#[serde(rename_all = "snake_case")]
28pub enum Mode {
29 #[default]
31 Async,
32 Spawn,
44}
45
46impl Mode {
47 #[must_use]
49 pub fn is_spawn(self) -> bool {
50 matches!(self, Self::Spawn)
51 }
52}
53
54#[derive(Debug, Clone, Deserialize)]
56#[serde(deny_unknown_fields)]
57pub struct Route {
58 #[serde(deserialize_with = "one_or_many")]
60 pub from: Vec<AgentName>,
61 #[serde(deserialize_with = "one_or_many")]
63 pub to: Vec<AgentName>,
64 #[serde(default)]
66 pub mode: Mode,
67 #[serde(default)]
69 pub join: Option<Join>,
70 #[serde(default)]
72 pub timeout_sec: Option<u64>,
73}
74
75impl Route {
76 #[must_use]
78 pub fn is_spawn(&self) -> bool {
79 self.mode.is_spawn()
80 }
81
82 #[must_use]
84 pub fn is_join(&self) -> bool {
85 self.join.is_some()
86 }
87}
88
89fn one_or_many<'de, D>(deserializer: D) -> Result<Vec<AgentName>, D::Error>
91where
92 D: serde::Deserializer<'de>,
93{
94 #[derive(Deserialize)]
95 #[serde(untagged)]
96 enum Raw {
97 One(AgentName),
98 Many(Vec<AgentName>),
99 }
100
101 Ok(match Raw::deserialize(deserializer)? {
102 Raw::One(name) => vec![name],
103 Raw::Many(names) => names,
104 })
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 fn route(body: &str) -> Route {
112 toml::from_str(body).expect("route parses")
113 }
114
115 #[test]
116 fn from_and_to_accept_a_string_or_a_list() {
117 let route = route(
118 r#"
119 from = "planner"
120 to = ["probe_a", "probe_b"]
121 "#,
122 );
123
124 assert_eq!(route.from, vec![AgentName::from("planner")]);
125 assert_eq!(route.to.len(), 2);
126 assert!(!route.is_join());
127 }
128
129 #[test]
130 fn a_join_is_recognised_with_its_timeout() {
131 let route = route(
132 r#"
133 from = ["probe_a", "probe_b"]
134 to = "collector"
135 join = "all"
136 timeout_sec = 60
137 "#,
138 );
139
140 assert!(route.is_join());
141 assert_eq!(route.join, Some(Join::All));
142 assert_eq!(route.timeout_sec, Some(60));
143 }
144
145 #[test]
146 fn the_only_supported_mode_is_async() {
147 let route = route(
148 r#"
149 from = "a"
150 to = "b"
151 mode = "async"
152 "#,
153 );
154
155 assert_eq!(route.mode, Mode::Async);
156 }
157
158 #[test]
159 fn a_spawn_edge_is_recognised() {
160 let route = route(
161 r#"
162 from = "scanner"
163 to = "reviewer"
164 mode = "spawn"
165 "#,
166 );
167
168 assert!(route.is_spawn());
169 assert_eq!(route.mode, Mode::Spawn);
170 }
171
172 #[test]
173 fn a_deferred_mode_is_rejected_with_a_clear_error() {
174 let error = toml::from_str::<Route>(
175 r#"
176 from = "a"
177 to = "b"
178 mode = "request_response"
179 "#,
180 )
181 .expect_err("request_response was superseded by rendezvous joins");
182
183 assert!(error.to_string().contains("request_response"));
184 }
185}