Skip to main content

rite_runtime/
executor.rs

1//! Execution-level error / outcome types and shared helpers used by
2//! [`crate::runner::Executor`] when walking a resolved ceremony.
3
4use crate::actions::ArtifactValue;
5use crate::step_info::StepInfo;
6use crate::transcript::compute_file_fingerprint;
7use rite_model::{ActionType, ArtifactId, Material, MaterialKind, MaterialSource, Step, StepId};
8use rite_sdk::BackendError;
9use std::fs;
10use std::io;
11use std::path::PathBuf;
12use thiserror::Error;
13
14use crate::output_config::OutputConfig;
15
16/// Errors during ceremony execution.
17#[derive(Debug, Error)]
18pub enum ExecutionError {
19    /// Required parameters are missing values.
20    #[error("Validation failed: {0}")]
21    ValidationFailed(String),
22
23    /// A step was aborted by the user.
24    #[error("Step '{0}' was aborted by user")]
25    StepAborted(StepId),
26
27    /// An I/O error occurred.
28    #[error("IO error: {0}")]
29    Io(#[from] io::Error),
30
31    /// A step handler returned a failure.
32    #[error("Step '{step}' failed: {reason}")]
33    StepFailed {
34        /// Step that failed.
35        step: StepId,
36        /// Failure description.
37        reason: String,
38    },
39
40    /// The action type is not registered.
41    #[error("Unknown action: '{0}'")]
42    UnknownAction(ActionType),
43
44    /// Invalid or missing parameters for a handler.
45    #[error("Invalid params: {0}")]
46    InvalidParams(String),
47
48    /// A material could not be loaded.
49    #[error("Failed to load material '{name}': {reason}")]
50    MaterialLoadFailed {
51        /// Material name.
52        name: String,
53        /// Failure description.
54        reason: String,
55    },
56
57    /// An output artifact could not be written.
58    #[error("Failed to write output '{name}': {reason}")]
59    OutputWriteFailed {
60        /// Output name.
61        name: String,
62        /// Failure description.
63        reason: String,
64    },
65
66    /// Transcript writing failed.
67    #[error("Transcript error: {0}")]
68    TranscriptError(String),
69
70    /// A backend returned an error.
71    #[error("Backend error: {0}")]
72    BackendError(#[from] BackendError),
73}
74
75/// Build a `StepInfo` from a resolved `Step`.
76pub(crate) fn step_info_from(step: &Step) -> StepInfo {
77    StepInfo::new(
78        step.id.clone(),
79        step.role.clone(),
80        step.backend.clone(),
81        step.creates.clone(),
82        step.reads_resolved.clone(),
83    )
84}
85
86/// Load a single material into an `ArtifactValue`.
87pub(crate) fn load_material_artifact(
88    name: &str,
89    material: &Material,
90) -> Result<ArtifactValue, ExecutionError> {
91    match &material.kind {
92        MaterialKind::Physical { identifier, .. } => {
93            let text = identifier
94                .as_deref()
95                .or(material.title.as_deref())
96                .unwrap_or(name)
97                .to_string();
98            Ok(ArtifactValue::Text(text))
99        }
100        MaterialKind::Digital { source } => {
101            let source = source
102                .as_ref()
103                .ok_or_else(|| ExecutionError::MaterialLoadFailed {
104                    name: name.to_string(),
105                    reason: "no source provided for digital material".to_string(),
106                })?;
107            match source {
108                MaterialSource::File { file } => {
109                    let bytes = fs::read(file).map_err(|e| ExecutionError::MaterialLoadFailed {
110                        name: name.to_string(),
111                        reason: e.to_string(),
112                    })?;
113                    Ok(ArtifactValue::Bytes(bytes))
114                }
115                MaterialSource::Identifier { identifier } => {
116                    // Digital material with an inline identifier: treat as text content.
117                    Ok(ArtifactValue::Text(identifier.clone()))
118                }
119            }
120        }
121    }
122}
123
124/// Serialize an artifact, write it to disk, and return `(path, sha256-hex, size-bytes, mime-type)`.
125pub(crate) fn write_artifact_to_disk(
126    artifact_id: &ArtifactId,
127    artifact_value: &ArtifactValue,
128    output_config: &OutputConfig,
129) -> Result<(PathBuf, String, u64, Option<String>), ExecutionError> {
130    let serialized =
131        artifact_value
132            .serialize(None)
133            .map_err(|e| ExecutionError::OutputWriteFailed {
134                name: artifact_id.as_str().to_string(),
135                reason: e,
136            })?;
137
138    let path = output_config.artifact_path(artifact_id.as_str(), serialized.extension);
139
140    fs::write(&path, &serialized.bytes).map_err(|e| ExecutionError::OutputWriteFailed {
141        name: artifact_id.as_str().to_string(),
142        reason: e.to_string(),
143    })?;
144
145    let hash = compute_file_fingerprint(&path).map_err(|e| ExecutionError::OutputWriteFailed {
146        name: artifact_id.as_str().to_string(),
147        reason: format!("hash computation failed: {e}"),
148    })?;
149
150    let size = fs::metadata(&path)
151        .map_err(|e| ExecutionError::OutputWriteFailed {
152            name: artifact_id.as_str().to_string(),
153            reason: format!("metadata read failed: {e}"),
154        })?
155        .len();
156
157    Ok((path, hash, size, serialized.mime_type))
158}