molten_core/workflow.rs
1//! This module defines the core structures for managing workflow definitions,
2//! which dictate the lifecycle and state transitions of documents within the
3//! Molten system.
4//!
5//! It includes `Phase` and `Transition` to model the states and movements
6//! within a workflow, `WorkflowDefinition` to represent a complete state machine,
7//! and `WorkflowBuilder` for programmatic construction and validation of workflows.
8use serde::{Deserialize, Serialize};
9use std::collections::HashSet;
10use std::convert::TryFrom;
11use validator::{Validate, ValidationError};
12
13// -----------------------------------------------------------------------------
14// Enums & Sub-Structs
15// -----------------------------------------------------------------------------
16
17/// Defines the behavior of a specific phase.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum PhaseType {
21 /// The entry point of the workflow. There should typically be only one.
22 Start,
23 /// A standard working state (e.g., "Draft", "Under Review").
24 Normal,
25 /// A terminal state (e.g., "Approved", "Rejected", "Void").
26 End,
27}
28
29/// A single state within the workflow.
30#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
31pub struct Phase {
32 /// Unique identifier for this phase (e.g., "draft").
33 #[validate(length(min = 1, max = 64))]
34 pub id: String,
35
36 /// Human-readable name (e.g., "Draft Mode").
37 #[validate(length(min = 1, max = 100))]
38 pub label: String,
39
40 /// The behavior type of this phase.
41 #[serde(rename = "type")]
42 pub phase_type: PhaseType,
43}
44
45impl Phase {
46 /// Creates a new `Phase` instance.
47 ///
48 /// # Arguments
49 /// * `id` - The unique identifier for the phase.
50 /// * `label` - The human-readable name for the phase.
51 /// * `phase_type` - The type of the phase (e.g., Start, Normal, End).
52 pub fn new(id: &str, label: &str, phase_type: PhaseType) -> Self {
53 Self {
54 id: id.to_string(),
55 label: label.to_string(),
56 phase_type,
57 }
58 }
59}
60
61/// A directed edge between two Phases.
62/// Represents a valid movement from `from_phase` to `to_phase`.
63#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
64pub struct Transition {
65 /// The name of the action (e.g., "Submit for Review").
66 #[validate(length(min = 1, max = 100))]
67 pub name: String,
68
69 /// The ID of the source phase.
70 #[validate(length(min = 1, max = 64))]
71 pub from: String,
72
73 /// The ID of the target phase.
74 #[validate(length(min = 1, max = 64))]
75 pub to: String,
76 // Future expansion: We will add "guards" or "permissions" here later.
77 // e.g., pub required_role: Option<String>
78
79 // TODO: Add required_fields for a transition to be valid.
80 // This is different from the global is_required in Field definition
81 // #[serde(default)]
82 // pub required_fields: Vec<String>,
83}
84
85impl Transition {
86 /// Creates a new `Transition` instance.
87 ///
88 /// # Arguments
89 /// * `name` - The name of the action represented by this transition.
90 /// * `from` - The ID of the source phase.
91 /// * `to` - The ID of the target phase.
92 pub fn new(name: &str, from: &str, to: &str) -> Self {
93 Self {
94 name: name.to_string(),
95 from: from.to_string(),
96 to: to.to_string(),
97 }
98 }
99}
100
101// -----------------------------------------------------------------------------
102// Workflow Definition (The Graph)
103// -----------------------------------------------------------------------------
104
105/// Defines the complete state machine.
106#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
107#[serde(try_from = "WorkflowBuilder")]
108pub struct WorkflowDefinition {
109 /// The unique identifier for this workflow.
110 #[validate(length(min = 1, max = 64))]
111 id: String,
112
113 /// Human-readable name for the workflow.
114 #[validate(length(min = 1, max = 100))]
115 name: String,
116
117 /// All available phases (states) within this workflow.
118 #[validate(nested)]
119 phases: Vec<Phase>,
120
121 /// All allowed transitions (movements) between phases.
122 #[validate(nested)]
123 transitions: Vec<Transition>,
124}
125
126/// Trait for querying workflow capability.
127pub trait WorkflowGraph {
128 /// Returns true if a transition exists from `current_phase` to `target_phase`.
129 fn can_transition(&self, current_phase: &str, target_phase: &str) -> bool;
130
131 /// Returns the Phase definition for a given ID.
132 fn get_phase(&self, phase_id: &str) -> Option<&Phase>;
133
134 /// Returns the starting phase of the workflow.
135 fn get_start_phase(&self) -> Option<&Phase>;
136}
137
138impl WorkflowGraph for WorkflowDefinition {
139 fn can_transition(&self, current_phase: &str, target_phase: &str) -> bool {
140 self.transitions
141 .iter()
142 .any(|t| t.from == current_phase && t.to == target_phase)
143 }
144
145 fn get_phase(&self, phase_id: &str) -> Option<&Phase> {
146 self.phases.iter().find(|p| p.id == phase_id)
147 }
148
149 fn get_start_phase(&self) -> Option<&Phase> {
150 self.phases
151 .iter()
152 .find(|p| p.phase_type == PhaseType::Start)
153 }
154}
155
156impl WorkflowDefinition {
157 /// Returns the ID of the workflow.
158 pub fn id(&self) -> &str {
159 &self.id
160 }
161 /// Returns the human-readable name of the workflow.
162 pub fn name(&self) -> &str {
163 &self.name
164 }
165 /// Returns a slice of all phases in the workflow.
166 pub fn phases(&self) -> &[Phase] {
167 &self.phases
168 }
169 /// Returns a slice of all transitions in the workflow.
170 pub fn transitions(&self) -> &[Transition] {
171 &self.transitions
172 }
173}
174
175// -----------------------------------------------------------------------------
176// Validation Logic
177// -----------------------------------------------------------------------------
178
179// TODO: Implement additional validations for transitions, such as rules for phase types
180/// Ensures that all transitions in a `WorkflowDefinition` refer to valid, existing phases.
181///
182/// This validation prevents transitions from or to non-existent phases, ensuring the
183/// integrity and consistency of the workflow graph.
184///
185/// # Arguments
186/// * `definition` - A reference to the `WorkflowDefinition` to validate.
187///
188/// # Returns
189/// A `Result` which is `Ok` if all transitions are valid, or `Err` with
190/// `validator::ValidationErrors` if any invalid transitions are found.
191fn validate_workflow_integrity(
192 definition: &WorkflowDefinition,
193) -> Result<(), validator::ValidationErrors> {
194 let phase_ids: HashSet<&String> = definition.phases.iter().map(|p| &p.id).collect();
195
196 let mut errors = validator::ValidationErrors::new();
197
198 for transition in definition.transitions.iter() {
199 if !phase_ids.contains(&transition.from) {
200 let mut err = ValidationError::new("invalid_transition_source");
201 err.add_param("phase_id".into(), &transition.from);
202 errors.add("transitions", err);
203 }
204
205 if !phase_ids.contains(&transition.to) {
206 let mut err = ValidationError::new("invalid_transition_target");
207 err.add_param("phase_id".into(), &transition.to);
208 errors.add("transitions", err);
209 }
210 }
211
212 if errors.is_empty() {
213 Ok(())
214 } else {
215 Err(errors)
216 }
217}
218
219// -----------------------------------------------------------------------------
220// Builder & Deserialization
221// -----------------------------------------------------------------------------
222
223/// Builder for constructing validated [`WorkflowDefinition`] instances.
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct WorkflowBuilder {
226 /// The unique identifier for the workflow.
227 pub id: String,
228 /// Human-readable name for the workflow.
229 pub name: String,
230 #[serde(default)]
231 /// The phases that make up this workflow.
232 pub phases: Vec<Phase>,
233 #[serde(default)]
234 /// The transitions between phases in this workflow.
235 pub transitions: Vec<Transition>,
236}
237
238impl WorkflowBuilder {
239 /// Creates a new `WorkflowBuilder` instance with the given ID and name,
240 /// defaulting phases and transitions to empty lists.
241 pub fn new(id: &str, name: &str) -> Self {
242 Self {
243 id: id.to_string(),
244 name: name.to_string(),
245 phases: Vec::new(),
246 transitions: Vec::new(),
247 }
248 }
249
250 /// Adds a `Phase` to the workflow.
251 pub fn add_phase(mut self, phase: Phase) -> Self {
252 self.phases.push(phase);
253 self
254 }
255
256 /// Adds a `Transition` to the workflow.
257 pub fn add_transition(mut self, transition: Transition) -> Self {
258 self.transitions.push(transition);
259 self
260 }
261
262 /// Builds a validated `WorkflowDefinition` from the `WorkflowBuilder` instance.
263 ///
264 /// # Returns
265 /// A `Result` containing the `WorkflowDefinition` if valid, or a
266 /// `validator::ValidationErrors` if validation fails.
267 pub fn build(self) -> Result<WorkflowDefinition, validator::ValidationErrors> {
268 WorkflowDefinition::try_from(self)
269 }
270}
271
272impl TryFrom<WorkflowBuilder> for WorkflowDefinition {
273 type Error = validator::ValidationErrors;
274
275 fn try_from(builder: WorkflowBuilder) -> Result<Self, Self::Error> {
276 let wf = WorkflowDefinition {
277 id: builder.id,
278 name: builder.name,
279 phases: builder.phases,
280 transitions: builder.transitions,
281 };
282
283 // 1. Standard Field Validation
284 wf.validate()?;
285
286 // 2. Graph Integrity Validation (Custom Logic)
287 validate_workflow_integrity(&wf)?;
288
289 Ok(wf)
290 }
291}
292
293// -----------------------------------------------------------------------------
294// Tests
295// -----------------------------------------------------------------------------
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 #[test]
302 fn test_workflow_integrity() {
303 let res = WorkflowBuilder::new("wf_1", "Simple Workflow")
304 .add_phase(Phase::new("start", "Start", PhaseType::Start))
305 .add_phase(Phase::new("end", "End", PhaseType::End))
306 // Valid transition
307 .add_transition(Transition::new("finish", "start", "end"))
308 .build();
309
310 assert!(res.is_ok());
311 let wf = res.unwrap();
312 assert!(wf.can_transition("start", "end"));
313 assert!(!wf.can_transition("end", "start")); // One way!
314 }
315
316 #[test]
317 fn test_broken_reference() {
318 let res = WorkflowBuilder::new("wf_bad", "Broken Workflow")
319 .add_phase(Phase::new("start", "Start", PhaseType::Start))
320 // Transition to 'end', but 'end' phase is not added!
321 .add_transition(Transition::new("finish", "start", "end"))
322 .build();
323
324 assert!(res.is_err());
325 let err_msg = res.unwrap_err().to_string();
326 assert!(err_msg.contains("invalid_transition_target"));
327 }
328
329 #[test]
330 fn test_get_start_phase() {
331 let wf = WorkflowBuilder::new("wf_1", "Test")
332 .add_phase(Phase::new("draft", "Draft", PhaseType::Start))
333 .build()
334 .unwrap();
335
336 let start = wf.get_start_phase().unwrap();
337 assert_eq!(start.id, "draft");
338 assert!(matches!(start.phase_type, PhaseType::Start));
339 }
340}