use std::collections::BTreeMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::error::Result;
use crate::model::{ActorRef, KeepsakeId, RelationId, RelationSpec, SubjectRef};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CommandContext {
pub actor: ActorRef,
pub idempotency_key: Option<String>,
pub metadata: BTreeMap<String, String>,
}
impl CommandContext {
#[must_use]
pub const fn new(actor: ActorRef) -> Self {
Self {
actor,
idempotency_key: None,
metadata: BTreeMap::new(),
}
}
#[must_use]
pub fn with_idempotency_key(mut self, key: impl Into<String>) -> Self {
self.idempotency_key = Some(key.into());
self
}
#[must_use]
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn validate(&self) -> Result<()> {
self.actor.validate()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ApplyKeepsake {
pub id: KeepsakeId,
pub subject: SubjectRef,
pub relation_id: RelationId,
pub at: DateTime<Utc>,
pub metadata: BTreeMap<String, String>,
pub context: CommandContext,
}
impl ApplyKeepsake {
#[must_use]
pub fn new(
subject: SubjectRef,
relation_id: RelationId,
at: DateTime<Utc>,
context: CommandContext,
) -> Self {
Self {
id: Uuid::now_v7(),
subject,
relation_id,
at,
metadata: BTreeMap::new(),
context,
}
}
#[must_use]
pub fn for_spec<Spec>(subject: SubjectRef, at: DateTime<Utc>, context: CommandContext) -> Self
where
Spec: RelationSpec,
{
Self::new(subject, Spec::ID, at, context)
}
#[must_use]
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RevokeKeepsake {
pub keepsake_id: KeepsakeId,
pub at: DateTime<Utc>,
pub context: CommandContext,
}
impl RevokeKeepsake {
#[must_use]
pub const fn new(keepsake_id: KeepsakeId, at: DateTime<Utc>, context: CommandContext) -> Self {
Self {
keepsake_id,
at,
context,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RevokeBySubject {
pub subject: SubjectRef,
pub relation_id: RelationId,
pub at: DateTime<Utc>,
pub context: CommandContext,
}
impl RevokeBySubject {
#[must_use]
pub const fn new(
subject: SubjectRef,
relation_id: RelationId,
at: DateTime<Utc>,
context: CommandContext,
) -> Self {
Self {
subject,
relation_id,
at,
context,
}
}
#[must_use]
pub const fn for_spec<Spec>(
subject: SubjectRef,
at: DateTime<Utc>,
context: CommandContext,
) -> Self
where
Spec: RelationSpec,
{
Self::new(subject, Spec::ID, at, context)
}
}
#[cfg(test)]
mod tests {
use chrono::Utc;
use uuid::Uuid;
use super::*;
use crate::model::{ActorRef, StaticRelationKey, SubjectRef};
use crate::{ExpiryPolicy, RelationSpec};
struct TrustedTag;
impl RelationSpec for TrustedTag {
const ID: Uuid = Uuid::from_u128(1);
const KEY: StaticRelationKey = StaticRelationKey::new("tag", "trusted");
fn expiry(_at: chrono::DateTime<chrono::Utc>) -> ExpiryPolicy {
ExpiryPolicy::ManualOnly
}
}
#[test]
fn command_context_builder_sets_idempotency_and_metadata() -> crate::Result<()> {
let context = CommandContext::new(ActorRef::new("user", "admin")?)
.with_idempotency_key("request-1")
.with_metadata("request_id", "req_123");
assert_eq!(context.actor, ActorRef::new("user", "admin")?);
assert_eq!(context.idempotency_key.as_deref(), Some("request-1"));
assert_eq!(
context.metadata.get("request_id").map(String::as_str),
Some("req_123")
);
Ok(())
}
#[test]
fn apply_builder_attaches_metadata() -> crate::Result<()> {
let command = ApplyKeepsake::new(
SubjectRef::new("account", "acct_123")?,
Uuid::nil(),
Utc::now(),
CommandContext::new(ActorRef::new("system", "worker")?),
)
.with_metadata("source", "support");
assert_eq!(
command.metadata.get("source").map(String::as_str),
Some("support")
);
Ok(())
}
#[test]
fn typed_apply_and_revoke_constructors_set_command_fields() -> crate::Result<()> {
let at = Utc::now();
let context = CommandContext::new(ActorRef::new("system", "worker")?);
let apply = ApplyKeepsake::for_spec::<TrustedTag>(
SubjectRef::new("account", "acct_123")?,
at,
context.clone(),
);
assert_eq!(apply.relation_id, TrustedTag::ID);
assert_eq!(apply.at, at);
assert_eq!(apply.context, context);
let revoke = RevokeKeepsake::new(apply.id, at, apply.context.clone());
assert_eq!(revoke.keepsake_id, apply.id);
assert_eq!(revoke.at, at);
assert_eq!(revoke.context, apply.context);
Ok(())
}
#[test]
fn revoke_by_subject_constructors_set_command_fields() -> crate::Result<()> {
let at = Utc::now();
let subject = SubjectRef::new("account", "acct_123")?;
let context = CommandContext::new(ActorRef::new("user", "moderator")?);
let by_id = RevokeBySubject::new(subject.clone(), Uuid::nil(), at, context.clone());
assert_eq!(by_id.subject, subject);
assert_eq!(by_id.relation_id, Uuid::nil());
assert_eq!(by_id.at, at);
assert_eq!(by_id.context, context);
let by_spec = RevokeBySubject::for_spec::<TrustedTag>(subject, at, context);
assert_eq!(by_spec.relation_id, TrustedTag::ID);
Ok(())
}
}