Skip to main content

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}