code refactor
Some checks failed
CI / Check / Test (push) Has been cancelled

Reviewed-on: #26
This commit was merged in pull request #26.
This commit is contained in:
2026-10-26 09:36:23 +00:00
parent 2ce7db12c2
commit 017b58d6fa
804 changed files with 33423 additions and 28955 deletions

View File

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

View File

@@ -0,0 +1,728 @@
use chrono::NaiveDateTime;
use domain::{
errors::DomainError,
events::DomainEvent,
models::{ExternalPersonId, PersonId},
value_objects::{
ExternalMetadataId, GoalId, MovieId, PosterPath, Rating, ReviewId, SocialIdentity, UserId,
WrapUpId,
},
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", content = "data")]
pub enum EventPayload {
ReviewLogged {
review_id: String,
movie_id: String,
user_id: String,
rating: u8,
watched_at: i64,
},
ReviewUpdated {
review_id: String,
movie_id: String,
user_id: String,
rating: u8,
watched_at: i64,
},
MovieDiscovered {
movie_id: String,
external_metadata_id: String,
},
MovieDeleted {
movie_id: String,
poster_path: Option<String>,
},
UserUpdated {
user_id: String,
},
ReviewDeleted {
review_id: String,
user_id: String,
},
MovieEnrichmentRequested {
movie_id: String,
external_metadata_id: String,
},
ImageStored {
key: String,
},
WatchlistEntryAdded {
user_id: String,
movie_id: String,
movie_title: String,
release_year: u16,
external_metadata_id: Option<String>,
added_at: i64,
},
WatchlistEntryRemoved {
user_id: String,
movie_id: String,
},
FollowRequested {
follower_id: String,
target_kind: String,
target_id: String,
},
FollowAccepted {
owner_id: String,
requester_kind: String,
requester_id: String,
},
FollowRejected {
owner_id: String,
requester_kind: String,
requester_id: String,
},
Unfollowed {
follower_id: String,
target_kind: String,
target_id: String,
},
FollowerRemoved {
owner_id: String,
follower_kind: String,
follower_id: String,
},
ActorBlocked {
blocker_id: String,
target_kind: String,
target_id: String,
},
ActorUnblocked {
blocker_id: String,
target_kind: String,
target_id: String,
},
BackfillFollower {
owner_user_id: String,
follower_inbox_url: String,
},
FederationDeliveryRequested {
inbox_url: String,
activity_json: serde_json::Value,
signing_actor_id: String,
},
WatchEventIngested {
user_id: String,
title: String,
source: String,
},
WrapUpRequested {
wrapup_id: String,
user_id: Option<String>,
start_date: String,
end_date: String,
},
WrapUpCompleted {
wrapup_id: String,
},
SearchReindexRequested,
PosterSynced {
movie_id: String,
},
GoalCreated {
goal_id: String,
user_id: String,
year: u16,
target_count: u32,
},
GoalUpdated {
goal_id: String,
user_id: String,
year: u16,
target_count: u32,
},
GoalDeleted {
goal_id: String,
user_id: String,
year: u16,
},
PersonEnrichmentRequested {
person_id: String,
external_person_id: String,
},
UserDeleted {
user_id: String,
},
UserAccountMoved {
user_id: String,
new_actor_url: String,
},
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct EventMessage {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub event_id: Option<String>,
#[serde(flatten)]
pub payload: EventPayload,
}
impl EventPayload {
pub fn event_type(&self) -> &'static str {
match self {
EventPayload::ReviewLogged { .. } => "ReviewLogged",
EventPayload::ReviewUpdated { .. } => "ReviewUpdated",
EventPayload::MovieDiscovered { .. } => "MovieDiscovered",
EventPayload::MovieDeleted { .. } => "MovieDeleted",
EventPayload::UserUpdated { .. } => "UserUpdated",
EventPayload::ReviewDeleted { .. } => "ReviewDeleted",
EventPayload::MovieEnrichmentRequested { .. } => "MovieEnrichmentRequested",
EventPayload::ImageStored { .. } => "ImageStored",
EventPayload::WatchlistEntryAdded { .. } => "WatchlistEntryAdded",
EventPayload::WatchlistEntryRemoved { .. } => "WatchlistEntryRemoved",
EventPayload::FollowRequested { .. } => "FollowRequested",
EventPayload::FollowAccepted { .. } => "FollowAccepted",
EventPayload::FollowRejected { .. } => "FollowRejected",
EventPayload::Unfollowed { .. } => "Unfollowed",
EventPayload::FollowerRemoved { .. } => "FollowerRemoved",
EventPayload::ActorBlocked { .. } => "ActorBlocked",
EventPayload::ActorUnblocked { .. } => "ActorUnblocked",
EventPayload::BackfillFollower { .. } => "BackfillFollower",
EventPayload::FederationDeliveryRequested { .. } => "FederationDeliveryRequested",
EventPayload::WatchEventIngested { .. } => "WatchEventIngested",
EventPayload::WrapUpRequested { .. } => "WrapUpRequested",
EventPayload::WrapUpCompleted { .. } => "WrapUpCompleted",
EventPayload::SearchReindexRequested => "SearchReindexRequested",
EventPayload::PosterSynced { .. } => "PosterSynced",
EventPayload::GoalCreated { .. } => "GoalCreated",
EventPayload::GoalUpdated { .. } => "GoalUpdated",
EventPayload::GoalDeleted { .. } => "GoalDeleted",
EventPayload::PersonEnrichmentRequested { .. } => "PersonEnrichmentRequested",
EventPayload::UserDeleted { .. } => "UserDeleted",
EventPayload::UserAccountMoved { .. } => "UserAccountMoved",
}
}
}
fn parse_uuid(s: &str, field: &str) -> Result<Uuid, DomainError> {
Uuid::parse_str(s).map_err(|e| DomainError::InfrastructureError(format!("{field}: {e}")))
}
fn identity_to_payload(id: &SocialIdentity) -> (String, String) {
match id {
SocialIdentity::Local(uid) => ("local".into(), uid.value().to_string()),
SocialIdentity::Remote { actor_url } => ("remote".into(), actor_url.clone()),
}
}
fn follow_target_to_payload(target: &domain::value_objects::FollowTarget) -> (String, String) {
match target {
domain::value_objects::FollowTarget::Identity(id) => identity_to_payload(id),
domain::value_objects::FollowTarget::Handle(h) => ("handle".into(), h.clone()),
}
}
fn payload_to_identity(kind: &str, id: String) -> Result<SocialIdentity, DomainError> {
match kind {
"local" => Ok(SocialIdentity::Local(UserId::from_uuid(parse_uuid(
&id, "user_id",
)?))),
"remote" => Ok(SocialIdentity::Remote { actor_url: id }),
other => Err(DomainError::InfrastructureError(format!(
"unknown identity kind: {other}"
))),
}
}
fn payload_to_follow_target(
kind: &str,
id: String,
) -> Result<domain::value_objects::FollowTarget, DomainError> {
match kind {
"handle" => Ok(domain::value_objects::FollowTarget::Handle(id)),
other => Ok(domain::value_objects::FollowTarget::Identity(
payload_to_identity(other, id)?,
)),
}
}
fn parse_ts(ts: i64) -> Result<NaiveDateTime, DomainError> {
chrono::DateTime::from_timestamp(ts, 0)
.map(|dt| dt.naive_utc())
.ok_or_else(|| DomainError::InfrastructureError(format!("invalid timestamp: {ts}")))
}
impl From<&DomainEvent> for EventPayload {
fn from(event: &DomainEvent) -> Self {
match event {
DomainEvent::ReviewLogged {
review_id,
movie_id,
user_id,
rating,
watched_at,
} => EventPayload::ReviewLogged {
review_id: review_id.value().to_string(),
movie_id: movie_id.value().to_string(),
user_id: user_id.value().to_string(),
rating: rating.value(),
watched_at: watched_at.and_utc().timestamp(),
},
DomainEvent::ReviewUpdated {
review_id,
movie_id,
user_id,
rating,
watched_at,
} => EventPayload::ReviewUpdated {
review_id: review_id.value().to_string(),
movie_id: movie_id.value().to_string(),
user_id: user_id.value().to_string(),
rating: rating.value(),
watched_at: watched_at.and_utc().timestamp(),
},
DomainEvent::MovieDiscovered {
movie_id,
external_metadata_id,
} => EventPayload::MovieDiscovered {
movie_id: movie_id.value().to_string(),
external_metadata_id: external_metadata_id.value().to_owned(),
},
DomainEvent::MovieDeleted {
movie_id,
poster_path,
} => EventPayload::MovieDeleted {
movie_id: movie_id.value().to_string(),
poster_path: poster_path.as_ref().map(|p| p.value().to_string()),
},
DomainEvent::UserUpdated { user_id } => EventPayload::UserUpdated {
user_id: user_id.value().to_string(),
},
DomainEvent::ReviewDeleted { review_id, user_id } => EventPayload::ReviewDeleted {
review_id: review_id.value().to_string(),
user_id: user_id.value().to_string(),
},
DomainEvent::MovieEnrichmentRequested {
movie_id,
external_metadata_id,
} => EventPayload::MovieEnrichmentRequested {
movie_id: movie_id.value().to_string(),
external_metadata_id: external_metadata_id.value().to_string(),
},
DomainEvent::ImageStored { key } => EventPayload::ImageStored { key: key.clone() },
DomainEvent::WatchlistEntryAdded {
user_id,
movie_id,
movie_title,
release_year,
external_metadata_id,
added_at,
} => EventPayload::WatchlistEntryAdded {
user_id: user_id.value().to_string(),
movie_id: movie_id.value().to_string(),
movie_title: movie_title.clone(),
release_year: *release_year,
external_metadata_id: external_metadata_id.clone(),
added_at: added_at.and_utc().timestamp(),
},
DomainEvent::WatchlistEntryRemoved { user_id, movie_id } => {
EventPayload::WatchlistEntryRemoved {
user_id: user_id.value().to_string(),
movie_id: movie_id.value().to_string(),
}
}
DomainEvent::FollowRequested { follower, target } => {
let (kind, id) = follow_target_to_payload(target);
EventPayload::FollowRequested {
follower_id: follower.value().to_string(),
target_kind: kind,
target_id: id,
}
}
DomainEvent::FollowAccepted { owner, requester } => {
let (kind, id) = identity_to_payload(requester);
EventPayload::FollowAccepted {
owner_id: owner.value().to_string(),
requester_kind: kind,
requester_id: id,
}
}
DomainEvent::FollowRejected { owner, requester } => {
let (kind, id) = identity_to_payload(requester);
EventPayload::FollowRejected {
owner_id: owner.value().to_string(),
requester_kind: kind,
requester_id: id,
}
}
DomainEvent::Unfollowed { follower, target } => {
let (kind, id) = identity_to_payload(target);
EventPayload::Unfollowed {
follower_id: follower.value().to_string(),
target_kind: kind,
target_id: id,
}
}
DomainEvent::FollowerRemoved { owner, follower } => {
let (kind, id) = identity_to_payload(follower);
EventPayload::FollowerRemoved {
owner_id: owner.value().to_string(),
follower_kind: kind,
follower_id: id,
}
}
DomainEvent::ActorBlocked { blocker, target } => {
let (kind, id) = identity_to_payload(target);
EventPayload::ActorBlocked {
blocker_id: blocker.value().to_string(),
target_kind: kind,
target_id: id,
}
}
DomainEvent::ActorUnblocked { blocker, target } => {
let (kind, id) = identity_to_payload(target);
EventPayload::ActorUnblocked {
blocker_id: blocker.value().to_string(),
target_kind: kind,
target_id: id,
}
}
DomainEvent::BackfillFollower {
owner_user_id,
follower_inbox_url,
} => EventPayload::BackfillFollower {
owner_user_id: owner_user_id.value().to_string(),
follower_inbox_url: follower_inbox_url.clone(),
},
DomainEvent::FederationDeliveryRequested {
inbox_url,
activity_json,
signing_actor_id,
} => EventPayload::FederationDeliveryRequested {
inbox_url: inbox_url.clone(),
activity_json: activity_json.clone(),
signing_actor_id: signing_actor_id.to_string(),
},
DomainEvent::WatchEventIngested {
user_id,
title,
source,
} => EventPayload::WatchEventIngested {
user_id: user_id.value().to_string(),
title: title.clone(),
source: source.clone(),
},
DomainEvent::WrapUpRequested {
wrapup_id,
user_id,
start_date,
end_date,
} => EventPayload::WrapUpRequested {
wrapup_id: wrapup_id.value().to_string(),
user_id: user_id.as_ref().map(|u| u.value().to_string()),
start_date: start_date.to_string(),
end_date: end_date.to_string(),
},
DomainEvent::WrapUpCompleted { wrapup_id } => EventPayload::WrapUpCompleted {
wrapup_id: wrapup_id.value().to_string(),
},
DomainEvent::SearchReindexRequested => EventPayload::SearchReindexRequested,
DomainEvent::PosterSynced { movie_id } => EventPayload::PosterSynced {
movie_id: movie_id.value().to_string(),
},
DomainEvent::GoalCreated {
goal_id,
user_id,
year,
target_count,
} => EventPayload::GoalCreated {
goal_id: goal_id.value().to_string(),
user_id: user_id.value().to_string(),
year: *year,
target_count: *target_count,
},
DomainEvent::GoalUpdated {
goal_id,
user_id,
year,
target_count,
} => EventPayload::GoalUpdated {
goal_id: goal_id.value().to_string(),
user_id: user_id.value().to_string(),
year: *year,
target_count: *target_count,
},
DomainEvent::GoalDeleted {
goal_id,
user_id,
year,
} => EventPayload::GoalDeleted {
goal_id: goal_id.value().to_string(),
user_id: user_id.value().to_string(),
year: *year,
},
DomainEvent::PersonEnrichmentRequested {
person_id,
external_person_id,
} => EventPayload::PersonEnrichmentRequested {
person_id: person_id.value().to_string(),
external_person_id: external_person_id.value().to_string(),
},
DomainEvent::UserDeleted { user_id } => EventPayload::UserDeleted {
user_id: user_id.value().to_string(),
},
DomainEvent::UserAccountMoved {
user_id,
new_actor_url,
} => EventPayload::UserAccountMoved {
user_id: user_id.value().to_string(),
new_actor_url: new_actor_url.clone(),
},
}
}
}
impl TryFrom<EventPayload> for DomainEvent {
type Error = DomainError;
fn try_from(payload: EventPayload) -> Result<Self, DomainError> {
match payload {
EventPayload::ReviewLogged {
review_id,
movie_id,
user_id,
rating,
watched_at,
} => Ok(DomainEvent::ReviewLogged {
review_id: ReviewId::from_uuid(parse_uuid(&review_id, "review_id")?),
movie_id: MovieId::from_uuid(parse_uuid(&movie_id, "movie_id")?),
user_id: UserId::from_uuid(parse_uuid(&user_id, "user_id")?),
rating: Rating::new(rating)?,
watched_at: parse_ts(watched_at)?,
}),
EventPayload::ReviewUpdated {
review_id,
movie_id,
user_id,
rating,
watched_at,
} => Ok(DomainEvent::ReviewUpdated {
review_id: ReviewId::from_uuid(parse_uuid(&review_id, "review_id")?),
movie_id: MovieId::from_uuid(parse_uuid(&movie_id, "movie_id")?),
user_id: UserId::from_uuid(parse_uuid(&user_id, "user_id")?),
rating: Rating::new(rating)?,
watched_at: parse_ts(watched_at)?,
}),
EventPayload::MovieDiscovered {
movie_id,
external_metadata_id,
} => Ok(DomainEvent::MovieDiscovered {
movie_id: MovieId::from_uuid(parse_uuid(&movie_id, "movie_id")?),
external_metadata_id: ExternalMetadataId::new(external_metadata_id)?,
}),
EventPayload::MovieDeleted {
movie_id,
poster_path,
} => {
let movie_id = MovieId::from_uuid(parse_uuid(&movie_id, "movie_id")?);
let poster_path = poster_path
.map(PosterPath::new)
.transpose()
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
Ok(DomainEvent::MovieDeleted {
movie_id,
poster_path,
})
}
EventPayload::UserUpdated { user_id } => Ok(DomainEvent::UserUpdated {
user_id: UserId::from_uuid(parse_uuid(&user_id, "user_id")?),
}),
EventPayload::ReviewDeleted { review_id, user_id } => Ok(DomainEvent::ReviewDeleted {
review_id: ReviewId::from_uuid(parse_uuid(&review_id, "review_id")?),
user_id: UserId::from_uuid(parse_uuid(&user_id, "user_id")?),
}),
EventPayload::MovieEnrichmentRequested {
movie_id,
external_metadata_id,
} => Ok(DomainEvent::MovieEnrichmentRequested {
movie_id: MovieId::from_uuid(parse_uuid(&movie_id, "movie_id")?),
external_metadata_id: ExternalMetadataId::new(external_metadata_id)
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
}),
EventPayload::ImageStored { key } => Ok(DomainEvent::ImageStored { key }),
EventPayload::WatchlistEntryAdded {
user_id,
movie_id,
movie_title,
release_year,
external_metadata_id,
added_at,
} => Ok(DomainEvent::WatchlistEntryAdded {
user_id: UserId::from_uuid(parse_uuid(&user_id, "user_id")?),
movie_id: MovieId::from_uuid(parse_uuid(&movie_id, "movie_id")?),
movie_title,
release_year,
external_metadata_id,
added_at: parse_ts(added_at)?,
}),
EventPayload::WatchlistEntryRemoved { user_id, movie_id } => {
Ok(DomainEvent::WatchlistEntryRemoved {
user_id: UserId::from_uuid(parse_uuid(&user_id, "user_id")?),
movie_id: MovieId::from_uuid(parse_uuid(&movie_id, "movie_id")?),
})
}
EventPayload::FollowRequested {
follower_id,
target_kind,
target_id,
} => Ok(DomainEvent::FollowRequested {
follower: UserId::from_uuid(parse_uuid(&follower_id, "follower_id")?),
target: payload_to_follow_target(&target_kind, target_id)?,
}),
EventPayload::FollowAccepted {
owner_id,
requester_kind,
requester_id,
} => Ok(DomainEvent::FollowAccepted {
owner: UserId::from_uuid(parse_uuid(&owner_id, "owner_id")?),
requester: payload_to_identity(&requester_kind, requester_id)?,
}),
EventPayload::FollowRejected {
owner_id,
requester_kind,
requester_id,
} => Ok(DomainEvent::FollowRejected {
owner: UserId::from_uuid(parse_uuid(&owner_id, "owner_id")?),
requester: payload_to_identity(&requester_kind, requester_id)?,
}),
EventPayload::Unfollowed {
follower_id,
target_kind,
target_id,
} => Ok(DomainEvent::Unfollowed {
follower: UserId::from_uuid(parse_uuid(&follower_id, "follower_id")?),
target: payload_to_identity(&target_kind, target_id)?,
}),
EventPayload::FollowerRemoved {
owner_id,
follower_kind,
follower_id,
} => Ok(DomainEvent::FollowerRemoved {
owner: UserId::from_uuid(parse_uuid(&owner_id, "owner_id")?),
follower: payload_to_identity(&follower_kind, follower_id)?,
}),
EventPayload::ActorBlocked {
blocker_id,
target_kind,
target_id,
} => Ok(DomainEvent::ActorBlocked {
blocker: UserId::from_uuid(parse_uuid(&blocker_id, "blocker_id")?),
target: payload_to_identity(&target_kind, target_id)?,
}),
EventPayload::ActorUnblocked {
blocker_id,
target_kind,
target_id,
} => Ok(DomainEvent::ActorUnblocked {
blocker: UserId::from_uuid(parse_uuid(&blocker_id, "blocker_id")?),
target: payload_to_identity(&target_kind, target_id)?,
}),
EventPayload::BackfillFollower {
owner_user_id,
follower_inbox_url,
} => Ok(DomainEvent::BackfillFollower {
owner_user_id: UserId::from_uuid(parse_uuid(&owner_user_id, "owner_user_id")?),
follower_inbox_url,
}),
EventPayload::FederationDeliveryRequested {
inbox_url,
activity_json,
signing_actor_id,
} => Ok(DomainEvent::FederationDeliveryRequested {
inbox_url,
activity_json,
signing_actor_id: parse_uuid(&signing_actor_id, "signing_actor_id")?,
}),
EventPayload::WatchEventIngested {
user_id,
title,
source,
} => Ok(DomainEvent::WatchEventIngested {
user_id: UserId::from_uuid(parse_uuid(&user_id, "user_id")?),
title,
source,
}),
EventPayload::WrapUpRequested {
wrapup_id,
user_id,
start_date,
end_date,
} => {
let wid = parse_uuid(&wrapup_id, "wrapup_id")?;
let uid = user_id.map(|s| parse_uuid(&s, "user_id")).transpose()?;
let sd = chrono::NaiveDate::parse_from_str(&start_date, "%Y-%m-%d")
.map_err(|e| DomainError::ValidationError(e.to_string()))?;
let ed = chrono::NaiveDate::parse_from_str(&end_date, "%Y-%m-%d")
.map_err(|e| DomainError::ValidationError(e.to_string()))?;
Ok(DomainEvent::WrapUpRequested {
wrapup_id: WrapUpId::from_uuid(wid),
user_id: uid.map(UserId::from_uuid),
start_date: sd,
end_date: ed,
})
}
EventPayload::WrapUpCompleted { wrapup_id } => {
let wid = parse_uuid(&wrapup_id, "wrapup_id")?;
Ok(DomainEvent::WrapUpCompleted {
wrapup_id: WrapUpId::from_uuid(wid),
})
}
EventPayload::SearchReindexRequested => Ok(DomainEvent::SearchReindexRequested),
EventPayload::PosterSynced { movie_id } => Ok(DomainEvent::PosterSynced {
movie_id: MovieId::from_uuid(parse_uuid(&movie_id, "movie_id")?),
}),
EventPayload::GoalCreated {
goal_id,
user_id,
year,
target_count,
} => Ok(DomainEvent::GoalCreated {
goal_id: GoalId::from_uuid(parse_uuid(&goal_id, "goal_id")?),
user_id: UserId::from_uuid(parse_uuid(&user_id, "user_id")?),
year,
target_count,
}),
EventPayload::GoalUpdated {
goal_id,
user_id,
year,
target_count,
} => Ok(DomainEvent::GoalUpdated {
goal_id: GoalId::from_uuid(parse_uuid(&goal_id, "goal_id")?),
user_id: UserId::from_uuid(parse_uuid(&user_id, "user_id")?),
year,
target_count,
}),
EventPayload::GoalDeleted {
goal_id,
user_id,
year,
} => Ok(DomainEvent::GoalDeleted {
goal_id: GoalId::from_uuid(parse_uuid(&goal_id, "goal_id")?),
user_id: UserId::from_uuid(parse_uuid(&user_id, "user_id")?),
year,
}),
EventPayload::PersonEnrichmentRequested {
person_id,
external_person_id,
} => Ok(DomainEvent::PersonEnrichmentRequested {
person_id: PersonId::from_uuid(parse_uuid(&person_id, "person_id")?),
external_person_id: ExternalPersonId::new(external_person_id),
}),
EventPayload::UserDeleted { user_id } => Ok(DomainEvent::UserDeleted {
user_id: UserId::from_uuid(parse_uuid(&user_id, "user_id")?),
}),
EventPayload::UserAccountMoved {
user_id,
new_actor_url,
} => Ok(DomainEvent::UserAccountMoved {
user_id: UserId::from_uuid(parse_uuid(&user_id, "user_id")?),
new_actor_url,
}),
}
}
}

View File

@@ -0,0 +1,321 @@
use chrono::NaiveDateTime;
use domain::{
events::DomainEvent,
value_objects::{ExternalMetadataId, MovieId, Rating, ReviewId, UserId},
};
use event_payload::EventPayload;
use uuid::Uuid;
fn fixed_dt() -> NaiveDateTime {
chrono::DateTime::from_timestamp(1_700_000_000, 0)
.unwrap()
.naive_utc()
}
fn review_logged() -> DomainEvent {
DomainEvent::ReviewLogged {
review_id: ReviewId::from_uuid(Uuid::new_v4()),
movie_id: MovieId::from_uuid(Uuid::new_v4()),
user_id: UserId::from_uuid(Uuid::new_v4()),
rating: Rating::new(4).unwrap(),
watched_at: fixed_dt(),
}
}
fn review_updated() -> DomainEvent {
DomainEvent::ReviewUpdated {
review_id: ReviewId::from_uuid(Uuid::new_v4()),
movie_id: MovieId::from_uuid(Uuid::new_v4()),
user_id: UserId::from_uuid(Uuid::new_v4()),
rating: Rating::new(3).unwrap(),
watched_at: fixed_dt(),
}
}
fn movie_discovered() -> DomainEvent {
DomainEvent::MovieDiscovered {
movie_id: MovieId::from_uuid(Uuid::new_v4()),
external_metadata_id: ExternalMetadataId::new("tt1234567".into()).unwrap(),
}
}
fn round_trip(event: DomainEvent) {
let payload = EventPayload::from(&event);
let json = serde_json::to_string(&payload).expect("serialize");
let back: EventPayload = serde_json::from_str(&json).expect("deserialize");
let recovered = DomainEvent::try_from(back).expect("try_from");
assert_eq!(EventPayload::from(&event), EventPayload::from(&recovered));
}
#[test]
fn round_trip_review_logged() {
round_trip(review_logged());
}
#[test]
fn round_trip_review_updated() {
round_trip(review_updated());
}
#[test]
fn round_trip_movie_discovered() {
round_trip(movie_discovered());
}
#[test]
fn serialized_format_is_tagged() {
let payload = EventPayload::from(&movie_discovered());
let json = serde_json::to_string(&payload).unwrap();
assert!(json.contains(r#""type":"MovieDiscovered""#));
assert!(json.contains(r#""data":"#));
}
#[test]
fn event_type_strings() {
assert_eq!(
EventPayload::from(&review_logged()).event_type(),
"ReviewLogged"
);
assert_eq!(
EventPayload::from(&review_updated()).event_type(),
"ReviewUpdated"
);
assert_eq!(
EventPayload::from(&movie_discovered()).event_type(),
"MovieDiscovered"
);
}
#[test]
fn round_trip_image_stored() {
let event = DomainEvent::ImageStored {
key: "avatars/abc123".into(),
};
let payload = EventPayload::from(&event);
let json = serde_json::to_string(&payload).unwrap();
let back: EventPayload = serde_json::from_str(&json).unwrap();
let recovered = DomainEvent::try_from(back).unwrap();
assert_eq!(EventPayload::from(&event), EventPayload::from(&recovered));
}
#[test]
fn image_stored_event_type() {
let payload = EventPayload::from(&DomainEvent::ImageStored {
key: "posters/x".into(),
});
assert_eq!(payload.event_type(), "ImageStored");
}
#[test]
fn a_message_round_trips_its_event_id_and_decodes_without_one() {
use event_payload::EventMessage;
let with_id = EventMessage {
event_id: Some(Uuid::new_v4().to_string()),
payload: EventPayload::from(&review_logged()),
};
let json = serde_json::to_string(&with_id).unwrap();
assert_eq!(
serde_json::from_str::<EventMessage>(&json).unwrap(),
with_id
);
let legacy = serde_json::to_string(&EventPayload::from(&review_logged())).unwrap();
let decoded: EventMessage = serde_json::from_str(&legacy).unwrap();
assert_eq!(decoded.event_id, None);
let unit = EventMessage {
event_id: Some("x".into()),
payload: EventPayload::SearchReindexRequested,
};
let json = serde_json::to_string(&unit).unwrap();
assert_eq!(serde_json::from_str::<EventMessage>(&json).unwrap(), unit);
}
fn every_event() -> Vec<DomainEvent> {
use domain::{
models::{ExternalPersonId, PersonId},
value_objects::{FollowTarget, GoalId, PosterPath, SocialIdentity, WrapUpId},
};
let user = || UserId::from_uuid(Uuid::new_v4());
let movie = || MovieId::from_uuid(Uuid::new_v4());
let local = || SocialIdentity::Local(UserId::from_uuid(Uuid::new_v4()));
let remote = || SocialIdentity::Remote {
actor_url: "https://remote.example/users/a".into(),
};
let date = |d| chrono::NaiveDate::from_ymd_opt(2026, 1, d).unwrap();
vec![
review_logged(),
review_updated(),
movie_discovered(),
DomainEvent::MovieDeleted {
movie_id: movie(),
poster_path: Some(PosterPath::new("p/x.jpg".into()).unwrap()),
},
DomainEvent::UserUpdated { user_id: user() },
DomainEvent::ReviewDeleted {
review_id: ReviewId::from_uuid(Uuid::new_v4()),
user_id: user(),
},
DomainEvent::MovieEnrichmentRequested {
movie_id: movie(),
external_metadata_id: ExternalMetadataId::new("tt7654321".into()).unwrap(),
},
DomainEvent::PersonEnrichmentRequested {
person_id: PersonId::from_uuid(Uuid::new_v4()),
external_person_id: ExternalPersonId::new("42"),
},
DomainEvent::ImageStored { key: "k".into() },
DomainEvent::WatchlistEntryAdded {
user_id: user(),
movie_id: movie(),
movie_title: "Heat".into(),
release_year: 1995,
external_metadata_id: Some("tt0113277".into()),
added_at: fixed_dt(),
},
DomainEvent::WatchlistEntryRemoved {
user_id: user(),
movie_id: movie(),
},
DomainEvent::FollowRequested {
follower: user(),
target: FollowTarget::Handle("a@b.example".into()),
},
DomainEvent::FollowAccepted {
owner: user(),
requester: remote(),
},
DomainEvent::FollowRejected {
owner: user(),
requester: local(),
},
DomainEvent::Unfollowed {
follower: user(),
target: remote(),
},
DomainEvent::FollowerRemoved {
owner: user(),
follower: local(),
},
DomainEvent::ActorBlocked {
blocker: user(),
target: remote(),
},
DomainEvent::ActorUnblocked {
blocker: user(),
target: remote(),
},
DomainEvent::BackfillFollower {
owner_user_id: user(),
follower_inbox_url: "https://remote.example/inbox".into(),
},
DomainEvent::FederationDeliveryRequested {
inbox_url: "https://remote.example/inbox".into(),
activity_json: serde_json::json!({"type": "Create"}),
signing_actor_id: Uuid::new_v4(),
},
DomainEvent::WatchEventIngested {
user_id: user(),
title: "Heat".into(),
source: "jellyfin".into(),
},
DomainEvent::WrapUpRequested {
wrapup_id: WrapUpId::from_uuid(Uuid::new_v4()),
user_id: Some(user()),
start_date: date(1),
end_date: date(2),
},
DomainEvent::WrapUpCompleted {
wrapup_id: WrapUpId::from_uuid(Uuid::new_v4()),
},
DomainEvent::SearchReindexRequested,
DomainEvent::PosterSynced { movie_id: movie() },
DomainEvent::GoalCreated {
goal_id: GoalId::from_uuid(Uuid::new_v4()),
user_id: user(),
year: 2026,
target_count: 50,
},
DomainEvent::GoalUpdated {
goal_id: GoalId::from_uuid(Uuid::new_v4()),
user_id: user(),
year: 2026,
target_count: 60,
},
DomainEvent::GoalDeleted {
goal_id: GoalId::from_uuid(Uuid::new_v4()),
user_id: user(),
year: 2026,
},
DomainEvent::UserDeleted { user_id: user() },
DomainEvent::UserAccountMoved {
user_id: user(),
new_actor_url: "https://new.example/users/a".into(),
},
]
}
fn assert_covers_every_variant(event: &DomainEvent) {
use DomainEvent::*;
match event {
ReviewLogged { .. }
| ReviewUpdated { .. }
| MovieDiscovered { .. }
| MovieDeleted { .. }
| UserUpdated { .. }
| ReviewDeleted { .. }
| MovieEnrichmentRequested { .. }
| PersonEnrichmentRequested { .. }
| ImageStored { .. }
| WatchlistEntryAdded { .. }
| WatchlistEntryRemoved { .. }
| FollowRequested { .. }
| FollowAccepted { .. }
| FollowRejected { .. }
| Unfollowed { .. }
| FollowerRemoved { .. }
| ActorBlocked { .. }
| ActorUnblocked { .. }
| BackfillFollower { .. }
| FederationDeliveryRequested { .. }
| WatchEventIngested { .. }
| WrapUpRequested { .. }
| WrapUpCompleted { .. }
| SearchReindexRequested
| PosterSynced { .. }
| GoalCreated { .. }
| GoalUpdated { .. }
| GoalDeleted { .. }
| UserDeleted { .. }
| UserAccountMoved { .. } => {}
}
}
#[test]
fn every_event_round_trips_through_an_event_message_with_and_without_an_id() {
use event_payload::EventMessage;
let events = every_event();
events.iter().for_each(assert_covers_every_variant);
let types: std::collections::HashSet<_> = events
.iter()
.map(|e| EventPayload::from(e).event_type())
.collect();
assert_eq!(types.len(), events.len(), "one sample per variant");
for event in &events {
for id in [Some(Uuid::new_v4().to_string()), None] {
let message = EventMessage {
event_id: id.clone(),
payload: EventPayload::from(event),
};
let json = serde_json::to_string(&message).unwrap();
let back: EventMessage = serde_json::from_str(&json)
.unwrap_or_else(|e| panic!("{} decode: {e}", message.payload.event_type()));
assert_eq!(back.event_id, id);
let recovered = DomainEvent::try_from(back.payload).unwrap();
assert_eq!(EventPayload::from(event), EventPayload::from(&recovered));
}
}
}