runledger-core 0.1.0

Core contracts and types for the Runledger durable job and workflow system
Documentation
use serde::{Deserialize, Serialize};
use std::str::FromStr;

#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum JobStage {
    Queued,
    Scheduled,
    Running,
    Completed,
}

impl JobStage {
    #[must_use]
    pub fn from_db_value(raw_stage: &str) -> Option<Self> {
        match raw_stage {
            "queued" => Some(Self::Queued),
            "scheduled" => Some(Self::Scheduled),
            "running" => Some(Self::Running),
            "completed" => Some(Self::Completed),
            _ => None,
        }
    }

    #[must_use]
    pub const fn as_db_value(self) -> &'static str {
        match self {
            Self::Queued => "queued",
            Self::Scheduled => "scheduled",
            Self::Running => "running",
            Self::Completed => "completed",
        }
    }
}

impl FromStr for JobStage {
    type Err = ();

    fn from_str(raw_stage: &str) -> Result<Self, Self::Err> {
        Self::from_db_value(raw_stage).ok_or(())
    }
}

#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum JobStatus {
    Pending,
    Leased,
    Succeeded,
    DeadLettered,
    Canceled,
}

impl JobStatus {
    #[must_use]
    pub fn from_db_value(raw_status: &str) -> Option<Self> {
        match raw_status {
            "PENDING" => Some(Self::Pending),
            "LEASED" => Some(Self::Leased),
            "SUCCEEDED" => Some(Self::Succeeded),
            "DEAD_LETTERED" => Some(Self::DeadLettered),
            "CANCELED" => Some(Self::Canceled),
            _ => None,
        }
    }

    #[must_use]
    pub const fn as_db_value(self) -> &'static str {
        match self {
            Self::Pending => "PENDING",
            Self::Leased => "LEASED",
            Self::Succeeded => "SUCCEEDED",
            Self::DeadLettered => "DEAD_LETTERED",
            Self::Canceled => "CANCELED",
        }
    }
}

impl FromStr for JobStatus {
    type Err = ();

    fn from_str(raw_status: &str) -> Result<Self, Self::Err> {
        Self::from_db_value(raw_status).ok_or(())
    }
}

#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum JobEventType {
    Enqueued,
    Leased,
    Heartbeat,
    StageChanged,
    Progress,
    RetryScheduled,
    Succeeded,
    Failed,
    DeadLettered,
    Canceled,
    Requeued,
}

impl JobEventType {
    #[must_use]
    pub fn from_db_value(raw_type: &str) -> Option<Self> {
        match raw_type {
            "ENQUEUED" => Some(Self::Enqueued),
            "LEASED" => Some(Self::Leased),
            "HEARTBEAT" => Some(Self::Heartbeat),
            "STAGE_CHANGED" => Some(Self::StageChanged),
            "PROGRESS" => Some(Self::Progress),
            "RETRY_SCHEDULED" => Some(Self::RetryScheduled),
            "SUCCEEDED" => Some(Self::Succeeded),
            "FAILED" => Some(Self::Failed),
            "DEAD_LETTERED" => Some(Self::DeadLettered),
            "CANCELED" => Some(Self::Canceled),
            "REQUEUED" => Some(Self::Requeued),
            _ => None,
        }
    }

    #[must_use]
    pub const fn as_db_value(self) -> &'static str {
        match self {
            Self::Enqueued => "ENQUEUED",
            Self::Leased => "LEASED",
            Self::Heartbeat => "HEARTBEAT",
            Self::StageChanged => "STAGE_CHANGED",
            Self::Progress => "PROGRESS",
            Self::RetryScheduled => "RETRY_SCHEDULED",
            Self::Succeeded => "SUCCEEDED",
            Self::Failed => "FAILED",
            Self::DeadLettered => "DEAD_LETTERED",
            Self::Canceled => "CANCELED",
            Self::Requeued => "REQUEUED",
        }
    }
}

impl FromStr for JobEventType {
    type Err = ();

    fn from_str(raw_type: &str) -> Result<Self, Self::Err> {
        Self::from_db_value(raw_type).ok_or(())
    }
}

#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum JobFailureKind {
    Retryable,
    Terminal,
    Timeout,
    LeaseExpired,
    Panicked,
}

impl JobFailureKind {
    #[must_use]
    pub const fn as_db_value(self) -> &'static str {
        match self {
            Self::Retryable => "RETRYABLE",
            Self::Terminal => "TERMINAL",
            Self::Timeout => "TIMEOUT",
            Self::LeaseExpired => "LEASE_EXPIRED",
            Self::Panicked => "PANICKED",
        }
    }
}

#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum WorkflowRunStatus {
    Running,
    WaitingForExternal,
    Succeeded,
    CompletedWithErrors,
    Canceled,
}

impl WorkflowRunStatus {
    #[must_use]
    pub fn from_db_value(raw_status: &str) -> Option<Self> {
        match raw_status {
            "RUNNING" => Some(Self::Running),
            "WAITING_FOR_EXTERNAL" => Some(Self::WaitingForExternal),
            "SUCCEEDED" => Some(Self::Succeeded),
            "COMPLETED_WITH_ERRORS" => Some(Self::CompletedWithErrors),
            "CANCELED" => Some(Self::Canceled),
            _ => None,
        }
    }

