1use crate::config::events::TriggerConfig;
23use crate::core::events::{EntityEvent, FrameworkEvent, LinkEvent};
24
25#[derive(Debug, Clone, PartialEq)]
27enum EventKind {
28 LinkCreated,
29 LinkDeleted,
30 EntityCreated,
31 EntityUpdated,
32 EntityDeleted,
33}
34
35#[derive(Debug, Clone)]
40pub struct EventMatcher {
41 kind: EventKind,
43
44 link_type: Option<String>,
46
47 entity_type: Option<String>,
49}
50
51#[derive(Debug, thiserror::Error)]
53#[error(
54 "unknown event kind: '{kind}'. Expected one of: link.created, link.deleted, entity.created, entity.updated, entity.deleted"
55)]
56pub struct UnknownEventKind {
57 pub kind: String,
58}
59
60impl EventMatcher {
61 pub fn compile(config: &TriggerConfig) -> Result<Self, UnknownEventKind> {
79 let kind = match config.kind.as_str() {
80 "link.created" => EventKind::LinkCreated,
81 "link.deleted" => EventKind::LinkDeleted,
82 "entity.created" => EventKind::EntityCreated,
83 "entity.updated" => EventKind::EntityUpdated,
84 "entity.deleted" => EventKind::EntityDeleted,
85 _ => {
86 return Err(UnknownEventKind {
87 kind: config.kind.clone(),
88 });
89 }
90 };
91
92 Ok(Self {
93 kind,
94 link_type: config.link_type.clone(),
95 entity_type: config.entity_type.clone(),
96 })
97 }
98
99 pub fn matches(&self, event: &FrameworkEvent) -> bool {
107 match event {
108 FrameworkEvent::Link(link_event) => self.matches_link(link_event),
109 FrameworkEvent::Entity(entity_event) => self.matches_entity(entity_event),
110 }
111 }
112
113 fn matches_link(&self, event: &LinkEvent) -> bool {
115 let (kind_matches, event_link_type) = match event {
116 LinkEvent::Created { link_type, .. } => {
117 (self.kind == EventKind::LinkCreated, link_type)
118 }
119 LinkEvent::Deleted { link_type, .. } => {
120 (self.kind == EventKind::LinkDeleted, link_type)
121 }
122 };
123
124 if !kind_matches {
125 return false;
126 }
127
128 match &self.link_type {
130 Some(expected) => expected == event_link_type,
131 None => true,
132 }
133 }
134
135 fn matches_entity(&self, event: &EntityEvent) -> bool {
137 let (kind_matches, event_entity_type) = match event {
138 EntityEvent::Created { entity_type, .. } => {
139 (self.kind == EventKind::EntityCreated, entity_type)
140 }
141 EntityEvent::Updated { entity_type, .. } => {
142 (self.kind == EventKind::EntityUpdated, entity_type)
143 }
144 EntityEvent::Deleted { entity_type, .. } => {
145 (self.kind == EventKind::EntityDeleted, entity_type)
146 }
147 };
148
149 if !kind_matches {
150 return false;
151 }
152
153 match &self.entity_type {
155 Some(expected) => expected == event_entity_type,
156 None => true,
157 }
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use serde_json::json;
165 use uuid::Uuid;
166
167 fn link_created(link_type: &str) -> FrameworkEvent {
170 FrameworkEvent::Link(LinkEvent::Created {
171 link_type: link_type.to_string(),
172 link_id: Uuid::new_v4(),
173 source_id: Uuid::new_v4(),
174 target_id: Uuid::new_v4(),
175 metadata: None,
176 })
177 }
178
179 fn link_deleted(link_type: &str) -> FrameworkEvent {
180 FrameworkEvent::Link(LinkEvent::Deleted {
181 link_type: link_type.to_string(),
182 link_id: Uuid::new_v4(),
183 source_id: Uuid::new_v4(),
184 target_id: Uuid::new_v4(),
185 })
186 }
187
188 fn entity_created(entity_type: &str) -> FrameworkEvent {
189 FrameworkEvent::Entity(EntityEvent::Created {
190 entity_type: entity_type.to_string(),
191 entity_id: Uuid::new_v4(),
192 data: json!({"name": "test"}),
193 })
194 }
195
196 fn entity_updated(entity_type: &str) -> FrameworkEvent {
197 FrameworkEvent::Entity(EntityEvent::Updated {
198 entity_type: entity_type.to_string(),
199 entity_id: Uuid::new_v4(),
200 data: json!({"name": "updated"}),
201 })
202 }
203
204 fn entity_deleted(entity_type: &str) -> FrameworkEvent {
205 FrameworkEvent::Entity(EntityEvent::Deleted {
206 entity_type: entity_type.to_string(),
207 entity_id: Uuid::new_v4(),
208 })
209 }
210
211 fn trigger(kind: &str, link_type: Option<&str>, entity_type: Option<&str>) -> TriggerConfig {
212 TriggerConfig {
213 kind: kind.to_string(),
214 link_type: link_type.map(String::from),
215 entity_type: entity_type.map(String::from),
216 }
217 }
218
219 #[test]
222 fn test_link_created_wildcard() {
223 let m = EventMatcher::compile(&trigger("link.created", None, None)).unwrap();
224 assert!(m.matches(&link_created("follows")));
225 assert!(m.matches(&link_created("likes")));
226 assert!(m.matches(&link_created("blocks")));
227 assert!(!m.matches(&link_deleted("follows")));
229 assert!(!m.matches(&entity_created("user")));
230 }
231
232 #[test]
233 fn test_link_created_with_type_filter() {
234 let m = EventMatcher::compile(&trigger("link.created", Some("follows"), None)).unwrap();
235 assert!(m.matches(&link_created("follows")));
236 assert!(!m.matches(&link_created("likes")));
237 assert!(!m.matches(&link_created("blocks")));
238 }
239
240 #[test]
243 fn test_link_deleted_wildcard() {
244 let m = EventMatcher::compile(&trigger("link.deleted", None, None)).unwrap();
245 assert!(m.matches(&link_deleted("follows")));
246 assert!(m.matches(&link_deleted("likes")));
247 assert!(!m.matches(&link_created("follows")));
248 assert!(!m.matches(&entity_deleted("user")));
249 }
250
251 #[test]
252 fn test_link_deleted_with_type_filter() {
253 let m = EventMatcher::compile(&trigger("link.deleted", Some("likes"), None)).unwrap();
254 assert!(m.matches(&link_deleted("likes")));
255 assert!(!m.matches(&link_deleted("follows")));
256 }
257
258 #[test]
261 fn test_entity_created_wildcard() {
262 let m = EventMatcher::compile(&trigger("entity.created", None, None)).unwrap();
263 assert!(m.matches(&entity_created("user")));
264 assert!(m.matches(&entity_created("post")));
265 assert!(!m.matches(&entity_updated("user")));
266 assert!(!m.matches(&link_created("follows")));
267 }
268
269 #[test]
270 fn test_entity_created_with_type_filter() {
271 let m = EventMatcher::compile(&trigger("entity.created", None, Some("capture"))).unwrap();
272 assert!(m.matches(&entity_created("capture")));
273 assert!(!m.matches(&entity_created("user")));
274 assert!(!m.matches(&entity_created("post")));
275 }
276
277 #[test]
280 fn test_entity_updated_wildcard() {
281 let m = EventMatcher::compile(&trigger("entity.updated", None, None)).unwrap();
282 assert!(m.matches(&entity_updated("user")));
283 assert!(m.matches(&entity_updated("post")));
284 assert!(!m.matches(&entity_created("user")));
285 assert!(!m.matches(&entity_deleted("user")));
286 }
287
288 #[test]
289 fn test_entity_updated_with_type_filter() {
290 let m = EventMatcher::compile(&trigger("entity.updated", None, Some("user"))).unwrap();
291 assert!(m.matches(&entity_updated("user")));
292 assert!(!m.matches(&entity_updated("post")));
293 }
294
295 #[test]
298 fn test_entity_deleted_wildcard() {
299 let m = EventMatcher::compile(&trigger("entity.deleted", None, None)).unwrap();
300 assert!(m.matches(&entity_deleted("user")));
301 assert!(m.matches(&entity_deleted("post")));
302 assert!(!m.matches(&entity_created("user")));
303 assert!(!m.matches(&entity_updated("user")));
304 }
305
306 #[test]
307 fn test_entity_deleted_with_type_filter() {
308 let m = EventMatcher::compile(&trigger("entity.deleted", None, Some("post"))).unwrap();
309 assert!(m.matches(&entity_deleted("post")));
310 assert!(!m.matches(&entity_deleted("user")));
311 }
312
313 #[test]
316 fn test_unknown_kind_returns_error() {
317 let result = EventMatcher::compile(&trigger("link.updated", None, None));
318 assert!(result.is_err());
319 let err = result.unwrap_err();
320 assert!(err.to_string().contains("link.updated"));
321 }
322
323 #[test]
324 fn test_invalid_kind_returns_error() {
325 let result = EventMatcher::compile(&trigger("banana", None, None));
326 assert!(result.is_err());
327 }
328
329 #[test]
332 fn test_link_matcher_never_matches_entity_events() {
333 let m = EventMatcher::compile(&trigger("link.created", None, None)).unwrap();
334 assert!(!m.matches(&entity_created("user")));
335 assert!(!m.matches(&entity_updated("user")));
336 assert!(!m.matches(&entity_deleted("user")));
337 }
338
339 #[test]
340 fn test_entity_matcher_never_matches_link_events() {
341 let m = EventMatcher::compile(&trigger("entity.created", None, None)).unwrap();
342 assert!(!m.matches(&link_created("follows")));
343 assert!(!m.matches(&link_deleted("follows")));
344 }
345
346 #[test]
349 fn test_link_type_filter_ignored_for_entity_matcher() {
350 let m = EventMatcher::compile(&trigger("entity.created", Some("follows"), Some("user")))
352 .unwrap();
353 assert!(m.matches(&entity_created("user")));
355 assert!(!m.matches(&entity_created("post")));
356 }
357
358 #[test]
359 fn test_entity_type_filter_ignored_for_link_matcher() {
360 let m =
362 EventMatcher::compile(&trigger("link.created", Some("follows"), Some("user"))).unwrap();
363 assert!(m.matches(&link_created("follows")));
365 assert!(!m.matches(&link_created("likes")));
366 }
367}