peat_protocol/command/
routing.rs1use peat_schema::command::v1::{command_target::Scope, HierarchicalCommand};
6use std::collections::HashSet;
7
8pub struct CommandRouter {
10 node_id: String,
12
13 squad_id: Option<String>,
15
16 squad_members: Vec<String>,
18
19 platoon_id: Option<String>,
21}
22
23#[derive(Debug, Clone, PartialEq)]
25pub enum TargetResolution {
26 Self_,
28
29 Subordinates(Vec<String>),
31
32 AllSquadMembers(Vec<String>),
34
35 NotApplicable,
37}
38
39impl CommandRouter {
40 pub fn new(
42 node_id: String,
43 squad_id: Option<String>,
44 squad_members: Vec<String>,
45 platoon_id: Option<String>,
46 ) -> Self {
47 Self {
48 node_id,
49 squad_id,
50 squad_members,
51 platoon_id,
52 }
53 }
54
55 pub fn resolve_target(&self, command: &HierarchicalCommand) -> TargetResolution {
57 let target = match &command.target {
58 Some(t) => t,
59 None => return TargetResolution::NotApplicable,
60 };
61
62 let scope = Scope::try_from(target.scope).unwrap_or(Scope::Unspecified);
63
64 match scope {
65 Scope::Individual => {
66 let target_ids: HashSet<String> = target.target_ids.iter().cloned().collect();
68
69 if target_ids.contains(&self.node_id) {
70 TargetResolution::Self_
71 } else {
72 let subordinate_targets: Vec<String> = self
74 .squad_members
75 .iter()
76 .filter(|m| target_ids.contains(*m))
77 .cloned()
78 .collect();
79
80 if !subordinate_targets.is_empty() {
81 TargetResolution::Subordinates(subordinate_targets)
82 } else {
83 TargetResolution::NotApplicable
84 }
85 }
86 }
87
88 Scope::Squad => {
89 if let Some(ref my_squad) = self.squad_id {
91 if target.target_ids.contains(my_squad) {
92 if !self.squad_members.is_empty() {
94 TargetResolution::AllSquadMembers(self.squad_members.clone())
96 } else {
97 TargetResolution::Self_
99 }
100 } else {
101 TargetResolution::NotApplicable
102 }
103 } else {
104 TargetResolution::NotApplicable
105 }
106 }
107
108 Scope::Platoon => {
109 if let Some(ref my_platoon) = self.platoon_id {
111 if target.target_ids.contains(my_platoon) {
112 if !self.squad_members.is_empty() {
114 TargetResolution::AllSquadMembers(self.squad_members.clone())
116 } else {
117 TargetResolution::Self_
119 }
120 } else {
121 TargetResolution::NotApplicable
122 }
123 } else {
124 TargetResolution::NotApplicable
125 }
126 }
127
128 Scope::Broadcast => {
129 if !self.squad_members.is_empty() {
131 TargetResolution::AllSquadMembers(self.squad_members.clone())
133 } else {
134 TargetResolution::Self_
136 }
137 }
138
139 Scope::Unspecified => TargetResolution::NotApplicable,
140 }
141 }
142
143 pub fn should_route(&self, resolution: &TargetResolution) -> bool {
145 matches!(
146 resolution,
147 TargetResolution::Subordinates(_) | TargetResolution::AllSquadMembers(_)
148 )
149 }
150
151 pub fn get_routing_targets(&self, resolution: &TargetResolution) -> Vec<String> {
153 match resolution {
154 TargetResolution::Subordinates(nodes) => nodes.clone(),
155 TargetResolution::AllSquadMembers(nodes) => nodes.clone(),
156 _ => Vec::new(),
157 }
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use peat_schema::command::v1::CommandTarget;
165
166 #[test]
167 fn test_resolve_individual_self() {
168 let router = CommandRouter::new(
169 "node-1".to_string(),
170 Some("squad-alpha".to_string()),
171 vec!["node-1".to_string(), "node-2".to_string()],
172 None,
173 );
174
175 let command = HierarchicalCommand {
176 command_id: "cmd-1".to_string(),
177 target: Some(CommandTarget {
178 scope: Scope::Individual as i32,
179 target_ids: vec!["node-1".to_string()],
180 }),
181 ..Default::default()
182 };
183
184 let resolution = router.resolve_target(&command);
185 assert_eq!(resolution, TargetResolution::Self_);
186 }
187
188 #[test]
189 fn test_resolve_individual_subordinate() {
190 let router = CommandRouter::new(
191 "node-1".to_string(),
192 Some("squad-alpha".to_string()),
193 vec!["node-1".to_string(), "node-2".to_string()],
194 None,
195 );
196
197 let command = HierarchicalCommand {
198 command_id: "cmd-1".to_string(),
199 target: Some(CommandTarget {
200 scope: Scope::Individual as i32,
201 target_ids: vec!["node-2".to_string()],
202 }),
203 ..Default::default()
204 };
205
206 let resolution = router.resolve_target(&command);
207 assert_eq!(
208 resolution,
209 TargetResolution::Subordinates(vec!["node-2".to_string()])
210 );
211 }
212
213 #[test]
214 fn test_resolve_squad() {
215 let router = CommandRouter::new(
216 "node-1".to_string(),
217 Some("squad-alpha".to_string()),
218 vec![
219 "node-1".to_string(),
220 "node-2".to_string(),
221 "node-3".to_string(),
222 ],
223 None,
224 );
225
226 let command = HierarchicalCommand {
227 command_id: "cmd-1".to_string(),
228 target: Some(CommandTarget {
229 scope: Scope::Squad as i32,
230 target_ids: vec!["squad-alpha".to_string()],
231 }),
232 ..Default::default()
233 };
234
235 let resolution = router.resolve_target(&command);
236 if let TargetResolution::AllSquadMembers(members) = resolution {
237 assert_eq!(members.len(), 3);
238 } else {
239 panic!("Expected AllSquadMembers resolution");
240 }
241 }
242
243 #[test]
244 fn test_should_route() {
245 let router = CommandRouter::new(
246 "node-1".to_string(),
247 Some("squad-alpha".to_string()),
248 vec!["node-1".to_string(), "node-2".to_string()],
249 None,
250 );
251
252 assert!(router.should_route(&TargetResolution::Subordinates(vec!["node-2".to_string()])));
253 assert!(!router.should_route(&TargetResolution::Self_));
254 assert!(!router.should_route(&TargetResolution::NotApplicable));
255 }
256}