1use serde::{Deserialize, Serialize};
43use std::collections::HashSet;
44
45#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct SubscriptionEntry {
71 pub category: EventCategory,
73 pub operations: Option<HashSet<String>>,
76}
77
78impl SubscriptionEntry {
79 #[must_use]
81 pub fn all(category: EventCategory) -> Self {
82 Self {
83 category,
84 operations: None,
85 }
86 }
87
88 #[must_use]
90 pub fn with_operations(
91 category: EventCategory,
92 operations: impl IntoIterator<Item = String>,
93 ) -> Self {
94 Self {
95 category,
96 operations: Some(operations.into_iter().collect()),
97 }
98 }
99
100 #[must_use]
106 pub fn matches(&self, category: &EventCategory, operation: &str) -> bool {
107 if self.category != *category {
108 return false;
109 }
110 match &self.operations {
111 None => true,
112 Some(ops) => ops.contains(operation),
113 }
114 }
115
116 #[must_use]
118 pub fn category(&self) -> &EventCategory {
119 &self.category
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
129pub enum EventCategory {
130 Lifecycle,
134
135 Hil,
139
140 Echo,
144
145 UserInput,
153
154 Output,
163
164 Extension {
168 namespace: String,
170 kind: String,
172 },
173}
174
175impl EventCategory {
176 #[must_use]
187 pub fn extension(namespace: impl Into<String>, kind: impl Into<String>) -> Self {
188 Self::Extension {
189 namespace: namespace.into(),
190 kind: kind.into(),
191 }
192 }
193
194 #[must_use]
196 pub fn is_lifecycle(&self) -> bool {
197 matches!(self, Self::Lifecycle)
198 }
199
200 #[must_use]
202 pub fn is_hil(&self) -> bool {
203 matches!(self, Self::Hil)
204 }
205
206 #[must_use]
208 pub fn is_extension(&self) -> bool {
209 matches!(self, Self::Extension { .. })
210 }
211
212 #[must_use]
214 pub fn is_output(&self) -> bool {
215 matches!(self, Self::Output)
216 }
217
218 #[must_use]
220 pub fn is_user_input(&self) -> bool {
221 matches!(self, Self::UserInput)
222 }
223
224 #[must_use]
226 pub fn name(&self) -> String {
227 match self {
228 Self::Lifecycle => "Lifecycle".to_string(),
229 Self::Hil => "Hil".to_string(),
230 Self::Echo => "Echo".to_string(),
231 Self::UserInput => "UserInput".to_string(),
232 Self::Output => "Output".to_string(),
233 Self::Extension { namespace, kind } => format!("{}:{}", namespace, kind),
234 }
235 }
236}
237
238impl std::fmt::Display for EventCategory {
239 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240 write!(f, "{}", self.name())
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn category_creation() {
250 let lifecycle = EventCategory::Lifecycle;
251 let hil = EventCategory::Hil;
252 let echo = EventCategory::Echo;
253
254 assert!(lifecycle.is_lifecycle());
255 assert!(hil.is_hil());
256 assert!(!echo.is_extension());
257 }
258
259 #[test]
260 fn category_extension() {
261 let ext = EventCategory::extension("my-plugin", "data");
262
263 assert!(ext.is_extension());
264 assert!(!ext.is_lifecycle());
265
266 if let EventCategory::Extension { namespace, kind } = ext {
267 assert_eq!(namespace, "my-plugin");
268 assert_eq!(kind, "data");
269 } else {
270 panic!("Expected Extension");
271 }
272 }
273
274 #[test]
275 fn category_display() {
276 assert_eq!(EventCategory::Lifecycle.to_string(), "Lifecycle");
277 assert_eq!(EventCategory::Hil.to_string(), "Hil");
278 assert_eq!(EventCategory::Echo.to_string(), "Echo");
279 assert_eq!(
280 EventCategory::extension("plugin", "event").to_string(),
281 "plugin:event"
282 );
283 }
284
285 #[test]
286 fn category_name() {
287 assert_eq!(EventCategory::Lifecycle.name(), "Lifecycle");
288 assert_eq!(EventCategory::extension("ns", "k").name(), "ns:k");
289 }
290
291 #[test]
292 fn category_equality() {
293 assert_eq!(EventCategory::Hil, EventCategory::Hil);
294 assert_ne!(EventCategory::Hil, EventCategory::Echo);
295
296 let ext1 = EventCategory::extension("a", "b");
297 let ext2 = EventCategory::extension("a", "b");
298 let ext3 = EventCategory::extension("a", "c");
299
300 assert_eq!(ext1, ext2);
301 assert_ne!(ext1, ext3);
302 }
303
304 #[test]
305 fn category_hash() {
306 use std::collections::HashSet;
307
308 let mut set = HashSet::new();
309 set.insert(EventCategory::Hil);
310 set.insert(EventCategory::Echo);
311 set.insert(EventCategory::Hil); assert_eq!(set.len(), 2);
314 assert!(set.contains(&EventCategory::Hil));
315 assert!(set.contains(&EventCategory::Echo));
316 }
317
318 #[test]
319 fn category_serialize() {
320 let hil = EventCategory::Hil;
321 let json =
322 serde_json::to_string(&hil).expect("EventCategory::Hil should serialize to JSON");
323 assert!(json.contains("Hil"));
324
325 let ext = EventCategory::extension("ns", "kind");
326 let json =
327 serde_json::to_string(&ext).expect("EventCategory::Extension should serialize to JSON");
328 assert!(json.contains("Extension"));
329 assert!(json.contains("ns"));
330 assert!(json.contains("kind"));
331 }
332
333 #[test]
334 fn category_deserialize() {
335 let json = r#""Hil""#;
336 let cat: EventCategory = serde_json::from_str(json)
337 .expect("'Hil' JSON string should deserialize to EventCategory::Hil");
338 assert_eq!(cat, EventCategory::Hil);
339
340 let json = r#"{"Extension":{"namespace":"ns","kind":"k"}}"#;
341 let cat: EventCategory = serde_json::from_str(json)
342 .expect("Extension JSON should deserialize to EventCategory::Extension");
343 assert_eq!(cat, EventCategory::extension("ns", "k"));
344 }
345
346 #[test]
349 fn subscription_entry_all_matches_any_operation() {
350 let entry = SubscriptionEntry::all(EventCategory::Echo);
351 assert!(entry.matches(&EventCategory::Echo, "echo"));
352 assert!(entry.matches(&EventCategory::Echo, "anything"));
353 assert!(!entry.matches(&EventCategory::Hil, "echo"));
354 }
355
356 #[test]
357 fn subscription_entry_with_operations_filters() {
358 let ext = EventCategory::extension("lua", "Extension");
359 let entry = SubscriptionEntry::with_operations(
360 ext.clone(),
361 ["route_response".to_string(), "skill_update".to_string()],
362 );
363 assert!(entry.matches(&ext, "route_response"));
364 assert!(entry.matches(&ext, "skill_update"));
365 assert!(!entry.matches(&ext, "llm_response"));
366 assert!(!entry.matches(&EventCategory::Echo, "route_response"));
367 }
368
369 #[test]
370 fn subscription_entry_empty_operations_rejects_all() {
371 let entry =
372 SubscriptionEntry::with_operations(EventCategory::Echo, std::iter::empty::<String>());
373 assert!(!entry.matches(&EventCategory::Echo, "echo"));
374 }
375
376 #[test]
377 fn subscription_entry_category_accessor() {
378 let entry = SubscriptionEntry::all(EventCategory::Hil);
379 assert_eq!(entry.category(), &EventCategory::Hil);
380 }
381}