zig_core/workflow/
parser.rs1use std::io::Read;
2use std::path::{Path, PathBuf};
3
4use crate::error::ZigError;
5use crate::workflow::model::Workflow;
6
7pub fn parse(content: &str) -> Result<Workflow, ZigError> {
9 let workflow: Workflow = toml::from_str(content).map_err(|e| ZigError::Parse(e.to_string()))?;
10 Ok(workflow)
11}
12
13pub fn parse_file(path: &Path) -> Result<Workflow, ZigError> {
21 let content = std::fs::read_to_string(path)
22 .map_err(|e| ZigError::Io(format!("failed to read {}: {e}", path.display())))?;
23 parse(&content)
24}
25
26pub fn parse_workflow(path: &Path) -> Result<(Workflow, WorkflowSource), ZigError> {
32 if is_zip_archive(path)? {
33 parse_zip(path)
34 } else {
35 let content = std::fs::read_to_string(path)
36 .map_err(|e| ZigError::Io(format!("failed to read {}: {e}", path.display())))?;
37 let wf = parse(&content)?;
38 let dir = path
39 .parent()
40 .unwrap_or_else(|| Path::new("."))
41 .to_path_buf();
42 Ok((wf, WorkflowSource::Directory(dir)))
43 }
44}
45
46#[derive(Debug)]
52pub enum WorkflowSource {
53 Directory(PathBuf),
55 Zip {
57 _temp_dir: tempfile::TempDir,
58 extract_dir: PathBuf,
59 },
60}
61
62impl WorkflowSource {
63 pub fn dir(&self) -> &Path {
65 match self {
66 WorkflowSource::Directory(dir) => dir,
67 WorkflowSource::Zip { extract_dir, .. } => extract_dir,
68 }
69 }
70}
71
72fn is_zip_archive(path: &Path) -> Result<bool, ZigError> {
74 let mut file = std::fs::File::open(path)
75 .map_err(|e| ZigError::Io(format!("failed to open {}: {e}", path.display())))?;
76 let mut magic = [0u8; 4];
77 match file.read_exact(&mut magic) {
78 Ok(()) => Ok(&magic == b"PK\x03\x04"),
79 Err(_) => Ok(false), }
81}
82
83pub fn extract_zip(archive_path: &Path, dest: &Path) -> Result<(), ZigError> {
89 let file = std::fs::File::open(archive_path)
90 .map_err(|e| ZigError::Io(format!("failed to open {}: {e}", archive_path.display())))?;
91 let mut archive = zip::ZipArchive::new(file)
92 .map_err(|e| ZigError::Parse(format!("failed to read zip archive: {e}")))?;
93
94 for i in 0..archive.len() {
95 let mut entry = archive
96 .by_index(i)
97 .map_err(|e| ZigError::Parse(format!("failed to read zip entry: {e}")))?;
98
99 let out_path = dest.join(
100 entry
101 .enclosed_name()
102 .ok_or_else(|| ZigError::Parse("zip entry has invalid path".into()))?,
103 );
104
105 if entry.is_dir() {
106 std::fs::create_dir_all(&out_path).map_err(|e| {
107 ZigError::Io(format!(
108 "failed to create directory {}: {e}",
109 out_path.display()
110 ))
111 })?;
112 } else {
113 if let Some(parent) = out_path.parent() {
114 std::fs::create_dir_all(parent).map_err(|e| {
115 ZigError::Io(format!(
116 "failed to create directory {}: {e}",
117 parent.display()
118 ))
119 })?;
120 }
121 let mut outfile = std::fs::File::create(&out_path).map_err(|e| {
122 ZigError::Io(format!("failed to create file {}: {e}", out_path.display()))
123 })?;
124 std::io::copy(&mut entry, &mut outfile).map_err(|e| {
125 ZigError::Io(format!("failed to extract {}: {e}", out_path.display()))
126 })?;
127 }
128 }
129
130 Ok(())
131}
132
133fn parse_zip(path: &Path) -> Result<(Workflow, WorkflowSource), ZigError> {
138 let temp_dir = tempfile::TempDir::new()
139 .map_err(|e| ZigError::Io(format!("failed to create temp directory: {e}")))?;
140
141 extract_zip(path, temp_dir.path())?;
142
143 let toml_files: Vec<PathBuf> = find_workflow_files(temp_dir.path())?;
145
146 if toml_files.is_empty() {
147 return Err(ZigError::Parse(
148 "zip archive contains no .toml or .zwf workflow file".into(),
149 ));
150 }
151 if toml_files.len() > 1 {
152 return Err(ZigError::Parse(format!(
153 "zip archive contains {} workflow files (expected exactly one): {}",
154 toml_files.len(),
155 toml_files
156 .iter()
157 .map(|p| p.display().to_string())
158 .collect::<Vec<_>>()
159 .join(", ")
160 )));
161 }
162
163 let toml_path = &toml_files[0];
164 let content = std::fs::read_to_string(toml_path)
165 .map_err(|e| ZigError::Io(format!("failed to read {}: {e}", toml_path.display())))?;
166 let wf = parse(&content)?;
167
168 let extract_dir = toml_path.parent().unwrap_or(temp_dir.path()).to_path_buf();
170
171 Ok((
172 wf,
173 WorkflowSource::Zip {
174 _temp_dir: temp_dir,
175 extract_dir,
176 },
177 ))
178}
179
180pub fn find_workflow_files(dir: &Path) -> Result<Vec<PathBuf>, ZigError> {
183 let mut results = Vec::new();
184
185 fn scan_dir(dir: &Path, results: &mut Vec<PathBuf>, depth: usize) -> Result<(), ZigError> {
186 let entries = std::fs::read_dir(dir).map_err(|e| {
187 ZigError::Io(format!("failed to read directory {}: {e}", dir.display()))
188 })?;
189
190 for entry in entries {
191 let entry =
192 entry.map_err(|e| ZigError::Io(format!("failed to read directory entry: {e}")))?;
193 let path = entry.path();
194
195 if path.is_file() {
196 if let Some(ext) = path.extension() {
197 if ext == "toml" || ext == "zwf" {
198 if let Ok(content) = std::fs::read_to_string(&path) {
200 if content.contains("[workflow]") {
201 results.push(path);
202 }
203 }
204 }
205 }
206 } else if path.is_dir() && depth < 1 {
207 scan_dir(&path, results, depth + 1)?;
208 }
209 }
210 Ok(())
211 }
212
213 scan_dir(dir, &mut results, 0)?;
214 Ok(results)
215}
216
217pub fn to_toml(workflow: &Workflow) -> Result<String, ZigError> {
219 toml::to_string_pretty(workflow).map_err(|e| ZigError::Serialize(e.to_string()))
220}
221
222#[cfg(test)]
223#[path = "parser_tests.rs"]
224mod tests;