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            params: &self.params,
74            artifacts: &self.artifacts,
75            roles: &self.roles,
76            materials: &self.materials,
77        }
78    }
79
80    /// Add a material artifact.
81    #[must_use]
82    pub fn with_material(mut self, id: ArtifactId, artifact: ArtifactValue) -> Self {
83        self.artifacts.insert(id, artifact);
84        self
85    }
86
87    /// Resolve a role ID to a display name.
88    #[must_use]
89    pub fn resolve_role(&self, role_id: &RoleId) -> String {
90        resolve_role_in(&self.roles, role_id)
91    }
92}
93
94/// Read-only view of execution state for handlers.
95///
96/// Handlers receive this view; they cannot modify state directly. They
97/// return a [`StepResult`], which the executor folds into the next state.
98#[derive(Debug, Clone, Copy)]
99pub struct HandlerContext<'a> {
100    /// Instance parameters.
101    pub params: &'a HashMap<ParamId, serde_json::Value>,
102    /// Artifacts produced so far.
103    pub artifacts: &'a HashMap<ArtifactId, ArtifactValue>,
104    /// Role ID → display name mapping.
105    pub roles: &'a HashMap<RoleId, String>,
106    /// Material ID → display name mapping (for showing material titles in expressions).
107    pub materials: &'a HashMap<MaterialId, String>,
108}
109
110impl HandlerContext<'_> {
111    /// Resolve a role ID to a display name.
112    #[must_use]
113    pub fn resolve_role(&self, role_id: &RoleId) -> String {
114        resolve_role_in(self.roles, role_id)
115    }
116
117    /// Resolve a role name to its display name.
118    ///
119    /// Takes a plain role name (not reference syntax) after expression
120    /// evaluation. Example: `resolve_role_name("admin")` → "Alice (Admin)".
121    #[must_use]
122    pub fn resolve_role_name(&self, role_name: &str) -> String {
123        self.resolve_role(&RoleId::new(role_name))
124    }
125
126    /// Get an artifact by ID.
127    #[must_use]
128    pub fn get_artifact(&self, id: &ArtifactId) -> Option<&ArtifactValue> {
129        self.artifacts.get(id)
130    }
131
132    /// Resolve a material ID to a display name (title or ID).
133    /// Returns `None` if the ID is not a material.
134    #[must_use]
135    pub fn resolve_material(&self, material_id: &MaterialId) -> Option<&str> {
136        self.materials.get(material_id).map(String::as_str)
137    }
138}
139
140/// Result of executing a step.
141///
142/// Handlers return this instead of mutating context. The executor uses
143/// it to accumulate state.
144#[derive(Debug)]
145pub struct StepResult {
146    /// Step outcome (completed or skipped).
147    pub outcome: StepOutcome,
148    /// Artifacts produced by this step.
149    pub artifacts: Vec<(ArtifactId, ArtifactValue)>,
150}
151
152impl StepResult {
153    /// Create a completed result with no artifacts.
154    pub fn completed(message: impl Into<String>) -> Self {
155        Self {
156            outcome: StepOutcome::Completed {
157                message: message.into(),
158            },
159            artifacts: Vec::new(),
160        }
161    }
162
163    /// Create a completed result with one artifact.
164    pub fn completed_with_artifact(
165        message: impl Into<String>,
166        id: ArtifactId,
167        value: ArtifactValue,
168    ) -> Self {
169        Self {
170            outcome: StepOutcome::Completed {
171                message: message.into(),
172            },
173            artifacts: vec![(id, value)],
174        }
175    }
176
177    /// Add an artifact to the result.
178    #[must_use]
179    pub fn with_artifact(mut self, id: ArtifactId, value: ArtifactValue) -> Self {
180        self.artifacts.push((id, value));
181        self
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn handler_context_is_read_only_view() {
191        let mut artifacts = HashMap::new();
192        artifacts.insert(
193            ArtifactId::new("test"),
194            ArtifactValue::Bytes(b"value".to_vec()),
195        );
196
197        let state = ExecutionState {
198            params: HashMap::new(),
199            roles: HashMap::new(),
200            materials: HashMap::new(),
201            dry_run: true,
202            artifacts,
203        };
204
205        let ctx = state.handler_context();
206
207        assert!(ctx.get_artifact(&ArtifactId::new("test")).is_some());
208        assert!(ctx.get_artifact(&ArtifactId::new("missing")).is_none());
209    }
210
211    #[test]
212    fn step_result_builder_pattern() {
213        let result = StepResult::completed("Done")
214            .with_artifact(ArtifactId::new("a"), ArtifactValue::Bytes(b"1".to_vec()))
215            .with_artifact(ArtifactId::new("b"), ArtifactValue::Bytes(b"2".to_vec()));
216
217        assert_eq!(result.artifacts.len(), 2);
218    }
219}