Skip to main content

libbrat_workflow/
schema.rs

1//! Workflow YAML schema types.
2
3use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6
7/// A workflow template loaded from YAML.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct WorkflowTemplate {
10    /// Workflow name (used as identifier).
11    pub name: String,
12
13    /// Schema version.
14    #[serde(default = "default_version")]
15    pub version: u32,
16
17    /// Human-readable description.
18    #[serde(default)]
19    pub description: Option<String>,
20
21    /// Workflow type: sequential or parallel convoy.
22    #[serde(rename = "type")]
23    pub workflow_type: WorkflowType,
24
25    /// Input variable definitions.
26    #[serde(default)]
27    pub inputs: HashMap<String, InputSpec>,
28
29    /// Sequential steps (for `type: workflow`).
30    #[serde(default)]
31    pub steps: Vec<StepSpec>,
32
33    /// Parallel legs (for `type: convoy`).
34    #[serde(default)]
35    pub legs: Vec<LegSpec>,
36
37    /// Synthesis step (for `type: convoy`).
38    #[serde(default)]
39    pub synthesis: Option<SynthesisSpec>,
40}
41
42fn default_version() -> u32 {
43    1
44}
45
46/// Workflow type determines execution model.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "lowercase")]
49pub enum WorkflowType {
50    /// Sequential workflow with dependency ordering.
51    Workflow,
52    /// Parallel convoy with optional synthesis.
53    Convoy,
54}
55
56/// Input variable specification.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct InputSpec {
59    /// Human-readable description.
60    #[serde(default)]
61    pub description: Option<String>,
62
63    /// Whether this input is required.
64    #[serde(default)]
65    pub required: bool,
66
67    /// Default value if not provided.
68    #[serde(default)]
69    pub default: Option<String>,
70}
71
72/// A step in a sequential workflow.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct StepSpec {
75    /// Step identifier (unique within workflow).
76    pub id: String,
77
78    /// Task title (supports {{variable}} substitution).
79    pub title: String,
80
81    /// Task body/description (supports {{variable}} substitution).
82    #[serde(default)]
83    pub body: String,
84
85    /// IDs of steps this step depends on.
86    #[serde(default)]
87    pub needs: Vec<String>,
88
89    /// Optional labels to add to the task.
90    #[serde(default)]
91    pub labels: Vec<String>,
92}
93
94/// A leg in a parallel convoy.
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct LegSpec {
97    /// Leg identifier (unique within workflow).
98    pub id: String,
99
100    /// Task title (supports {{variable}} substitution).
101    pub title: String,
102
103    /// Task body/description (supports {{variable}} substitution).
104    #[serde(default)]
105    pub body: String,
106
107    /// Optional labels to add to the task.
108    #[serde(default)]
109    pub labels: Vec<String>,
110}
111
112/// Synthesis step that runs after all legs complete.
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct SynthesisSpec {
115    /// Task title (supports {{variable}} substitution).
116    pub title: String,
117
118    /// Task body/description (supports {{variable}} substitution).
119    #[serde(default)]
120    pub body: String,
121
122    /// IDs of legs this synthesis depends on (defaults to all legs).
123    #[serde(default)]
124    pub depends_on: Vec<String>,
125
126    /// Optional labels to add to the task.
127    #[serde(default)]
128    pub labels: Vec<String>,
129}
130
131impl WorkflowTemplate {
132    /// Validate the workflow template.
133    pub fn validate(&self) -> Result<(), String> {
134        // Check workflow has appropriate content for its type
135        match self.workflow_type {
136            WorkflowType::Workflow => {
137                if self.steps.is_empty() {
138                    return Err("workflow type requires at least one step".to_string());
139                }
140                if !self.legs.is_empty() {
141                    return Err("workflow type should not have legs".to_string());
142                }
143            }
144            WorkflowType::Convoy => {
145                if self.legs.is_empty() {
146                    return Err("convoy type requires at least one leg".to_string());
147                }
148                if !self.steps.is_empty() {
149                    return Err("convoy type should not have steps".to_string());
150                }
151            }
152        }
153
154        // Check for unique step/leg IDs
155        let mut ids = std::collections::HashSet::new();
156        for step in &self.steps {
157            if !ids.insert(&step.id) {
158                return Err(format!("duplicate step id: {}", step.id));
159            }
160        }
161        for leg in &self.legs {
162            if !ids.insert(&leg.id) {
163                return Err(format!("duplicate leg id: {}", leg.id));
164            }
165        }
166
167        // Check step dependencies reference valid steps
168        for step in &self.steps {
169            for dep in &step.needs {
170                if !self.steps.iter().any(|s| &s.id == dep) {
171                    return Err(format!(
172                        "step '{}' depends on unknown step '{}'",
173                        step.id, dep
174                    ));
175                }
176                if dep == &step.id {
177                    return Err(format!("step '{}' cannot depend on itself", step.id));
178                }
179            }
180        }
181
182        // Check synthesis dependencies reference valid legs
183        if let Some(ref synthesis) = self.synthesis {
184            for dep in &synthesis.depends_on {
185                if !self.legs.iter().any(|l| &l.id == dep) {
186                    return Err(format!(
187                        "synthesis depends on unknown leg '{}'",
188                        dep
189                    ));
190                }
191            }
192        }
193
194        Ok(())
195    }
196}