Skip to main content

libbrat_workflow/
parser.rs

1//! Workflow parser and loader.
2
3use std::collections::HashMap;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7use crate::error::WorkflowError;
8use crate::schema::WorkflowTemplate;
9
10/// Parser for loading workflow templates from YAML files.
11pub struct WorkflowParser {
12    /// Root directory containing workflows (usually `.brat/workflows/`).
13    workflows_dir: PathBuf,
14}
15
16impl WorkflowParser {
17    /// Create a new parser for the given workflows directory.
18    pub fn new(workflows_dir: impl Into<PathBuf>) -> Self {
19        Self {
20            workflows_dir: workflows_dir.into(),
21        }
22    }
23
24    /// Create a parser from a repository root.
25    ///
26    /// Looks for workflows in `.brat/workflows/`.
27    pub fn from_repo_root(repo_root: impl AsRef<Path>) -> Self {
28        Self::new(repo_root.as_ref().join(".brat").join("workflows"))
29    }
30
31    /// Get the workflows directory path.
32    pub fn workflows_dir(&self) -> &Path {
33        &self.workflows_dir
34    }
35
36    /// Check if the workflows directory exists.
37    pub fn workflows_dir_exists(&self) -> bool {
38        self.workflows_dir.is_dir()
39    }
40
41    /// List available workflow names.
42    pub fn list_workflows(&self) -> Result<Vec<String>, WorkflowError> {
43        if !self.workflows_dir.is_dir() {
44            return Ok(Vec::new());
45        }
46
47        let mut workflows = Vec::new();
48        for entry in fs::read_dir(&self.workflows_dir)? {
49            let entry = entry?;
50            let path = entry.path();
51            if path.is_file() {
52                if let Some(ext) = path.extension() {
53                    if ext == "yaml" || ext == "yml" {
54                        if let Some(stem) = path.file_stem() {
55                            workflows.push(stem.to_string_lossy().to_string());
56                        }
57                    }
58                }
59            }
60        }
61        workflows.sort();
62        Ok(workflows)
63    }
64
65    /// Load a workflow by name.
66    pub fn load(&self, name: &str) -> Result<WorkflowTemplate, WorkflowError> {
67        let path = self.find_workflow_path(name)?;
68        self.load_from_path(&path)
69    }
70
71    /// Load a workflow from a specific path.
72    pub fn load_from_path(&self, path: &Path) -> Result<WorkflowTemplate, WorkflowError> {
73        let content = fs::read_to_string(path)?;
74        let template: WorkflowTemplate = serde_yaml::from_str(&content)?;
75
76        // Validate the template
77        template
78            .validate()
79            .map_err(WorkflowError::ValidationError)?;
80
81        Ok(template)
82    }
83
84    /// Find the path to a workflow file by name.
85    fn find_workflow_path(&self, name: &str) -> Result<PathBuf, WorkflowError> {
86        // Try .yaml extension first
87        let yaml_path = self.workflows_dir.join(format!("{}.yaml", name));
88        if yaml_path.is_file() {
89            return Ok(yaml_path);
90        }
91
92        // Try .yml extension
93        let yml_path = self.workflows_dir.join(format!("{}.yml", name));
94        if yml_path.is_file() {
95            return Ok(yml_path);
96        }
97
98        Err(WorkflowError::NotFound(name.to_string()))
99    }
100
101    /// Substitute variables in a template string.
102    ///
103    /// Replaces `{{var}}` with the corresponding value from the vars map.
104    pub fn substitute_vars(template: &str, vars: &HashMap<String, String>) -> String {
105        let mut result = template.to_string();
106        for (key, value) in vars {
107            let pattern = format!("{{{{{}}}}}", key);
108            result = result.replace(&pattern, value);
109        }
110        result
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn test_substitute_vars() {
120        let vars: HashMap<String, String> = [
121            ("name".to_string(), "Alice".to_string()),
122            ("count".to_string(), "42".to_string()),
123        ]
124        .into_iter()
125        .collect();
126
127        assert_eq!(
128            WorkflowParser::substitute_vars("Hello {{name}}", &vars),
129            "Hello Alice"
130        );
131        assert_eq!(
132            WorkflowParser::substitute_vars("Count: {{count}} items", &vars),
133            "Count: 42 items"
134        );
135        assert_eq!(
136            WorkflowParser::substitute_vars("{{name}} has {{count}}", &vars),
137            "Alice has 42"
138        );
139        assert_eq!(
140            WorkflowParser::substitute_vars("No vars here", &vars),
141            "No vars here"
142        );
143    }
144}