use std::path::{Path, PathBuf};
use crate::registry::error::LoaderError;
#[derive(Debug, Clone)]
pub struct ExtractedPythonPackage {
pub root_dir: PathBuf,
pub vendor_dir: PathBuf,
pub workflow_dir: PathBuf,
pub entry_module: String,
pub package_name: String,
pub version: String,
pub workflow_name: String,
}
pub enum PackageKind {
Python {
workflow_name: String,
package_name: String,
version: String,
},
Rust {
workflow_name: String,
package_name: String,
version: String,
},
}
pub fn detect_package_kind(archive_data: &[u8]) -> Result<PackageKind, LoaderError> {
let tmp = tempfile::TempDir::new().map_err(|e| LoaderError::FileSystem {
path: "tempdir".to_string(),
error: e.to_string(),
})?;
let archive_path = tmp.path().join("pkg.cloacina");
std::fs::write(&archive_path, archive_data).map_err(|e| LoaderError::FileSystem {
path: archive_path.display().to_string(),
error: e.to_string(),
})?;
let extract_dir = tmp.path().join("extract");
std::fs::create_dir_all(&extract_dir).map_err(|e| LoaderError::FileSystem {
path: extract_dir.display().to_string(),
error: e.to_string(),
})?;
let source_dir =
fidius_core::package::unpack_package(&archive_path, &extract_dir).map_err(|e| {
LoaderError::MetadataExtraction {
reason: format!("Failed to unpack source archive: {e}"),
}
})?;
let manifest =
fidius_core::package::load_manifest::<cloacina_workflow_plugin::CloacinaMetadata>(
&source_dir,
)
.map_err(|e| LoaderError::ManifestParse {
reason: format!("Failed to parse package.toml: {e}"),
})?;
let pkg = &manifest.package;
let meta = &manifest.metadata;
match meta.language.as_str() {
"python" => Ok(PackageKind::Python {
workflow_name: meta.workflow_name.clone(),
package_name: pkg.name.clone(),
version: pkg.version.clone(),
}),
_ => Ok(PackageKind::Rust {
workflow_name: meta.workflow_name.clone(),
package_name: pkg.name.clone(),
version: pkg.version.clone(),
}),
}
}
pub fn extract_python_package(
archive_data: &[u8],
staging_dir: &Path,
) -> Result<ExtractedPythonPackage, LoaderError> {
let archive_path = staging_dir.join(format!("{}.cloacina", uuid::Uuid::new_v4()));
std::fs::write(&archive_path, archive_data).map_err(|e| LoaderError::FileSystem {
path: archive_path.display().to_string(),
error: e.to_string(),
})?;
let extract_dir = staging_dir.join(uuid::Uuid::new_v4().to_string());
std::fs::create_dir_all(&extract_dir).map_err(|e| LoaderError::FileSystem {
path: extract_dir.display().to_string(),
error: e.to_string(),
})?;
let source_dir =
fidius_core::package::unpack_package(&archive_path, &extract_dir).map_err(|e| {
LoaderError::FileSystem {
path: archive_path.display().to_string(),
error: format!("Failed to unpack source archive: {e}"),
}
})?;
let manifest =
fidius_core::package::load_manifest::<cloacina_workflow_plugin::CloacinaMetadata>(
&source_dir,
)
.map_err(|e| LoaderError::ManifestParse {
reason: format!("Failed to parse package.toml: {e}"),
})?;
if manifest.metadata.language != "python" {
return Err(LoaderError::WrongLanguage {
expected: "python".to_string(),
actual: manifest.metadata.language.clone(),
});
}
let entry_module = manifest
.metadata
.entry_module
.as_ref()
.ok_or(LoaderError::MissingPythonConfig)?
.clone();
let vendor_dir = source_dir.join("vendor");
let workflow_dir = source_dir.join("workflow");
if !workflow_dir.exists() {
return Err(LoaderError::MissingSourceDir);
}
let _ = std::fs::remove_file(&archive_path);
Ok(ExtractedPythonPackage {
root_dir: source_dir,
vendor_dir,
workflow_dir,
entry_module,
package_name: manifest.package.name,
version: manifest.package.version,
workflow_name: manifest.metadata.workflow_name,
})
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn create_python_source_package(
dir: &Path,
name: &str,
include_workflow: bool,
) -> std::path::PathBuf {
let package_toml = format!(
r#"[package]
name = "{name}"
version = "0.1.0"
interface = "cloacina-workflow-plugin"
interface_version = 1
extension = "cloacina"
[metadata]
workflow_name = "test_workflow"
language = "python"
description = "Test Python workflow"
requires_python = ">=3.10"
entry_module = "workflow.tasks"
"#
);
std::fs::write(dir.join("package.toml"), package_toml).unwrap();
if include_workflow {
std::fs::create_dir_all(dir.join("workflow")).unwrap();
std::fs::write(dir.join("workflow/__init__.py"), "# workflow init\n").unwrap();
std::fs::write(
dir.join("workflow/tasks.py"),
"def hello(ctx): return ctx\n",
)
.unwrap();
}
std::fs::create_dir_all(dir.join("vendor")).unwrap();
let output = dir.parent().unwrap().join(format!("{name}-0.1.0.cloacina"));
fidius_core::package::pack_package(dir, Some(&output)).unwrap();
output
}
#[test]
fn test_detect_package_kind_python() {
let tmp = TempDir::new().unwrap();
let pkg_dir = tmp.path().join("pkg");
std::fs::create_dir_all(&pkg_dir).unwrap();
let archive_path = create_python_source_package(&pkg_dir, "test-py", true);
let archive_data = std::fs::read(&archive_path).unwrap();
let kind = detect_package_kind(&archive_data).unwrap();
assert!(matches!(kind, PackageKind::Python { .. }));
}
#[test]
fn test_extract_python_package() {
let tmp = TempDir::new().unwrap();
let pkg_dir = tmp.path().join("pkg");
std::fs::create_dir_all(&pkg_dir).unwrap();
let archive_path = create_python_source_package(&pkg_dir, "test-py", true);
let archive_data = std::fs::read(&archive_path).unwrap();
let staging = TempDir::new().unwrap();
let extracted = extract_python_package(&archive_data, staging.path()).unwrap();
assert!(extracted.root_dir.exists());
assert!(extracted.workflow_dir.exists());
assert_eq!(extracted.entry_module, "workflow.tasks");
assert_eq!(extracted.package_name, "test-py");
assert_eq!(extracted.workflow_name, "test_workflow");
}
#[test]
fn test_extract_missing_workflow_dir() {
let tmp = TempDir::new().unwrap();
let pkg_dir = tmp.path().join("pkg");
std::fs::create_dir_all(&pkg_dir).unwrap();
let archive_path = create_python_source_package(&pkg_dir, "no-workflow", false);
let archive_data = std::fs::read(&archive_path).unwrap();
let staging = TempDir::new().unwrap();
let err = extract_python_package(&archive_data, staging.path()).unwrap_err();
assert!(matches!(err, LoaderError::MissingSourceDir));
}
#[test]
fn test_wrong_language_rejected() {
let tmp = TempDir::new().unwrap();
let pkg_dir = tmp.path().join("pkg");
std::fs::create_dir_all(&pkg_dir).unwrap();
let package_toml = r#"[package]
name = "rust-pkg"
version = "0.1.0"
interface = "cloacina-workflow-plugin"
interface_version = 1
extension = "cloacina"
[metadata]
workflow_name = "rust_workflow"
language = "rust"
"#;
std::fs::write(pkg_dir.join("package.toml"), package_toml).unwrap();
std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
std::fs::write(pkg_dir.join("src/lib.rs"), "// placeholder").unwrap();
let output = tmp.path().join("rust-pkg-0.1.0.cloacina");
fidius_core::package::pack_package(&pkg_dir, Some(&output)).unwrap();
let archive_data = std::fs::read(&output).unwrap();
let staging = TempDir::new().unwrap();
let err = extract_python_package(&archive_data, staging.path()).unwrap_err();
assert!(matches!(err, LoaderError::WrongLanguage { .. }));
}
}