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
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}