refactor (v2): better arch

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-06-07 21:19:54 +02:00
parent 0753f3d256
commit 839308ec19
166 changed files with 8553 additions and 884 deletions

View File

@@ -0,0 +1,11 @@
[package]
name = "event-payload"
version = "0.1.0"
edition = "2024"
[dependencies]
domain = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
uuid = { workspace = true }
thiserror = { workspace = true }

View File

@@ -0,0 +1,86 @@
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use domain::{
errors::DomainError, events::DomainEvent, note::entity::NoteId, user::entity::UserId,
};
/// Wire-format representation of a DomainEvent.
/// Uses primitive types only — no domain newtypes — so it is stable across
/// schema versions and safe to serialize to any transport (NATS, HTTP, file).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", content = "data")]
pub enum EventPayload {
NoteCreated { note_id: String, user_id: String },
NoteUpdated { note_id: String, user_id: String },
NoteDeleted { note_id: String, user_id: String },
}
impl EventPayload {
pub fn event_type(&self) -> &'static str {
match self {
Self::NoteCreated { .. } => "NoteCreated",
Self::NoteUpdated { .. } => "NoteUpdated",
Self::NoteDeleted { .. } => "NoteDeleted",
}
}
pub fn to_json(&self) -> Result<Vec<u8>, DomainError> {
serde_json::to_vec(self)
.map_err(|e| DomainError::Infrastructure(format!("serialize failed: {e}")))
}
pub fn from_json(bytes: &[u8]) -> Result<Self, DomainError> {
serde_json::from_slice(bytes)
.map_err(|e| DomainError::Infrastructure(format!("deserialize failed: {e}")))
}
}
impl From<&DomainEvent> for EventPayload {
fn from(event: &DomainEvent) -> Self {
match event {
DomainEvent::NoteCreated { note_id, user_id } => Self::NoteCreated {
note_id: note_id.as_uuid().to_string(),
user_id: user_id.as_uuid().to_string(),
},
DomainEvent::NoteUpdated { note_id, user_id } => Self::NoteUpdated {
note_id: note_id.as_uuid().to_string(),
user_id: user_id.as_uuid().to_string(),
},
DomainEvent::NoteDeleted { note_id, user_id } => Self::NoteDeleted {
note_id: note_id.as_uuid().to_string(),
user_id: user_id.as_uuid().to_string(),
},
}
}
}
impl TryFrom<EventPayload> for DomainEvent {
type Error = DomainError;
fn try_from(payload: EventPayload) -> Result<Self, Self::Error> {
fn parse(s: &str) -> Result<Uuid, DomainError> {
Uuid::parse_str(s)
.map_err(|e| DomainError::Infrastructure(format!("invalid uuid '{s}': {e}")))
}
match payload {
EventPayload::NoteCreated { note_id, user_id } => Ok(DomainEvent::NoteCreated {
note_id: NoteId::from_uuid(parse(&note_id)?),
user_id: UserId::from_uuid(parse(&user_id)?),
}),
EventPayload::NoteUpdated { note_id, user_id } => Ok(DomainEvent::NoteUpdated {
note_id: NoteId::from_uuid(parse(&note_id)?),
user_id: UserId::from_uuid(parse(&user_id)?),
}),
EventPayload::NoteDeleted { note_id, user_id } => Ok(DomainEvent::NoteDeleted {
note_id: NoteId::from_uuid(parse(&note_id)?),
user_id: UserId::from_uuid(parse(&user_id)?),
}),
}
}
}
#[cfg(test)]
#[path = "tests/lib.rs"]
mod tests;

View File

@@ -0,0 +1,88 @@
use domain::{events::DomainEvent, note::entity::NoteId, user::entity::UserId};
use crate::EventPayload;
fn note_created() -> DomainEvent {
DomainEvent::NoteCreated {
note_id: NoteId::new(),
user_id: UserId::new(),
}
}
#[test]
fn domain_event_round_trips_through_payload() {
let event = note_created();
let payload = EventPayload::from(&event);
let recovered = DomainEvent::try_from(payload).unwrap();
// Compare by serialising both — DomainEvent doesn't implement PartialEq.
let EventPayload::NoteCreated {
note_id: orig_nid,
user_id: orig_uid,
} = EventPayload::from(&event)
else {
panic!("wrong variant");
};
let EventPayload::NoteCreated {
note_id: rec_nid,
user_id: rec_uid,
} = EventPayload::from(&recovered)
else {
panic!("wrong variant");
};
assert_eq!(orig_nid, rec_nid);
assert_eq!(orig_uid, rec_uid);
}
#[test]
fn payload_serialises_to_json_and_back() {
let event = note_created();
let payload = EventPayload::from(&event);
let bytes = payload.to_json().unwrap();
let recovered = EventPayload::from_json(&bytes).unwrap();
assert_eq!(payload, recovered);
}
#[test]
fn event_type_label_is_correct() {
let uid = UserId::new();
let nid = NoteId::new();
assert_eq!(
EventPayload::NoteCreated {
note_id: nid.to_string(),
user_id: uid.to_string()
}
.event_type(),
"NoteCreated"
);
assert_eq!(
EventPayload::NoteUpdated {
note_id: nid.to_string(),
user_id: uid.to_string()
}
.event_type(),
"NoteUpdated"
);
assert_eq!(
EventPayload::NoteDeleted {
note_id: nid.to_string(),
user_id: uid.to_string()
}
.event_type(),
"NoteDeleted"
);
}
#[test]
fn invalid_json_returns_error() {
assert!(EventPayload::from_json(b"not json at all").is_err());
}
#[test]
fn invalid_uuid_in_payload_returns_error() {
let payload = EventPayload::NoteCreated {
note_id: "not-a-uuid".into(),
user_id: "also-not-a-uuid".into(),
};
assert!(DomainEvent::try_from(payload).is_err());
}