rite_runtime/step_info.rs
1//! Step information for action handlers.
2//!
3//! This module provides [`StepInfo`], a lightweight struct containing
4//! exactly what action handlers need from a step. This replaces the
5//! AST `Step` type in the handler interface, enabling cleaner separation
6//! between resolution and execution.
7
8use rite_model::{ArtifactId, ArtifactRef, RoleId, StepId, StepInputs};
9
10use crate::runner::ActionError;
11
12/// Information about a step needed by action handlers.
13///
14/// This is a subset of `Step` containing only what handlers need:
15/// - Step ID (for error messages)
16/// - Role (for attestation)
17/// - Backend (for cryptographic operations)
18/// - Produces (for artifact storage)
19/// - Typed inputs (pre-resolved artifact references)
20///
21/// Constructed from `Step` by the executor before calling handlers.
22#[derive(Debug, Clone)]
23pub struct StepInfo {
24 /// Step identifier (for error messages).
25 pub id: StepId,
26
27 /// Role that performs this step (for attestation).
28 pub role: Option<RoleId>,
29
30 /// Backend to use for this step (if action requires one).
31 pub backend: Option<String>,
32
33 /// Artifact ID this step produces (if any).
34 pub produces: Option<ArtifactId>,
35
36 /// Pre-resolved input artifact references.
37 /// Handlers can access artifact IDs directly without parsing `${...}` strings.
38 pub typed_inputs: Option<StepInputs>,
39}
40
41impl StepInfo {
42 /// Create a new `StepInfo` from `Step` fields.
43 pub fn new(
44 id: StepId,
45 role: Option<RoleId>,
46 backend: Option<String>,
47 produces: Option<ArtifactId>,
48 typed_inputs: Option<StepInputs>,
49 ) -> Self {
50 Self {
51 id,
52 role,
53 backend,
54 produces,
55 typed_inputs,
56 }
57 }
58
59 /// Get the step ID as a string slice.
60 pub fn id_str(&self) -> &str {
61 self.id.as_str()
62 }
63
64 /// Get the role as a string reference (for display).
65 pub fn role_str(&self) -> Option<&str> {
66 self.role.as_ref().map(rite_model::RoleId::as_str)
67 }
68
69 /// Look up a named input by key, returning `None` if missing or if the
70 /// step uses a single (positional) input.
71 #[must_use]
72 pub fn named_input(&self, key: &str) -> Option<&ArtifactRef> {
73 self.typed_inputs.as_ref().and_then(|i| i.get(key))
74 }
75
76 /// Look up a required named input, returning a uniform `ActionError`
77 /// when the input is missing.
78 ///
79 /// # Errors
80 ///
81 /// Returns [`ActionError::Failed`] if the input is missing or if the
82 /// step uses a single (positional) input.
83 pub fn required_named_input(
84 &self,
85 key: &str,
86 action: &'static str,
87 ) -> Result<&ArtifactRef, ActionError> {
88 self.named_input(key)
89 .ok_or_else(|| ActionError::Failed(format!("{action}: missing required input '{key}'")))
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use rite_model::ArtifactRef;
97
98 #[test]
99 fn step_info_basic() {
100 // Create typed inputs instead of raw JSON
101 let typed_inputs = Some(StepInputs::Single(ArtifactRef::Produced {
102 id: ArtifactId::new("foo"),
103 property: None,
104 }));
105
106 let info = StepInfo::new(
107 StepId::new("test_step"),
108 Some(RoleId::new("operator")),
109 Some("software".to_string()),
110 Some(ArtifactId::new("output")),
111 typed_inputs,
112 );
113
114 assert_eq!(info.id_str(), "test_step");
115 assert_eq!(info.role_str(), Some("operator"));
116 assert_eq!(info.backend, Some("software".to_string()));
117 assert!(info.produces.is_some());
118 assert!(info.typed_inputs.is_some());
119 }
120}