use std::io::Read;
use std::path::{Path, PathBuf};
use crate::error::ZigError;
use crate::workflow::model::Workflow;
pub fn parse(content: &str) -> Result<Workflow, ZigError> {
let workflow: Workflow = toml::from_str(content).map_err(|e| ZigError::Parse(e.to_string()))?;
Ok(workflow)
}
pub fn parse_file(path: &Path) -> Result<Workflow, ZigError> {
let content = std::fs::read_to_string(path)
.map_err(|e| ZigError::Io(format!("failed to read {}: {e}", path.display())))?;
parse(&content)
}
pub fn parse_workflow(path: &Path) -> Result<(Workflow, WorkflowSource), ZigError> {
if is_zip_archive(path)? {
parse_zip(path)
} else {
let content = std::fs::read_to_string(path)
.map_err(|e| ZigError::Io(format!("failed to read {}: {e}", path.display())))?;
let wf = parse(&content)?;
let dir = path
.parent()
.unwrap_or_else(|| Path::new("."))
.to_path_buf();
Ok((wf, WorkflowSource::Directory(dir)))
}
}
#[derive(Debug)]
pub enum WorkflowSource {
Directory(PathBuf),
Zip {
_temp_dir: tempfile::TempDir,
extract_dir: PathBuf,
},
}
impl WorkflowSource {
pub fn dir(&self) -> &Path {
match self {
WorkflowSource::Directory(dir) => dir,
WorkflowSource::Zip { extract_dir, .. } => extract_dir,
}
}
}
fn is_zip_archive(path: &Path) -> Result<bool, ZigError> {
let mut file = std::fs::File::open(path)
.map_err(|e| ZigError::Io(format!("failed to open {}: {e}", path.display())))?;
let mut magic = [0u8; 4];
match file.read_exact(&mut magic) {
Ok(()) => Ok(&magic == b"PK\x03\x04"),
Err(_) => Ok(false), }
}
pub fn extract_zip(archive_path: &Path, dest: &Path) -> Result<(), ZigError> {
let file = std::fs::File::open(archive_path)
.map_err(|e| ZigError::Io(format!("failed to open {}: {e}", archive_path.display())))?;
let mut archive = zip::ZipArchive::new(file)
.map_err(|e| ZigError::Parse(format!("failed to read zip archive: {e}")))?;
for i in 0..archive.len() {
let mut entry = archive
.by_index(i)
.map_err(|e| ZigError::Parse(format!("failed to read zip entry: {e}")))?;
let out_path = dest.join(
entry
.enclosed_name()
.ok_or_else(|| ZigError::Parse("zip entry has invalid path".into()))?,
);
if entry.is_dir() {
std::fs::create_dir_all(&out_path).map_err(|e| {
ZigError::Io(format!(
"failed to create directory {}: {e}",
out_path.display()
))
})?;
} else {
if let Some(parent) = out_path.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
ZigError::Io(format!(
"failed to create directory {}: {e}",
parent.display()
))
})?;
}
let mut outfile = std::fs::File::create(&out_path).map_err(|e| {
ZigError::Io(format!("failed to create file {}: {e}", out_path.display()))
})?;
std::io::copy(&mut entry, &mut outfile).map_err(|e| {
ZigError::Io(format!("failed to extract {}: {e}", out_path.display()))
})?;
}
}
Ok(())
}
fn parse_zip(path: &Path) -> Result<(Workflow, WorkflowSource), ZigError> {
let temp_dir = tempfile::TempDir::new()
.map_err(|e| ZigError::Io(format!("failed to create temp directory: {e}")))?;
extract_zip(path, temp_dir.path())?;
let toml_files: Vec<PathBuf> = find_workflow_files(temp_dir.path())?;
if toml_files.is_empty() {
return Err(ZigError::Parse(
"zip archive contains no .toml or .zwf workflow file".into(),
));
}
if toml_files.len() > 1 {
return Err(ZigError::Parse(format!(
"zip archive contains {} workflow files (expected exactly one): {}",
toml_files.len(),
toml_files
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(", ")
)));
}
let toml_path = &toml_files[0];
let content = std::fs::read_to_string(toml_path)
.map_err(|e| ZigError::Io(format!("failed to read {}: {e}", toml_path.display())))?;
let wf = parse(&content)?;
let extract_dir = toml_path.parent().unwrap_or(temp_dir.path()).to_path_buf();
Ok((
wf,
WorkflowSource::Zip {
_temp_dir: temp_dir,
extract_dir,
},
))
}
pub fn find_workflow_files(dir: &Path) -> Result<Vec<PathBuf>, ZigError> {
let mut results = Vec::new();
fn scan_dir(dir: &Path, results: &mut Vec<PathBuf>, depth: usize) -> Result<(), ZigError> {
let entries = std::fs::read_dir(dir).map_err(|e| {
ZigError::Io(format!("failed to read directory {}: {e}", dir.display()))
})?;
for entry in entries {
let entry =
entry.map_err(|e| ZigError::Io(format!("failed to read directory entry: {e}")))?;
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if ext == "toml" || ext == "zwf" {
if let Ok(content) = std::fs::read_to_string(&path) {
if content.contains("[workflow]") {
results.push(path);
}
}
}
}
} else if path.is_dir() && depth < 1 {
scan_dir(&path, results, depth + 1)?;
}
}
Ok(())
}
scan_dir(dir, &mut results, 0)?;
Ok(results)
}
pub fn to_toml(workflow: &Workflow) -> Result<String, ZigError> {
toml::to_string_pretty(workflow).map_err(|e| ZigError::Serialize(e.to_string()))
}
#[cfg(test)]
#[path = "parser_tests.rs"]
mod tests;