refactor (v2): better arch
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
11
crates/adapters/event-payload/Cargo.toml
Normal file
11
crates/adapters/event-payload/Cargo.toml
Normal 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 }
|
||||
86
crates/adapters/event-payload/src/lib.rs
Normal file
86
crates/adapters/event-payload/src/lib.rs
Normal 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(¬e_id)?),
|
||||
user_id: UserId::from_uuid(parse(&user_id)?),
|
||||
}),
|
||||
EventPayload::NoteUpdated { note_id, user_id } => Ok(DomainEvent::NoteUpdated {
|
||||
note_id: NoteId::from_uuid(parse(¬e_id)?),
|
||||
user_id: UserId::from_uuid(parse(&user_id)?),
|
||||
}),
|
||||
EventPayload::NoteDeleted { note_id, user_id } => Ok(DomainEvent::NoteDeleted {
|
||||
note_id: NoteId::from_uuid(parse(¬e_id)?),
|
||||
user_id: UserId::from_uuid(parse(&user_id)?),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/lib.rs"]
|
||||
mod tests;
|
||||
88
crates/adapters/event-payload/src/tests/lib.rs
Normal file
88
crates/adapters/event-payload/src/tests/lib.rs
Normal 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());
|
||||
}
|
||||
Reference in New Issue
Block a user