Skip to main content

rite_runtime/
state.rs

1//! Execution state management for ceremonies.
2//!
3//! This module provides:
4//! - [`ExecutionState`]: immutable state that accumulates during execution
5//! - [`HandlerContext`]: read-only view for action handlers
6//! - [`StepResult`]: what handlers return (outcome + produced artifacts)
7//!
8//! The design follows an FP accumulator pattern: state is recreated after
9//! each step rather than mutated. This eliminates borrow conflicts and
10//! makes state flow explicit.
11//!
12//! # Role resolution
13//!
14//! At runtime, role references have already been evaluated by the
15//! expression system. Handlers receive plain role names (e.g. `"admin"`)
16//! and use [`HandlerContext::resolve_role_name`] to get display names.
17
18use std::collections::HashMap;
19
20use rite_model::{ArtifactId, MaterialId, ParamId, RoleId, StepOutcome};
21
22use crate::actions::ArtifactValue;
23
24/// Complete execution state, recreated after each step.
25///
26/// This struct owns all state that evolves during ceremony execution.
27/// Rather than mutating fields, new state is created via
28/// [`ExecutionState::with_material`] (for materials loaded up front) and
29/// via the executor's internal fold (for step results).
30#[derive(Debug)]
31pub struct ExecutionState {
32    /// Resolved parameters (from ceremony defaults, CLI flags, or env vars).
33    pub params: HashMap<ParamId, serde_json::Value>,
34    /// Role ID → display name mapping.
35    pub roles: HashMap<RoleId, String>,
36    /// Material ID → display name mapping (for showing material titles).
37    pub materials: HashMap<MaterialId, String>,
38    /// Whether this is a dry run.
39    pub dry_run: bool,
40    /// Artifacts produced by completed steps and loaded materials.
41    pub artifacts: HashMap<ArtifactId, ArtifactValue>,
42}
43
44fn resolve_role_in(roles: &HashMap<RoleId, String>, role_id: &RoleId) -> String {
45    roles
46        .get(role_id)
47        .cloned()
48        .unwrap_or_else(|| role_id.as_str().to_string())
49}
50
51impl ExecutionState {
52    /// Create initial execution state.
53    #[must_use]
54    pub fn new(
55        params: HashMap<ParamId, serde_json::Value>,
56        roles: HashMap<RoleId, String>,
57        materials: HashMap<MaterialId, String>,
58        dry_run: bool,
59    ) -> Self {
60        Self {
61            params,
62            roles,
63            materials,
64            dry_run,
65            artifacts: HashMap::new(),
66        }
67    }
68
69    /// Create a read-only view for handlers.
70    #[must_use]
71    pub fn handler_context(&self) -> HandlerContext<'_> {
72        HandlerContext {
73            dry_run: self.dry_run,
74            params: &self.params,
75            artifacts: &self.artifacts,
76            roles: &self.roles,
77            materials: &self.materials,
78        }
79    }
80
81    /// Add a material artifact.
82    #[must_use]
83    pub fn with_material(mut self, id: ArtifactId, artifact: ArtifactValue) -> Self {
84        self.artifacts.insert(id, artifact);
85        self
86    }
87
88    /// Resolve a role ID to a display name.
89    #[must_use]
90    pub fn resolve_role(&self, role_id: &RoleId) -> String {
91        resolve_role_in(&self.roles, role_id)
92    }
93}
94
95/// Read-only view of execution state for handlers.
96///
97/// Handlers receive this view; they cannot modify state directly. They
98/// return a [`StepResult`], which the executor folds into the next state.
99#[derive(Debug, Clone, Copy)]
100pub struct HandlerContext<'a> {
101    /// Whether this is a dry run.
102    pub dry_run: bool,
103    /// Instance parameters.
104    pub params: &'a HashMap<ParamId, serde_json::Value>,
105    /// Artifacts produced so far.
106    pub artifacts: &'a HashMap<ArtifactId, ArtifactValue>,
107    /// Role ID → display name mapping.
108    pub roles: &'a HashMap<RoleId, String>,
109    /// Material ID → display name mapping (for showing material titles in expressions).
110    pub materials: &'a HashMap<MaterialId, String>,
111}
112
113impl HandlerContext<'_> {
114    /// Resolve a role ID to a display name.
115    #[must_use]
116    pub fn resolve_role(&self, role_id: &RoleId) -> String {
117        resolve_role_in(self.roles, role_id)
118    }
119
120    /// Resolve a role name to its display name.
121    ///
122    /// Takes a plain role name (not reference syntax) after expression
123    /// evaluation. Example: `resolve_role_name("admin")` → "Alice (Admin)".
124    #[must_use]
125    pub fn resolve_role_name(&self, role_name: &str) -> String {
126        self.resolve_role(&RoleId::new(role_name))
127    }
128
129    /// Get an artifact by ID.
130    #[must_use]
131    pub fn get_artifact(&self, id: &ArtifactId) -> Option<&ArtifactValue> {
132        self.artifacts.get(id)
133    }
134
135    /// Resolve a material ID to a display name (title or ID).
136    /// Returns `None` if the ID is not a material.
137    #[must_use]
138    pub fn resolve_material(&self, material_id: &MaterialId) -> Option<&str> {
139        self.materials.get(material_id).map(String::as_str)
140    }
141}
142
143/// Result of executing a step.
144///
145/// Handlers return this instead of mutating context. The executor uses
146/// it to accumulate state.
147#[derive(Debug)]
148pub struct StepResult {
149    /// Step outcome (completed or skipped).
150    pub outcome: StepOutcome,
151    /// Artifacts produced by this step.
152    pub artifacts: Vec<(ArtifactId, ArtifactValue)>,
153}
154
155impl StepResult {
156    /// Create a completed result with no artifacts.
157    pub fn completed(message: impl Into<String>) -> Self {
158        Self {
159            outcome: StepOutcome::Completed {
160                message: message.into(),
161            },
162            artifacts: Vec::new(),
163        }
164    }
165
166    /// Create a completed result with one artifact.
167    pub fn completed_with_artifact(
168        message: impl Into<String>,
169        id: ArtifactId,
170        value: ArtifactValue,
171    ) -> Self {
172        Self {
173            outcome: StepOutcome::Completed {
174                message: message.into(),
175            },
176            artifacts: vec![(id, value)],
177        }
178    }
179
180    /// Add an artifact to the result.
181    #[must_use]
182    pub fn with_artifact(mut self, id: ArtifactId, value: ArtifactValue) -> Self {
183        self.artifacts.push((id, value));
184        self
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn handler_context_is_read_only_view() {
194        let mut artifacts = HashMap::new();
195        artifacts.insert(
196            ArtifactId::new("test"),
197            ArtifactValue::Bytes(b"value".to_vec()),
198        );
199
200        let state = ExecutionState {
201            params: HashMap::new(),
202            roles: HashMap::new(),
203            materials: HashMap::new(),
204            dry_run: true,
205            artifacts,
206        };
207
208        let ctx = state.handler_context();
209
210        assert!(ctx.dry_run);
211        assert!(ctx.get_artifact(&ArtifactId::new("test")).is_some());
212        assert!(ctx.get_artifact(&ArtifactId::new("missing")).is_none());
213    }
214
215    #[test]
216    fn step_result_builder_pattern() {
217        let result = StepResult::completed("Done")
218            .with_artifact(ArtifactId::new("a"), ArtifactValue::Bytes(b"1".to_vec()))
219            .with_artifact(ArtifactId::new("b"), ArtifactValue::Bytes(b"2".to_vec()));
220
221        assert_eq!(result.artifacts.len(), 2);
222    }
223}