1use async_trait::async_trait;
15use serde_json::Value;
16
17pub use khive_types::VerbDef;
18
19use crate::error::RuntimeError;
20
21#[async_trait]
30pub trait PackRuntime: Send + Sync {
31 fn name(&self) -> &str;
33
34 fn note_kinds(&self) -> &'static [&'static str];
36
37 fn entity_kinds(&self) -> &'static [&'static str];
39
40 fn verbs(&self) -> &'static [VerbDef];
42
43 async fn dispatch(&self, verb: &str, params: Value) -> Result<Value, RuntimeError>;
45}
46
47pub 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 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 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#[derive(Clone)]
85pub struct VerbRegistry {
86 packs: std::sync::Arc<Vec<Box<dyn PackRuntime>>>,
87}
88
89impl VerbRegistry {
90 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 pub fn all_verbs(&self) -> Vec<&VerbDef> {
112 self.packs.iter().flat_map(|p| p.verbs().iter()).collect()
113 }
114
115 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 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}