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 /// Look up a named input that holds a list, in the order written,
93 /// returning a uniform `ActionError` when it is missing or holds one
94 /// reference instead.
95 ///
96 /// # Errors
97 ///
98 /// Returns [`ActionError::Failed`] if the input is missing, is not a
99 /// list, or if the step uses a single (positional) input.
100 pub fn required_named_inputs(
101 &self,
102 key: &str,
103 action: &'static str,
104 ) -> Result<&[ArtifactRef], ActionError> {
105 self.typed_inputs
106 .as_ref()
107 .and_then(|i| i.get_many(key))
108 .ok_or_else(|| {
109 ActionError::Failed(format!("{action}: missing required list input '{key}'"))
110 })
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use rite_model::ArtifactRef;
118
119 #[test]
120 fn step_info_exposes_its_id_role_and_backend() {
121 let typed_inputs = Some(StepInputs::Single(ArtifactRef::Produced {
122 id: ArtifactId::new("foo"),
123 property: None,
124 }));
125
126 let info = StepInfo::new(
127 StepId::new("test_step"),
128 Some(RoleId::new("operator")),
129 Some("software".to_string()),
130 Some(ArtifactId::new("output")),
131 typed_inputs,
132 );
133
134 assert_eq!(info.id_str(), "test_step");
135 assert_eq!(info.role_str(), Some("operator"));
136 assert_eq!(info.backend, Some("software".to_string()));
137 assert!(info.produces.is_some());
138 assert!(info.typed_inputs.is_some());
139 }
140}