From 57232705fe1da898ccb79472c6927bc9788f10cf Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Thu, 14 May 2026 09:48:58 +0200 Subject: [PATCH] feat(event-payload): serializable NATS event payload types --- Cargo.toml | 2 + crates/adapters/event-payload/Cargo.toml | 4 + crates/adapters/event-payload/src/lib.rs | 118 +++++++++++++++++++++++ 3 files changed, 124 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 4187405..677432e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,8 @@ axum = { version = "0.8", features = ["macros"] } tower-http = { version = "0.6", features = ["cors", "trace"] } futures = "0.3" dotenvy = "0.15" +async-nats = "0.38" +async-stream = "0.3" domain = { path = "crates/domain" } application = { path = "crates/application" } diff --git a/crates/adapters/event-payload/Cargo.toml b/crates/adapters/event-payload/Cargo.toml index 1057e83..ae5332c 100644 --- a/crates/adapters/event-payload/Cargo.toml +++ b/crates/adapters/event-payload/Cargo.toml @@ -2,3 +2,7 @@ name = "event-payload" version = "0.1.0" edition = "2021" + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } diff --git a/crates/adapters/event-payload/src/lib.rs b/crates/adapters/event-payload/src/lib.rs index e69de29..a2a4b5f 100644 --- a/crates/adapters/event-payload/src/lib.rs +++ b/crates/adapters/event-payload/src/lib.rs @@ -0,0 +1,118 @@ +use serde::{Deserialize, Serialize}; + +/// Serializable mirror of domain::events::DomainEvent. +/// All IDs are Strings (UUID hex) — no domain type dependencies. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "data")] +pub enum EventPayload { + ThoughtCreated { + thought_id: String, + user_id: String, + in_reply_to_id: Option, + }, + ThoughtDeleted { + thought_id: String, + user_id: String, + }, + ThoughtUpdated { + thought_id: String, + user_id: String, + }, + LikeAdded { + like_id: String, + user_id: String, + thought_id: String, + }, + LikeRemoved { + user_id: String, + thought_id: String, + }, + BoostAdded { + boost_id: String, + user_id: String, + thought_id: String, + }, + BoostRemoved { + user_id: String, + thought_id: String, + }, + FollowRequested { + follower_id: String, + following_id: String, + }, + FollowAccepted { + follower_id: String, + following_id: String, + }, + FollowRejected { + follower_id: String, + following_id: String, + }, + Unfollowed { + follower_id: String, + following_id: String, + }, + UserBlocked { + blocker_id: String, + blocked_id: String, + }, +} + +impl EventPayload { + /// Returns the NATS subject for this event. + pub fn subject(&self) -> &'static str { + match self { + Self::ThoughtCreated { .. } => "thoughts.created", + Self::ThoughtDeleted { .. } => "thoughts.deleted", + Self::ThoughtUpdated { .. } => "thoughts.updated", + Self::LikeAdded { .. } => "likes.added", + Self::LikeRemoved { .. } => "likes.removed", + Self::BoostAdded { .. } => "boosts.added", + Self::BoostRemoved { .. } => "boosts.removed", + Self::FollowRequested { .. } => "follows.requested", + Self::FollowAccepted { .. } => "follows.accepted", + Self::FollowRejected { .. } => "follows.rejected", + Self::Unfollowed { .. } => "follows.removed", + Self::UserBlocked { .. } => "users.blocked", + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn thought_created_roundtrip() { + let p = EventPayload::ThoughtCreated { + thought_id: "abc".into(), + user_id: "def".into(), + in_reply_to_id: None, + }; + let json = serde_json::to_string(&p).unwrap(); + let back: EventPayload = serde_json::from_str(&json).unwrap(); + assert_eq!(back.subject(), "thoughts.created"); + } + + #[test] + fn all_subjects_are_unique() { + let samples: &[EventPayload] = &[ + EventPayload::ThoughtCreated { thought_id: "a".into(), user_id: "b".into(), in_reply_to_id: None }, + EventPayload::ThoughtDeleted { thought_id: "a".into(), user_id: "b".into() }, + EventPayload::ThoughtUpdated { thought_id: "a".into(), user_id: "b".into() }, + EventPayload::LikeAdded { like_id: "a".into(), user_id: "b".into(), thought_id: "c".into() }, + EventPayload::LikeRemoved { user_id: "b".into(), thought_id: "c".into() }, + EventPayload::BoostAdded { boost_id: "a".into(), user_id: "b".into(), thought_id: "c".into() }, + EventPayload::BoostRemoved { user_id: "b".into(), thought_id: "c".into() }, + EventPayload::FollowRequested { follower_id: "a".into(), following_id: "b".into() }, + EventPayload::FollowAccepted { follower_id: "a".into(), following_id: "b".into() }, + EventPayload::FollowRejected { follower_id: "a".into(), following_id: "b".into() }, + EventPayload::Unfollowed { follower_id: "a".into(), following_id: "b".into() }, + EventPayload::UserBlocked { blocker_id: "a".into(), blocked_id: "b".into() }, + ]; + let mut subjects: Vec<&str> = samples.iter().map(|p| p.subject()).collect(); + subjects.sort(); + subjects.dedup(); + assert_eq!(subjects.len(), samples.len(), "each event must have a unique subject"); + } +}