use uuid::Uuid;
use crate::error::{
ContractResult, ContractViolation, CODE_COUNTER_OVERFLOW, CODE_COUNTER_ZERO,
CODE_IDENTIFIER_CHARSET, CODE_IDENTIFIER_EMPTY, CODE_IDENTIFIER_TOO_LONG,
CODE_IDENTIFIER_UNTRIMMED,
};
const SAGA_ID_NAMESPACE: Uuid = Uuid::from_bytes(*b"nasasaga-v1-idns");
const IDENTIFIER_MAX_LEN: usize = 128;
const OPAQUE_MAX_LEN: usize = 256;
fn validate_identifier(raw: &str, kind: &str) -> ContractResult<String> {
if raw != raw.trim() {
return Err(ContractViolation::new(
CODE_IDENTIFIER_UNTRIMMED,
format!("{kind} must not have leading or trailing whitespace"),
));
}
if raw.is_empty() {
return Err(ContractViolation::new(
CODE_IDENTIFIER_EMPTY,
format!("{kind} must not be empty"),
));
}
if raw.len() > IDENTIFIER_MAX_LEN {
return Err(ContractViolation::new(
CODE_IDENTIFIER_TOO_LONG,
format!("{kind} exceeds {IDENTIFIER_MAX_LEN} bytes"),
));
}
if !raw
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.'))
{
return Err(ContractViolation::new(
CODE_IDENTIFIER_CHARSET,
format!("{kind} allows only ASCII alphanumeric, '_', '-' and '.'"),
));
}
Ok(raw.to_string())
}
fn validate_opaque(raw: &str, kind: &str) -> ContractResult<String> {
if raw != raw.trim() {
return Err(ContractViolation::new(
CODE_IDENTIFIER_UNTRIMMED,
format!("{kind} must not have leading or trailing whitespace"),
));
}
if raw.is_empty() {
return Err(ContractViolation::new(
CODE_IDENTIFIER_EMPTY,
format!("{kind} must not be empty"),
));
}
if raw.len() > OPAQUE_MAX_LEN {
return Err(ContractViolation::new(
CODE_IDENTIFIER_TOO_LONG,
format!("{kind} exceeds {OPAQUE_MAX_LEN} bytes"),
));
}
if raw.chars().any(char::is_control) {
return Err(ContractViolation::new(
CODE_IDENTIFIER_CHARSET,
format!("{kind} must not contain control characters"),
));
}
Ok(raw.to_string())
}
pub(crate) fn canonical_bytes(fields: &[&[u8]]) -> Vec<u8> {
let mut encoded = Vec::new();
for field in fields {
encoded.extend_from_slice(&(field.len() as u64).to_be_bytes());
encoded.extend_from_slice(field);
}
encoded
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SagaId(String);
impl SagaId {
pub fn new(raw: impl AsRef<str>) -> ContractResult<Self> {
validate_opaque(raw.as_ref(), "saga id").map(Self)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TenantId(String);
impl TenantId {
pub fn new(raw: impl AsRef<str>) -> ContractResult<Self> {
validate_opaque(raw.as_ref(), "tenant id").map(Self)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct BusinessKey(String);
impl BusinessKey {
pub fn new(raw: impl AsRef<str>) -> ContractResult<Self> {
validate_opaque(raw.as_ref(), "business key").map(Self)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct WorkflowName(String);
impl WorkflowName {
pub fn new(raw: impl AsRef<str>) -> ContractResult<Self> {
validate_identifier(raw.as_ref(), "workflow name").map(Self)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct StepName(String);
impl StepName {
pub fn new(raw: impl AsRef<str>) -> ContractResult<Self> {
validate_identifier(raw.as_ref(), "step name").map(Self)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ServiceIdentity(String);
impl ServiceIdentity {
pub fn new(raw: impl AsRef<str>) -> ContractResult<Self> {
validate_identifier(raw.as_ref(), "service identity").map(Self)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct DefinitionVersion(u32);
impl DefinitionVersion {
pub fn new(value: u32) -> ContractResult<Self> {
if value == 0 {
return Err(ContractViolation::new(
CODE_COUNTER_ZERO,
"definition version must start at 1",
));
}
Ok(Self(value))
}
pub fn get(self) -> u32 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct AttemptNo(u32);
impl AttemptNo {
pub const FIRST: Self = Self(1);
pub fn new(value: u32) -> ContractResult<Self> {
if value == 0 {
return Err(ContractViolation::new(
CODE_COUNTER_ZERO,
"attempt number must start at 1",
));
}
Ok(Self(value))
}
pub fn next(self) -> ContractResult<Self> {
self.0
.checked_add(1)
.map(Self)
.ok_or_else(|| ContractViolation::new(CODE_COUNTER_OVERFLOW, "attempt number overflow"))
}
pub fn get(self) -> u32 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum StepPhase {
Execute,
Cancel,
Compensate,
Resolve,
}
impl StepPhase {
pub fn as_str(self) -> &'static str {
match self {
Self::Execute => "execute",
Self::Cancel => "cancel",
Self::Compensate => "compensate",
Self::Resolve => "resolve",
}
}
pub fn parse(raw: &str) -> Option<Self> {
match raw {
"execute" => Some(Self::Execute),
"cancel" => Some(Self::Cancel),
"compensate" => Some(Self::Compensate),
"resolve" => Some(Self::Resolve),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct EffectId(Uuid);
impl EffectId {
pub fn derive(
saga_id: &SagaId,
definition_version: DefinitionVersion,
step: &StepName,
phase: StepPhase,
) -> Self {
let encoded = canonical_bytes(&[
saga_id.as_str().as_bytes(),
&definition_version.get().to_be_bytes(),
step.as_str().as_bytes(),
phase.as_str().as_bytes(),
]);
Self(Uuid::new_v5(&SAGA_ID_NAMESPACE, &encoded))
}
pub fn as_uuid(&self) -> Uuid {
self.0
}
}
impl std::fmt::Display for EffectId {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct CommandId(Uuid);
impl CommandId {
pub fn derive(effect: &EffectId, attempt: AttemptNo) -> Self {
let encoded = canonical_bytes(&[effect.as_uuid().as_bytes(), &attempt.get().to_be_bytes()]);
Self(Uuid::new_v5(&SAGA_ID_NAMESPACE, &encoded))
}
pub fn as_uuid(&self) -> Uuid {
self.0
}
}
impl std::fmt::Display for CommandId {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "{}", self.0)
}
}