Skip to main content

sayiir_runtime/
error.rs

1//! Typed error for the sayiir runtime layer.
2
3use sayiir_core::InvalidInstanceId;
4use sayiir_core::error::{BoxError, BuildError, BuildErrors, CodecError, WorkflowError};
5use sayiir_persistence::{BackendError, RunConflict};
6
7/// Typed error for the sayiir runtime layer.
8///
9/// Replaces `BoxError` in internal runtime APIs, keeping `BoxError` only at
10/// true user boundaries (codec traits, user task callbacks).
11#[derive(Debug, thiserror::Error)]
12pub enum RuntimeError {
13    /// Workflow logic error (cancellation, definition mismatch, task not found, etc.)
14    #[error(transparent)]
15    Workflow(#[from] WorkflowError),
16
17    /// Build/hydration errors (duplicate IDs, missing tasks, empty branches).
18    #[error(transparent)]
19    Build(#[from] BuildErrors),
20
21    /// Persistent backend error (storage failures).
22    #[error(transparent)]
23    Backend(#[from] BackendError),
24
25    /// Codec encode/decode error (schema mismatch, serialization failure).
26    #[error(transparent)]
27    Codec(#[from] CodecError),
28
29    /// User task execution error (opaque — from user-provided code).
30    #[error(transparent)]
31    Task(BoxError),
32
33    /// Tokio task join error (branch spawn failures).
34    #[error(transparent)]
35    Join(#[from] tokio::task::JoinError),
36
37    /// A workflow instance with this ID already exists (conflict policy = Fail).
38    #[error("Workflow instance already exists: {0}")]
39    InstanceAlreadyExists(String),
40
41    /// The supplied workflow `instance_id` failed the API-boundary length check.
42    #[error(transparent)]
43    InvalidInstanceId(#[from] InvalidInstanceId),
44}
45
46impl From<BoxError> for RuntimeError {
47    fn from(err: BoxError) -> Self {
48        match err.downcast::<CodecError>() {
49            Ok(codec_err) => Self::Codec(*codec_err),
50            Err(other) => Self::Task(other),
51        }
52    }
53}
54
55impl From<BuildError> for RuntimeError {
56    fn from(error: BuildError) -> Self {
57        Self::Build(BuildErrors::from(error))
58    }
59}
60
61impl From<RunConflict> for RuntimeError {
62    fn from(error: RunConflict) -> Self {
63        match error {
64            RunConflict::InvalidInstanceId(e) => Self::InvalidInstanceId(e),
65            RunConflict::AlreadyExists(id) => Self::InstanceAlreadyExists(id),
66            RunConflict::DefinitionMismatch { expected, found } => {
67                Self::Workflow(WorkflowError::DefinitionMismatch { expected, found })
68            }
69            RunConflict::Backend(e) => Self::Backend(e),
70        }
71    }
72}
73
74impl RuntimeError {
75    /// Returns `true` if this error is a `TaskTimedOut` workflow error.
76    #[must_use]
77    pub fn is_timeout(&self) -> bool {
78        matches!(self, Self::Workflow(WorkflowError::TaskTimedOut { .. }))
79    }
80
81    /// Returns `true` if this error is a codec decode failure (schema mismatch).
82    #[must_use]
83    pub fn is_decode_error(&self) -> bool {
84        matches!(self, Self::Codec(CodecError::DecodeFailed { .. }))
85    }
86}