    #[must_use]
    pub const fn as_db_value(self) -> &'static str {
        match self {
            Self::Running => "RUNNING",
            Self::WaitingForExternal => "WAITING_FOR_EXTERNAL",
            Self::Succeeded => "SUCCEEDED",
            Self::CompletedWithErrors => "COMPLETED_WITH_ERRORS",
            Self::Canceled => "CANCELED",
        }
    }
}

impl FromStr for WorkflowRunStatus {
    type Err = ();

    fn from_str(raw_status: &str) -> Result<Self, Self::Err> {
        Self::from_db_value(raw_status).ok_or(())
    }
}

#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum WorkflowStepStatus {
    Blocked,
    WaitingForExternal,
    Enqueued,
    Running,
    Succeeded,
    Failed,
    Canceled,
}

impl WorkflowStepStatus {
    #[must_use]
    pub fn from_db_value(raw_status: &str) -> Option<Self> {
        match raw_status {
            "BLOCKED" => Some(Self::Blocked),
            "WAITING_FOR_EXTERNAL" => Some(Self::WaitingForExternal),
            "ENQUEUED" => Some(Self::Enqueued),
            "RUNNING" => Some(Self::Running),
            "SUCCEEDED" => Some(Self::Succeeded),
            "FAILED" => Some(Self::Failed),
            "CANCELED" => Some(Self::Canceled),
            _ => None,
        }
    }

    #[must_use]
    pub const fn as_db_value(self) -> &'static str {
        match self {
            Self::Blocked => "BLOCKED",
            Self::WaitingForExternal => "WAITING_FOR_EXTERNAL",
            Self::Enqueued => "ENQUEUED",
            Self::Running => "RUNNING",
            Self::Succeeded => "SUCCEEDED",
            Self::Failed => "FAILED",
            Self::Canceled => "CANCELED",
        }
    }

    #[must_use]
    pub const fn is_terminal(self) -> bool {
        matches!(self, Self::Succeeded | Self::Failed | Self::Canceled)
    }
}

impl FromStr for WorkflowStepStatus {
    type Err = ();

    fn from_str(raw_status: &str) -> Result<Self, Self::Err> {
        Self::from_db_value(raw_status).ok_or(())
    }
}

#[cfg(test)]
mod tests {
    use super::{JobStage, WorkflowRunStatus, WorkflowStepStatus};
    use proptest::prelude::*;

    #[test]
    fn parse_job_stage_from_str_rejects_invalid_value() {
        assert!("NOT_A_REAL_STAGE".parse::<JobStage>().is_err());
    }

    #[test]
    fn parse_workflow_run_status_from_str_rejects_invalid_value() {
        assert!("NOT_A_REAL_STATUS".parse::<WorkflowRunStatus>().is_err());
    }

    #[test]
    fn parse_workflow_step_status_from_str_rejects_invalid_value() {
        assert!("NOT_A_REAL_STATUS".parse::<WorkflowStepStatus>().is_err());
    }

    proptest! {
        #[test]
        fn job_stage_roundtrips_db_value(
            stage in prop_oneof![
                Just(JobStage::Queued),
                Just(JobStage::Scheduled),
                Just(JobStage::Running),
                Just(JobStage::Completed),
            ]
        ) {
            let raw = stage.as_db_value();

            prop_assert_eq!(JobStage::from_db_value(raw), Some(stage));
            prop_assert_eq!(raw.parse::<JobStage>().ok(), Some(stage));
        }

        #[test]
        fn workflow_step_status_roundtrips_db_value(
            status in prop_oneof![
                Just(WorkflowStepStatus::Blocked),
                Just(WorkflowStepStatus::WaitingForExternal),
                Just(WorkflowStepStatus::Enqueued),
                Just(WorkflowStepStatus::Running),
                Just(WorkflowStepStatus::Succeeded),
                Just(WorkflowStepStatus::Failed),
                Just(WorkflowStepStatus::Canceled),
            ]
        ) {
            let raw = status.as_db_value();

            prop_assert_eq!(WorkflowStepStatus::from_db_value(raw), Some(status));
            prop_assert_eq!(raw.parse::<WorkflowStepStatus>().ok(), Some(status));
        }

        #[test]
        fn workflow_run_status_roundtrips_db_value(
            status in prop_oneof![
                Just(WorkflowRunStatus::Running),
                Just(WorkflowRunStatus::WaitingForExternal),
                Just(WorkflowRunStatus::Succeeded),
                Just(WorkflowRunStatus::CompletedWithErrors),
                Just(WorkflowRunStatus::Canceled),
            ]
        ) {
            let raw = status.as_db_value();

            prop_assert_eq!(WorkflowRunStatus::from_db_value(raw), Some(status));
            prop_assert_eq!(raw.parse::<WorkflowRunStatus>().ok(), Some(status));
        }
    }
}