Skip to main content

actionqueue_actor/
routing.rs

1//! Capability-based task routing.
2
3use actionqueue_core::ids::ActorId;
4
5/// Stateless capability intersection matcher for task → actor routing.
6///
7/// The dispatch loop uses this to filter which actors are eligible to
8/// claim a task based on its `required_capabilities`.
9pub struct CapabilityRouter;
10
11impl CapabilityRouter {
12    /// Returns `true` if `actor_capabilities` contains ALL entries in `required`.
13    ///
14    /// An empty `required` slice matches any actor (no capability requirements).
15    pub fn can_handle(actor_capabilities: &[String], required: &[String]) -> bool {
16        required.iter().all(|r| actor_capabilities.iter().any(|c| c == r))
17    }
18
19    /// Filters a list of `(actor_id, capabilities)` pairs to those eligible
20    /// to handle a task with `required_capabilities`.
21    pub fn eligible_actors(actors: &[(ActorId, &[String])], required: &[String]) -> Vec<ActorId> {
22        actors
23            .iter()
24            .filter(|(_, caps)| Self::can_handle(caps, required))
25            .map(|(id, _)| *id)
26            .collect()
27    }
28}
29
30#[cfg(test)]
31mod tests {
32    use actionqueue_core::ids::ActorId;
33
34    use super::CapabilityRouter;
35
36    fn caps(c: &[&str]) -> Vec<String> {
37        c.iter().map(|s| s.to_string()).collect()
38    }
39
40    #[test]
41    fn can_handle_all_required_present() {
42        let actor = caps(&["compute", "review", "approve"]);
43        assert!(CapabilityRouter::can_handle(&actor, &caps(&["compute", "review"])));
44    }
45
46    #[test]
47    fn can_handle_missing_requirement() {
48        let actor = caps(&["compute"]);
49        assert!(!CapabilityRouter::can_handle(&actor, &caps(&["compute", "review"])));
50    }
51
52    #[test]
53    fn can_handle_empty_required_always_matches() {
54        let actor = caps(&[]);
55        assert!(CapabilityRouter::can_handle(&actor, &[]));
56    }
57
58    #[test]
59    fn eligible_actors_filters_correctly() {
60        let a = ActorId::new();
61        let b = ActorId::new();
62        let c = ActorId::new();
63        let a_caps = caps(&["compute", "review"]);
64        let b_caps = caps(&["compute"]);
65        let c_caps = caps(&["review"]);
66
67        let actors = vec![(a, a_caps.as_slice()), (b, b_caps.as_slice()), (c, c_caps.as_slice())];
68        let required = caps(&["compute", "review"]);
69        let eligible = CapabilityRouter::eligible_actors(&actors, &required);
70
71        assert_eq!(eligible.len(), 1);
72        assert!(eligible.contains(&a));
73    }
74}