Skip to main content

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}