allowthem_core/
event_sink.rs1use std::future::Future;
2use std::pin::Pin;
3
4use chrono::{DateTime, Utc};
5use serde::Serialize;
6use serde_json::Value;
7use uuid::Uuid;
8
9use crate::types::UserId;
10
11#[derive(Debug, Clone, Serialize)]
23pub struct AuthEvent {
24 pub event_id: Uuid,
25 pub event_type: String,
26 pub user_id: Option<UserId>,
27 pub timestamp: DateTime<Utc>,
28 pub data: Value,
29}
30
31impl AuthEvent {
32 pub fn new(event_type: impl Into<String>, user_id: Option<UserId>, data: Value) -> Self {
35 Self {
36 event_id: Uuid::now_v7(),
37 event_type: event_type.into(),
38 user_id,
39 timestamp: Utc::now(),
40 data,
41 }
42 }
43}
44
45pub trait EventSink: Send + Sync {
62 fn emit<'a>(&'a self, event: &'a AuthEvent) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>>;
63}
64
65pub struct NoopEventSink;
70
71impl EventSink for NoopEventSink {
72 fn emit<'a>(&'a self, _event: &'a AuthEvent) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
73 Box::pin(std::future::ready(()))
74 }
75}
76
77pub struct LoggingEventSink;
82
83impl EventSink for LoggingEventSink {
84 fn emit<'a>(&'a self, event: &'a AuthEvent) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
85 tracing::debug!(
86 event_type = %event.event_type,
87 user_id = ?event.user_id,
88 data = %event.data,
89 "auth event",
90 );
91 Box::pin(std::future::ready(()))
92 }
93}
94
95impl<T: EventSink + ?Sized> EventSink for std::sync::Arc<T> {
97 fn emit<'a>(&'a self, event: &'a AuthEvent) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
98 (**self).emit(event)
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 fn _assert_object_safe(_: &dyn EventSink) {}
108
109 fn sample_event() -> AuthEvent {
110 AuthEvent::new("user.created", None, serde_json::json!({}))
111 }
112
113 #[tokio::test]
114 async fn noop_sink_returns_immediately() {
115 NoopEventSink.emit(&sample_event()).await;
116 }
117
118 #[tokio::test]
119 async fn logging_sink_returns_immediately() {
120 LoggingEventSink.emit(&sample_event()).await;
121 }
122
123 #[tokio::test]
124 async fn arc_dispatch_works() {
125 let sink: std::sync::Arc<dyn EventSink> = std::sync::Arc::new(NoopEventSink);
126 sink.emit(&sample_event()).await;
127 }
128
129 #[tokio::test]
130 async fn auth_event_new_stamps_timestamp() {
131 let before = Utc::now();
132 let event = AuthEvent::new("test", None, serde_json::json!({"k": "v"}));
133 let after = Utc::now();
134 assert!(event.timestamp >= before);
135 assert!(event.timestamp <= after);
136 assert_eq!(event.event_type, "test");
137 assert!(event.user_id.is_none());
138 }
139
140 #[tokio::test]
141 async fn auth_event_new_assigns_distinct_non_nil_event_ids() {
142 let a = AuthEvent::new("test", None, serde_json::json!({}));
143 let b = AuthEvent::new("test", None, serde_json::json!({}));
144 assert_ne!(a.event_id, Uuid::nil());
145 assert_ne!(b.event_id, Uuid::nil());
146 assert_ne!(a.event_id, b.event_id);
147 }
148
149 #[tokio::test]
150 async fn auth_event_serializes_event_id_field() {
151 let event = AuthEvent::new("test", None, serde_json::json!({}));
152 let json = serde_json::to_value(&event).unwrap();
153 assert_eq!(
154 json["event_id"].as_str().unwrap(),
155 event.event_id.to_string()
156 );
157 }
158}