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