1use 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#[derive(Debug, Error)]
18pub enum ExecutionError {
19 #[error("Validation failed: {0}")]
21 ValidationFailed(String),
22
23 #[error("Step '{0}' was aborted by user")]
25 StepAborted(StepId),
26
27 #[error("IO error: {0}")]
29 Io(#[from] io::Error),
30
31 #[error("Step '{step}' failed: {reason}")]
33 StepFailed {
34 step: StepId,
36 reason: String,
38 },
39
40 #[error("Unknown action: '{0}'")]
42 UnknownAction(ActionType),
43
44 #[error("Invalid params: {0}")]
46 InvalidParams(String),
47
48 #[error("Failed to load material '{name}': {reason}")]
50 MaterialLoadFailed {
51 name: String,
53 reason: String,
55 },
56
57 #[error("Failed to write output '{name}': {reason}")]
59 OutputWriteFailed {
60 name: String,
62 reason: String,
64 },
65
66 #[error("Transcript error: {0}")]
68 TranscriptError(String),
69
70 #[error("Backend error: {0}")]
72 BackendError(#[from] BackendError),
73}
74
75pub(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
86pub(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 Ok(ArtifactValue::Text(identifier.clone()))
118 }
119 }
120 }
121 }
122}
123
124pub(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}