mod cargo;
mod packagejson;
mod pyproject;
use std::path::Path;
use act_types::ComponentInfo;
use anyhow::{Context, Result};
pub fn resolve(dir: &Path) -> Result<ComponentInfo> {
let mut info: Option<ComponentInfo> = None;
let cargo_path = dir.join("Cargo.toml");
let pyproject_path = dir.join("pyproject.toml");
if cargo_path.exists() {
let (base, inline_patch) = match cargo::from_cargo_metadata(dir) {
Ok(result) => result,
Err(_) => {
tracing::debug!("cargo metadata failed, falling back to raw TOML parsing");
cargo::from_toml(&cargo_path)?
}
};
info = Some(base);
if let Some(patch_val) = inline_patch {
let patch_json = serde_json::to_value(&patch_val)?;
info = Some(apply_merge_patch(info.unwrap(), &patch_json)?);
}
} else if pyproject_path.exists() {
let (base, inline_patch) = pyproject::from_toml(&pyproject_path)?;
info = Some(base);
if let Some(patch_val) = inline_patch {
let patch_json = serde_json::to_value(&patch_val)?;
info = Some(apply_merge_patch(info.unwrap(), &patch_json)?);
}
} else if dir.join("package.json").exists() {
let (base, inline_patch) = packagejson::from_json(&dir.join("package.json"))?;
info = Some(base);
if let Some(patch_val) = inline_patch {
info = Some(apply_merge_patch(info.unwrap(), &patch_val)?);
}
}
let act_toml_path = dir.join("act.toml");
if act_toml_path.exists() {
let raw = std::fs::read_to_string(&act_toml_path)
.with_context(|| format!("reading {}", act_toml_path.display()))?;
let doc: toml::Value =
toml::from_str(&raw).with_context(|| format!("parsing {}", act_toml_path.display()))?;
let patch_json = serde_json::to_value(&doc)?;
match info {
Some(existing) => info = Some(apply_merge_patch(existing, &patch_json)?),
None => {
info = Some(serde_json::from_value(patch_json).context("deserializing act.toml")?);
}
}
}
info.ok_or_else(|| {
anyhow::anyhow!(
"no component metadata found in {}; expected Cargo.toml, pyproject.toml, or act.toml",
dir.display()
)
})
}
fn apply_merge_patch(base: ComponentInfo, patch: &serde_json::Value) -> Result<ComponentInfo> {
let mut base_json = serde_json::to_value(&base).context("serializing base to JSON")?;
json_patch::merge(&mut base_json, patch);
serde_json::from_value(base_json).context("deserializing merged metadata")
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn act_toml_standalone() {
let dir = TempDir::new().unwrap();
fs::write(
dir.path().join("act.toml"),
"[std]\nname = \"my-component\"\nversion = \"1.0.0\"\ndescription = \"A standalone component\"\n",
).unwrap();
let info = resolve(dir.path()).unwrap();
assert_eq!(info.std.name, "my-component");
assert_eq!(info.std.version, "1.0.0");
assert_eq!(info.std.description, "A standalone component");
}
#[test]
fn cargo_toml_base_with_act_toml_patch() {
let dir = TempDir::new().unwrap();
fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-crate\"\nversion = \"0.2.0\"\nedition = \"2024\"\ndescription = \"A Rust component\"\n",
).unwrap();
fs::write(
dir.path().join("act.toml"),
"[std.capabilities.\"wasi:http\"]\n",
)
.unwrap();
let info = resolve(dir.path()).unwrap();
assert_eq!(info.std.name, "my-crate");
assert_eq!(info.std.version, "0.2.0");
assert_eq!(info.std.description, "A Rust component");
assert!(info.std.capabilities.has("wasi:http"));
}
#[test]
fn cargo_toml_inline_metadata() {
let dir = TempDir::new().unwrap();
fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"inline-test\"\nversion = \"0.3.0\"\nedition = \"2024\"\ndescription = \"Inline metadata test\"\n\n[package.metadata.act-component.std.capabilities.\"wasi:http\"]\n",
).unwrap();
let info = resolve(dir.path()).unwrap();
assert_eq!(info.std.name, "inline-test");
assert_eq!(info.std.version, "0.3.0");
assert!(info.std.capabilities.has("wasi:http"));
}
#[test]
fn pyproject_toml_base_with_inline() {
let dir = TempDir::new().unwrap();
fs::write(
dir.path().join("pyproject.toml"),
"[project]\nname = \"py-component\"\nversion = \"0.1.0\"\ndescription = \"A Python component\"\n\n[tool.act-component.std.capabilities.\"wasi:filesystem\"]\n",
).unwrap();
let info = resolve(dir.path()).unwrap();
assert_eq!(info.std.name, "py-component");
assert_eq!(info.std.version, "0.1.0");
assert_eq!(info.std.description, "A Python component");
assert!(info.std.capabilities.has("wasi:filesystem"));
}
#[test]
fn no_metadata_is_error() {
let dir = TempDir::new().unwrap();
let result = resolve(dir.path());
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("no component metadata found")
);
}
}