Skip to main content

peat_protocol/command/
routing.rs

1//! Command routing logic for hierarchical dissemination
2//!
3//! Implements target resolution and routing decisions based on command policies.
4
5use peat_schema::command::v1::{command_target::Scope, HierarchicalCommand};
6use std::collections::HashSet;
7
8/// Resolves command targets to specific node IDs
9pub struct CommandRouter {
10    /// Current node ID
11    node_id: String,
12
13    /// Squad ID (if member of a squad)
14    squad_id: Option<String>,
15
16    /// Squad members (if leader)
17    squad_members: Vec<String>,
18
19    /// Platoon ID (if member of a platoon)
20    platoon_id: Option<String>,
21}
22
23/// Result of target resolution
24#[derive(Debug, Clone, PartialEq)]
25pub enum TargetResolution {
26    /// Command targets this node directly
27    Self_,
28
29    /// Command targets subordinate nodes (IDs listed)
30    Subordinates(Vec<String>),
31
32    /// Command targets all squad members
33    AllSquadMembers(Vec<String>),
34
35    /// Command does not target this node or subordinates
36    NotApplicable,
37}
38
39impl CommandRouter {
40    /// Create new router for a node
41    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    /// Resolve command target to specific nodes
56    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                // Target specific individuals
67                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                    // Check if any subordinates are targeted
73                    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                // Target entire squad(s)
90                if let Some(ref my_squad) = self.squad_id {
91                    if target.target_ids.contains(my_squad) {
92                        // This squad is targeted
93                        if !self.squad_members.is_empty() {
94                            // This node is squad leader - target all members
95                            TargetResolution::AllSquadMembers(self.squad_members.clone())
96                        } else {
97                            // This node is a squad member - target self
98                            TargetResolution::Self_
99                        }
100                    } else {
101                        TargetResolution::NotApplicable
102                    }
103                } else {
104                    TargetResolution::NotApplicable
105                }
106            }
107
108            Scope::Platoon => {
109                // Target entire platoon(s)
110                if let Some(ref my_platoon) = self.platoon_id {
111                    if target.target_ids.contains(my_platoon) {
112                        // This platoon is targeted
113                        if !self.squad_members.is_empty() {
114                            // This node is squad leader - target all members
115                            TargetResolution::AllSquadMembers(self.squad_members.clone())
116                        } else {
117                            // This node is a platoon member - target self
118                            TargetResolution::Self_
119                        }
120                    } else {
121                        TargetResolution::NotApplicable
122                    }
123                } else {
124                    TargetResolution::NotApplicable
125                }
126            }
127
128            Scope::Broadcast => {
129                // Broadcast to all nodes
130                if !self.squad_members.is_empty() {
131                    // Squad leader - target all members
132                    TargetResolution::AllSquadMembers(self.squad_members.clone())
133                } else {
134                    // Regular node - target self
135                    TargetResolution::Self_
136                }
137            }
138
139            Scope::Unspecified => TargetResolution::NotApplicable,
140        }
141    }
142
143    /// Check if this node should route the command downward
144    pub fn should_route(&self, resolution: &TargetResolution) -> bool {
145        matches!(
146            resolution,
147            TargetResolution::Subordinates(_) | TargetResolution::AllSquadMembers(_)
148        )
149    }
150
151    /// Get list of nodes to route command to
152    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}