1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6use crate::matcher::GlobPattern;
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
10pub struct Event {
11 pub kind: EventKind,
12 #[serde(default, skip_serializing_if = "Option::is_none")]
13 pub agent: Option<String>,
14 #[serde(default, skip_serializing_if = "Option::is_none")]
15 pub target: Option<String>,
16 #[serde(default, skip_serializing_if = "Option::is_none")]
17 pub task_id: Option<String>,
18 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
19 pub metadata: HashMap<String, String>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
24pub enum EventKind {
25 FileWrite,
26 FileCreate,
27 DependencyAdd,
28 DependencyRemove,
29 SchemaChange,
30 ApiChange,
31 ConfigChange,
32 TestRun,
33 TestPass,
34 TestFail,
35 LintPass,
36 LintFail,
37 Commit,
38 Build,
39 TaskClaim,
40 TaskComplete,
41 DangerousCommand,
42 Custom(String),
43}
44
45impl EventKind {
46 pub fn parse(s: &str) -> Self {
48 match s {
49 "FileWrite" => EventKind::FileWrite,
50 "FileCreate" => EventKind::FileCreate,
51 "DependencyAdd" => EventKind::DependencyAdd,
52 "DependencyRemove" => EventKind::DependencyRemove,
53 "SchemaChange" => EventKind::SchemaChange,
54 "ApiChange" => EventKind::ApiChange,
55 "ConfigChange" => EventKind::ConfigChange,
56 "TestRun" => EventKind::TestRun,
57 "TestPass" => EventKind::TestPass,
58 "TestFail" => EventKind::TestFail,
59 "LintPass" => EventKind::LintPass,
60 "LintFail" => EventKind::LintFail,
61 "Commit" => EventKind::Commit,
62 "Build" => EventKind::Build,
63 "TaskClaim" => EventKind::TaskClaim,
64 "TaskComplete" => EventKind::TaskComplete,
65 "DangerousCommand" => EventKind::DangerousCommand,
66 other => EventKind::Custom(other.to_string()),
67 }
68 }
69
70 pub fn as_str(&self) -> &str {
72 match self {
73 EventKind::FileWrite => "FileWrite",
74 EventKind::FileCreate => "FileCreate",
75 EventKind::DependencyAdd => "DependencyAdd",
76 EventKind::DependencyRemove => "DependencyRemove",
77 EventKind::SchemaChange => "SchemaChange",
78 EventKind::ApiChange => "ApiChange",
79 EventKind::ConfigChange => "ConfigChange",
80 EventKind::TestRun => "TestRun",
81 EventKind::TestPass => "TestPass",
82 EventKind::TestFail => "TestFail",
83 EventKind::LintPass => "LintPass",
84 EventKind::LintFail => "LintFail",
85 EventKind::Commit => "Commit",
86 EventKind::Build => "Build",
87 EventKind::TaskClaim => "TaskClaim",
88 EventKind::TaskComplete => "TaskComplete",
89 EventKind::DangerousCommand => "DangerousCommand",
90 EventKind::Custom(s) => s.as_str(),
91 }
92 }
93}
94
95impl std::fmt::Display for EventKind {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 f.write_str(self.as_str())
98 }
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
103pub struct EventPattern {
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub kind: Option<EventKind>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub agent: Option<String>,
108 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub target: Option<GlobPattern>,
110 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub task_id: Option<String>,
112 #[serde(default)]
114 pub negate_agent: bool,
115 #[serde(default, skip_serializing_if = "Vec::is_empty")]
117 pub target_not: Vec<GlobPattern>,
118}
119
120impl EventPattern {
121 pub fn kind(kind: EventKind) -> Self {
123 EventPattern {
124 kind: Some(kind),
125 agent: None,
126 target: None,
127 task_id: None,
128 negate_agent: false,
129 target_not: Vec::new(),
130 }
131 }
132
133 pub fn matches_event(&self, event: &Event) -> bool {
135 if let Some(ref pk) = self.kind {
137 if pk != &event.kind {
138 return false;
139 }
140 }
141
142 if let Some(ref pa) = self.agent {
144 match &event.agent {
145 Some(ea) => {
146 if self.negate_agent {
147 if ea == pa {
148 return false;
149 }
150 } else if ea != pa {
151 return false;
152 }
153 }
154 None => {
155 if !self.negate_agent {
157 return false;
158 }
159 }
160 }
161 }
162
163 if let Some(ref pt) = self.target {
165 match &event.target {
166 Some(et) => {
167 if !pt.matches(et) {
168 return false;
169 }
170 }
171 None => return false,
172 }
173 }
174
175 if !self.target_not.is_empty() {
177 if let Some(ref et) = event.target {
178 for exclude in &self.target_not {
179 if exclude.matches(et) {
180 return false;
181 }
182 }
183 }
184 }
185
186 if let Some(ref pt) = self.task_id {
188 match &event.task_id {
189 Some(et) => {
190 if et != pt {
191 return false;
192 }
193 }
194 None => return false,
195 }
196 }
197
198 true
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_event_kind_parse_roundtrip() {
208 let kinds = [
209 "FileWrite",
210 "TestPass",
211 "Commit",
212 "DangerousCommand",
213 "CustomThing",
214 ];
215 for kind_str in kinds {
216 let kind = EventKind::parse(kind_str);
217 assert_eq!(kind.as_str(), kind_str);
218 }
219 }
220
221 #[test]
222 fn test_event_pattern_kind_only() {
223 let pattern = EventPattern::kind(EventKind::FileWrite);
224 let event = Event {
225 kind: EventKind::FileWrite,
226 agent: Some("agent-1".to_string()),
227 target: Some("src/main.rs".to_string()),
228 task_id: None,
229 metadata: HashMap::new(),
230 };
231 assert!(pattern.matches_event(&event));
232
233 let wrong_kind = Event {
234 kind: EventKind::Commit,
235 agent: Some("agent-1".to_string()),
236 target: None,
237 task_id: None,
238 metadata: HashMap::new(),
239 };
240 assert!(!pattern.matches_event(&wrong_kind));
241 }
242
243 #[test]
244 fn test_event_pattern_kind_and_agent() {
245 let pattern = EventPattern {
246 kind: Some(EventKind::FileWrite),
247 agent: Some("agent-1".to_string()),
248 target: None,
249 task_id: None,
250 negate_agent: false,
251 target_not: Vec::new(),
252 };
253
254 let matching = Event {
255 kind: EventKind::FileWrite,
256 agent: Some("agent-1".to_string()),
257 target: Some("x".to_string()),
258 task_id: None,
259 metadata: HashMap::new(),
260 };
261 assert!(pattern.matches_event(&matching));
262
263 let wrong_agent = Event {
264 kind: EventKind::FileWrite,
265 agent: Some("agent-2".to_string()),
266 target: None,
267 task_id: None,
268 metadata: HashMap::new(),
269 };
270 assert!(!pattern.matches_event(&wrong_agent));
271
272 let no_agent = Event {
273 kind: EventKind::FileWrite,
274 agent: None,
275 target: None,
276 task_id: None,
277 metadata: HashMap::new(),
278 };
279 assert!(!pattern.matches_event(&no_agent));
280 }
281
282 #[test]
283 fn test_event_pattern_negated_agent() {
284 let pattern = EventPattern {
285 kind: Some(EventKind::FileWrite),
286 agent: Some("admin".to_string()),
287 target: None,
288 task_id: None,
289 negate_agent: true,
290 target_not: Vec::new(),
291 };
292
293 let event = Event {
295 kind: EventKind::FileWrite,
296 agent: Some("agent-1".to_string()),
297 target: None,
298 task_id: None,
299 metadata: HashMap::new(),
300 };
301 assert!(pattern.matches_event(&event));
302
303 let admin_event = Event {
305 kind: EventKind::FileWrite,
306 agent: Some("admin".to_string()),
307 target: None,
308 task_id: None,
309 metadata: HashMap::new(),
310 };
311 assert!(!pattern.matches_event(&admin_event));
312
313 let no_agent = Event {
315 kind: EventKind::FileWrite,
316 agent: None,
317 target: None,
318 task_id: None,
319 metadata: HashMap::new(),
320 };
321 assert!(pattern.matches_event(&no_agent));
322 }
323
324 #[test]
325 fn test_event_pattern_target_glob() {
326 use crate::matcher::GlobPattern;
327 let pattern = EventPattern {
328 kind: Some(EventKind::FileWrite),
329 agent: None,
330 target: Some(GlobPattern::new("src/**/*.rs")),
331 task_id: None,
332 negate_agent: false,
333 target_not: Vec::new(),
334 };
335
336 let matching = Event {
337 kind: EventKind::FileWrite,
338 agent: None,
339 target: Some("src/weave/mod.rs".to_string()),
340 task_id: None,
341 metadata: HashMap::new(),
342 };
343 assert!(pattern.matches_event(&matching));
344
345 let non_matching = Event {
346 kind: EventKind::FileWrite,
347 agent: None,
348 target: Some("tests/test.rs".to_string()),
349 task_id: None,
350 metadata: HashMap::new(),
351 };
352 assert!(!pattern.matches_event(&non_matching));
353
354 let no_target = Event {
355 kind: EventKind::FileWrite,
356 agent: None,
357 target: None,
358 task_id: None,
359 metadata: HashMap::new(),
360 };
361 assert!(!pattern.matches_event(&no_target));
362 }
363
364 #[test]
365 fn test_event_pattern_target_not_exclusion() {
366 use crate::matcher::GlobPattern;
367 let pattern = EventPattern {
368 kind: Some(EventKind::FileWrite),
369 agent: None,
370 target: None,
371 task_id: None,
372 negate_agent: false,
373 target_not: vec![GlobPattern::new("docs/**"), GlobPattern::new("*.md")],
374 };
375
376 let ok = Event {
378 kind: EventKind::FileWrite,
379 agent: None,
380 target: Some("src/main.rs".to_string()),
381 task_id: None,
382 metadata: HashMap::new(),
383 };
384 assert!(pattern.matches_event(&ok));
385
386 let docs = Event {
388 kind: EventKind::FileWrite,
389 agent: None,
390 target: Some("docs/api.md".to_string()),
391 task_id: None,
392 metadata: HashMap::new(),
393 };
394 assert!(!pattern.matches_event(&docs));
395
396 let md = Event {
398 kind: EventKind::FileWrite,
399 agent: None,
400 target: Some("README.md".to_string()),
401 task_id: None,
402 metadata: HashMap::new(),
403 };
404 assert!(!pattern.matches_event(&md));
405 }
406
407 #[test]
408 fn test_event_pattern_with_task_id() {
409 let pattern = EventPattern {
410 kind: Some(EventKind::TaskClaim),
411 agent: None,
412 target: None,
413 task_id: Some("auth:1.1".to_string()),
414 negate_agent: false,
415 target_not: Vec::new(),
416 };
417
418 let matching = Event {
419 kind: EventKind::TaskClaim,
420 agent: Some("agent-1".to_string()),
421 target: None,
422 task_id: Some("auth:1.1".to_string()),
423 metadata: HashMap::new(),
424 };
425 assert!(pattern.matches_event(&matching));
426
427 let wrong_task = Event {
428 kind: EventKind::TaskClaim,
429 agent: None,
430 target: None,
431 task_id: Some("auth:1.2".to_string()),
432 metadata: HashMap::new(),
433 };
434 assert!(!pattern.matches_event(&wrong_task));
435
436 let no_task = Event {
437 kind: EventKind::TaskClaim,
438 agent: None,
439 target: None,
440 task_id: None,
441 metadata: HashMap::new(),
442 };
443 assert!(!pattern.matches_event(&no_task));
444 }
445
446 #[test]
447 fn test_event_pattern_wildcard_matches_all_kinds() {
448 let pattern = EventPattern {
450 kind: None,
451 agent: None,
452 target: None,
453 task_id: None,
454 negate_agent: false,
455 target_not: Vec::new(),
456 };
457
458 for kind in [EventKind::FileWrite, EventKind::Commit, EventKind::Build] {
459 let event = Event {
460 kind,
461 agent: None,
462 target: None,
463 task_id: None,
464 metadata: HashMap::new(),
465 };
466 assert!(pattern.matches_event(&event));
467 }
468 }
469
470 #[test]
471 fn test_event_kind_all_variants_roundtrip() {
472 let variants = [
473 "FileWrite", "FileCreate", "DependencyAdd", "DependencyRemove",
474 "SchemaChange", "ApiChange", "ConfigChange", "TestRun",
475 "TestPass", "TestFail", "LintPass", "LintFail",
476 "Commit", "Build", "TaskClaim", "TaskComplete", "DangerousCommand",
477 ];
478 for name in variants {
479 let kind = EventKind::parse(name);
480 assert_eq!(kind.as_str(), name, "Roundtrip failed for {}", name);
481 }
482 }
483
484 #[test]
485 fn test_event_kind_custom_roundtrip() {
486 let kind = EventKind::parse("MyCustomEvent");
487 assert_eq!(kind.as_str(), "MyCustomEvent");
488 match kind {
489 EventKind::Custom(s) => assert_eq!(s, "MyCustomEvent"),
490 _ => panic!("Expected Custom variant"),
491 }
492 }
493
494 #[test]
495 fn test_event_serde_roundtrip() {
496 let event = Event {
497 kind: EventKind::FileWrite,
498 agent: Some("agent-1".to_string()),
499 target: Some("src/main.rs".to_string()),
500 task_id: None,
501 metadata: HashMap::new(),
502 };
503 let json = serde_json::to_string(&event).unwrap();
504 let parsed: Event = serde_json::from_str(&json).unwrap();
505 assert_eq!(event, parsed);
506 }
507}