use std::collections::BTreeSet;
use nasaga_backend::{
SagaAttemptAuditRow, SagaConflictFactRow, SagaControlAuditRow, SagaManagementAuditRow,
SagaStepAttemptRow, SagaTransitionAuditRow,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum SagaManagementPermission {
Pause,
Resume,
RetryCompensation,
RetryResolution,
ReadAudit,
ListInstances,
ManualClose,
}
impl SagaManagementPermission {
pub fn as_str(self) -> &'static str {
match self {
Self::Pause => "saga.pause",
Self::Resume => "saga.resume",
Self::RetryCompensation => "saga.retry_compensation",
Self::RetryResolution => "saga.retry_resolution",
Self::ReadAudit => "saga.audit.read",
Self::ListInstances => "saga.instance.list",
Self::ManualClose => "saga.manual_close",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SagaManagementError {
InvalidContext,
PermissionDenied,
NotFound,
RateLimitExceeded,
PreconditionFailed,
OperationConflict,
}
impl SagaManagementError {
pub fn from_error(error: &anyhow::Error) -> Option<Self> {
error
.chain()
.find_map(|cause| cause.downcast_ref::<Self>().copied())
.or_else(|| {
error
.chain()
.any(|cause| {
cause
.downcast_ref::<nasaga_backend::SagaBackendError>()
.is_some_and(|error| {
error.kind() == nasaga_backend::SagaBackendErrorKind::Conflict
})
})
.then_some(Self::OperationConflict)
})
}
}
impl std::fmt::Display for SagaManagementError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::InvalidContext => "Saga management context is invalid",
Self::PermissionDenied => "Saga management permission is denied",
Self::NotFound => "Saga management target was not found",
Self::RateLimitExceeded => "saga_tenant_action_rate_exceeded",
Self::PreconditionFailed => "Saga management precondition is not satisfied",
Self::OperationConflict => "Saga management operation has different audit facts",
})
}
}
impl std::error::Error for SagaManagementError {}
#[derive(Debug, Clone)]
pub struct SagaManagementContext {
actor: String,
reason: String,
permissions: BTreeSet<SagaManagementPermission>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct SagaManagementExpectation {
expected_state_version: Option<u64>,
expected_control_version: Option<u64>,
}
impl SagaManagementExpectation {
pub fn new(expected_state_version: Option<u64>, expected_control_version: Option<u64>) -> Self {
Self {
expected_state_version,
expected_control_version,
}
}
pub(crate) fn verify(self, state_version: u64, control_version: u64) -> anyhow::Result<()> {
if self
.expected_state_version
.is_some_and(|expected| expected != state_version)
|| self
.expected_control_version
.is_some_and(|expected| expected != control_version)
{
return Err(crate::SagaConcurrencyError::StaleSnapshot.into());
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SagaAuditTrail {
pub attempts: Vec<SagaStepAttemptRow>,
pub transitions: Vec<SagaTransitionAuditRow>,
pub controls: Vec<SagaControlAuditRow>,
pub management_operations: Vec<SagaManagementAuditRow>,
pub conflicts: Vec<SagaConflictFactRow>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SagaAuditPageCursor {
pub audit_seq: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SagaAuditRecord {
Attempt(SagaAttemptAuditRow),
Transition(SagaTransitionAuditRow),
Control(SagaControlAuditRow),
Management(SagaManagementAuditRow),
Conflict(SagaConflictFactRow),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SagaAuditPage {
pub records: Vec<SagaAuditRecord>,
pub next_cursor: Option<SagaAuditPageCursor>,
}
impl SagaManagementContext {
pub fn new(
actor: impl Into<String>,
reason: impl Into<String>,
permissions: impl IntoIterator<Item = SagaManagementPermission>,
) -> anyhow::Result<Self> {
let actor = actor.into();
let reason = reason.into();
if !valid_audit_text(&actor, 128) || !valid_audit_text(&reason, 512) {
return Err(SagaManagementError::InvalidContext.into());
}
let permissions = permissions.into_iter().collect::<BTreeSet<_>>();
Ok(Self {
actor,
reason,
permissions,
})
}
pub fn actor(&self) -> &str {
&self.actor
}
pub fn reason(&self) -> &str {
&self.reason
}
pub fn require(&self, required: SagaManagementPermission) -> anyhow::Result<()> {
if self.permissions.contains(&required) {
Ok(())
} else {
Err(SagaManagementError::PermissionDenied.into())
}
}
}
fn valid_audit_text(value: &str, max_len: usize) -> bool {
!value.is_empty()
&& value.len() <= max_len
&& value.trim() == value
&& !value.chars().any(char::is_control)
}