Skip to main content

khive_runtime/
pack.rs

1//! Pack runtime trait and verb registry (ADR-025 step 2).
2//!
3//! Packs register verbs into the runtime. The registry routes verb calls
4//! to the pack that declares them.
5//!
6//! `Pack` (in khive-types) uses const associated items which are not
7//! object-safe. `PackRuntime` mirrors that metadata as methods so the
8//! registry can store packs as trait objects. See ADR-025 §PackRuntime.
9//!
10//! Lifecycle: build with `VerbRegistryBuilder`, then call `.build()` to
11//! get a cheaply-cloneable `VerbRegistry`. Registration is only possible
12//! through the builder.
13
14use async_trait::async_trait;
15use serde_json::Value;
16
17pub use khive_types::VerbDef;
18
19use crate::error::RuntimeError;
20
21/// Async dispatch trait for packs (ADR-025).
22///
23/// This is the object-safe behavioral counterpart to `khive_types::Pack`.
24/// `Pack` uses const associated items (not object-safe in Rust); this trait
25/// mirrors that metadata as methods and adds async dispatch.
26///
27/// Registration requires `P: Pack + PackRuntime` — the compiler enforces
28/// that every runtime pack also declares its vocabulary via `Pack`.
29#[async_trait]
30pub trait PackRuntime: Send + Sync {
31    /// Pack name — must equal `<Self as Pack>::NAME`.
32    fn name(&self) -> &str;
33
34    /// Note kinds this pack owns — must equal `<Self as Pack>::NOTE_KINDS`.
35    fn note_kinds(&self) -> &'static [&'static str];
36
37    /// Entity kinds this pack owns — must equal `<Self as Pack>::ENTITY_KINDS`.
38    fn entity_kinds(&self) -> &'static [&'static str];
39
40    /// Verbs this pack handles — must equal `<Self as Pack>::VERBS`.
41    fn verbs(&self) -> &'static [VerbDef];
42
43    /// Dispatch a verb call. Returns serialized JSON response.
44    async fn dispatch(&self, verb: &str, params: Value) -> Result<Value, RuntimeError>;
45}
46
47/// Builder for constructing a `VerbRegistry`.
48///
49/// Packs are registered here; once `.build()` is called the registry is
50/// immutable and cheaply cloneable.
51pub struct VerbRegistryBuilder {
52    packs: Vec<Box<dyn PackRuntime>>,
53}
54
55impl VerbRegistryBuilder {
56    pub fn new() -> Self {
57        Self { packs: Vec::new() }
58    }
59
60    /// Register a pack. The bound `P: Pack + PackRuntime` ensures the pack
61    /// declares vocabulary via `Pack` consts alongside runtime dispatch.
62    pub fn register<P: khive_types::Pack + PackRuntime + 'static>(&mut self, pack: P) -> &mut Self {
63        self.packs.push(Box::new(pack));
64        self
65    }
66
67    /// Consume the builder and produce an immutable, cloneable registry.
68    pub fn build(self) -> VerbRegistry {
69        VerbRegistry {
70            packs: std::sync::Arc::new(self.packs),
71        }
72    }
73}
74
75impl Default for VerbRegistryBuilder {
76    fn default() -> Self {
77        Self::new()
78    }
79}
80
81/// Immutable registry that dispatches verb calls to registered packs.
82///
83/// Clone is cheap (Arc-wrapped). Constructed via `VerbRegistryBuilder`.
84#[derive(Clone)]
85pub struct VerbRegistry {
86    packs: std::sync::Arc<Vec<Box<dyn PackRuntime>>>,
87}
88
89impl VerbRegistry {
90    /// Dispatch a verb to the first pack that handles it.
91    ///
92    /// When multiple packs declare the same verb, the first registered pack wins.
93    pub async fn dispatch(&self, verb: &str, params: Value) -> Result<Value, RuntimeError> {
94        for pack in self.packs.iter() {
95            if pack.verbs().iter().any(|v| v.name == verb) {
96                return pack.dispatch(verb, params).await;
97            }
98        }
99        let available: Vec<&str> = self
100            .packs
101            .iter()
102            .flat_map(|p| p.verbs().iter().map(|v| v.name))
103            .collect();
104        Err(RuntimeError::InvalidInput(format!(
105            "unknown verb {verb:?}; available: {}",
106            available.join(", ")
107        )))
108    }
109
110    /// All verb definitions across all registered packs.
111    pub fn all_verbs(&self) -> Vec<&VerbDef> {
112        self.packs.iter().flat_map(|p| p.verbs().iter()).collect()
113    }
114
115    /// Merged set of note kinds across all registered packs (deduplicated,
116    /// first-seen order preserved).
117    pub fn all_note_kinds(&self) -> Vec<&'static str> {
118        let mut seen = std::collections::HashSet::new();
119        self.packs
120            .iter()
121            .flat_map(|p| p.note_kinds().iter().copied())
122            .filter(|k| seen.insert(*k))
123            .collect()
124    }
125
126    /// Merged set of entity kinds across all registered packs (deduplicated,
127    /// first-seen order preserved).
128    pub fn all_entity_kinds(&self) -> Vec<&'static str> {
129        let mut seen = std::collections::HashSet::new();
130        self.packs
131            .iter()
132            .flat_map(|p| p.entity_kinds().iter().copied())
133            .filter(|k| seen.insert(*k))
134            .collect()
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use khive_types::Pack;
142
143    struct AlphaPack;
144
145    impl Pack for AlphaPack {
146        const NAME: &'static str = "alpha";
147        const NOTE_KINDS: &'static [&'static str] = &["memo", "log"];
148        const ENTITY_KINDS: &'static [&'static str] = &["widget"];
149        const VERBS: &'static [VerbDef] = &[
150            VerbDef {
151                name: "create",
152                description: "create a widget",
153            },
154            VerbDef {
155                name: "list",
156                description: "list widgets",
157            },
158        ];
159    }
160
161    #[async_trait]
162    impl PackRuntime for AlphaPack {
163        fn name(&self) -> &str {
164            AlphaPack::NAME
165        }
166        fn note_kinds(&self) -> &'static [&'static str] {
167            AlphaPack::NOTE_KINDS
168        }
169        fn entity_kinds(&self) -> &'static [&'static str] {
170            AlphaPack::ENTITY_KINDS
171        }
172        fn verbs(&self) -> &'static [VerbDef] {
173            AlphaPack::VERBS
174        }
175        async fn dispatch(&self, verb: &str, _params: Value) -> Result<Value, RuntimeError> {
176            Ok(serde_json::json!({ "pack": "alpha", "verb": verb }))
177        }
178    }
179
180    struct BetaPack;
181
182    impl Pack for BetaPack {
183        const NAME: &'static str = "beta";
184        const NOTE_KINDS: &'static [&'static str] = &["log", "alert"];
185        const ENTITY_KINDS: &'static [&'static str] = &["widget", "gadget"];
186        const VERBS: &'static [VerbDef] = &[
187            VerbDef {
188                name: "notify",
189                description: "send alert",
190            },
191            VerbDef {
192                name: "create",
193                description: "create a gadget",
194            },
195        ];
196    }
197
198    #[async_trait]
199    impl PackRuntime for BetaPack {
200        fn name(&self) -> &str {
201            BetaPack::NAME
202        }
203        fn note_kinds(&self) -> &'static [&'static str] {
204            BetaPack::NOTE_KINDS
205        }
206        fn entity_kinds(&self) -> &'static [&'static str] {
207            BetaPack::ENTITY_KINDS
208        }
209        fn verbs(&self) -> &'static [VerbDef] {
210            BetaPack::VERBS
211        }
212        async fn dispatch(&self, verb: &str, _params: Value) -> Result<Value, RuntimeError> {
213            Ok(serde_json::json!({ "pack": "beta", "verb": verb }))
214        }
215    }
216
217    fn build_registry() -> VerbRegistry {
218        let mut builder = VerbRegistryBuilder::new();
219        builder.register(AlphaPack);
220        builder.register(BetaPack);
221        builder.build()
222    }
223
224    #[tokio::test]
225    async fn dispatch_routes_to_correct_pack() {
226        let reg = build_registry();
227
228        let res = reg.dispatch("list", Value::Null).await.unwrap();
229        assert_eq!(res["pack"], "alpha");
230
231        let res = reg.dispatch("notify", Value::Null).await.unwrap();
232        assert_eq!(res["pack"], "beta");
233    }
234
235    #[tokio::test]
236    async fn dispatch_first_registered_wins_on_collision() {
237        let reg = build_registry();
238
239        let res = reg.dispatch("create", Value::Null).await.unwrap();
240        assert_eq!(res["pack"], "alpha", "first registered pack wins");
241    }
242
243    #[tokio::test]
244    async fn dispatch_unknown_verb_returns_error() {
245        let reg = build_registry();
246
247        let err = reg.dispatch("explode", Value::Null).await.unwrap_err();
248        let msg = err.to_string();
249        assert!(msg.contains("explode"));
250        assert!(msg.contains("create"));
251    }
252
253    #[test]
254    fn all_verbs_aggregates_across_packs() {
255        let reg = build_registry();
256        let verbs: Vec<&str> = reg.all_verbs().iter().map(|v| v.name).collect();
257        assert_eq!(verbs, vec!["create", "list", "notify", "create"]);
258    }
259
260    #[test]
261    fn note_kinds_are_deduplicated() {
262        let reg = build_registry();
263        let kinds = reg.all_note_kinds();
264        assert_eq!(kinds, vec!["memo", "log", "alert"]);
265    }
266
267    #[test]
268    fn entity_kinds_are_deduplicated() {
269        let reg = build_registry();
270        let kinds = reg.all_entity_kinds();
271        assert_eq!(kinds, vec!["widget", "gadget"]);
272    }
273}