use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use nab::site::wasm_manifest::{
InstalledProvider, WasmManifest, load_installed_providers, manifest_path, provider_dir,
wasm_path, wasm_providers_dir, write_manifest,
};
#[allow(clippy::unnecessary_wraps)]
pub fn cmd_provider_list() -> Result<()> {
let base = wasm_providers_dir();
let providers = load_installed_providers(&base);
if providers.is_empty() {
println!("No WASM providers installed.");
println!();
println!("Install one with: nab provider install <url-or-path>");
return Ok(());
}
print_provider_table(&providers);
Ok(())
}
pub async fn cmd_provider_install(src: &str) -> Result<()> {
let base = wasm_providers_dir();
std::fs::create_dir_all(&base).with_context(|| format!("cannot create {}", base.display()))?;
let installed = install_provider(src, &base).await?;
println!(
"Installed '{}' v{} ({} URL pattern{})",
installed.manifest.name,
installed.manifest.version,
installed.manifest.url_patterns.len(),
if installed.manifest.url_patterns.len() == 1 {
""
} else {
"s"
}
);
Ok(())
}
pub fn cmd_provider_remove(name: &str) -> Result<()> {
let base = wasm_providers_dir();
let dir = provider_dir(&base, name);
if !dir.exists() {
bail!(
"no installed provider named '{name}'\n\
Run 'nab provider list' to see installed providers."
);
}
if !manifest_path(&dir).exists() {
bail!(
"directory {} does not contain a manifest.toml — refusing to remove",
dir.display()
);
}
std::fs::remove_dir_all(&dir).with_context(|| format!("failed to remove {}", dir.display()))?;
println!("Removed provider '{name}'.");
Ok(())
}
async fn install_provider(src: &str, base_dir: &Path) -> Result<InstalledProvider> {
let src_path = PathBuf::from(src);
if src_path.is_dir() {
install_from_dir(&src_path, base_dir)
} else if src_path.is_file() {
install_from_wasm_file(&src_path, base_dir)
} else if src.starts_with("http://") || src.starts_with("https://") {
install_from_url(src, base_dir).await
} else {
bail!(
"cannot find '{src}': not a directory, file, or HTTP URL.\n\
Provide a local path to a provider directory or .wasm file, or an HTTP URL."
)
}
}
fn install_from_dir(src_dir: &Path, base_dir: &Path) -> Result<InstalledProvider> {
use nab::site::wasm_manifest::load_single_provider;
let src_provider = load_single_provider(src_dir)?;
let dest_dir = provider_dir(base_dir, &src_provider.manifest.name);
ensure_not_already_installed(&src_provider.manifest.name, &dest_dir)?;
std::fs::create_dir_all(&dest_dir)
.with_context(|| format!("cannot create {}", dest_dir.display()))?;
write_manifest(&dest_dir, &src_provider.manifest)?;
copy_wasm(&src_provider.wasm_path, &dest_dir)?;
load_installed_from_dest(&dest_dir)
}
fn install_from_wasm_file(wasm_file: &Path, base_dir: &Path) -> Result<InstalledProvider> {
let manifest_path = wasm_file.with_extension("manifest.toml");
if !manifest_path.exists() {
bail!(
"no manifest found for '{}'\n\
Expected a sidecar file at: {}",
wasm_file.display(),
manifest_path.display()
);
}
let manifest_str = std::fs::read_to_string(&manifest_path)
.with_context(|| format!("cannot read {}", manifest_path.display()))?;
let manifest: WasmManifest = toml::from_str(&manifest_str)
.with_context(|| format!("invalid TOML in {}", manifest_path.display()))?;
manifest.validate()?;
let dest_dir = provider_dir(base_dir, &manifest.name);
ensure_not_already_installed(&manifest.name, &dest_dir)?;
write_manifest(&dest_dir, &manifest)?;
copy_wasm(wasm_file, &dest_dir)?;
load_installed_from_dest(&dest_dir)
}
#[allow(clippy::case_sensitive_file_extension_comparisons)]
async fn install_from_url(url: &str, base_dir: &Path) -> Result<InstalledProvider> {
let base_url = url.strip_suffix(".wasm").unwrap_or(url);
let manifest_url = format!("{base_url}.manifest.toml");
let client = reqwest::Client::new();
let manifest_bytes = client
.get(&manifest_url)
.send()
.await
.with_context(|| format!("failed to fetch manifest from {manifest_url}"))?
.bytes()
.await
.context("failed to read manifest response")?;
let manifest: WasmManifest = toml::from_str(
std::str::from_utf8(&manifest_bytes).context("manifest response is not valid UTF-8")?,
)
.with_context(|| format!("invalid TOML manifest from {manifest_url}"))?;
manifest.validate()?;
let dest_dir = provider_dir(base_dir, &manifest.name);
ensure_not_already_installed(&manifest.name, &dest_dir)?;
let wasm_url = if url.ends_with(".wasm") {
url.to_string()
} else {
format!("{base_url}.wasm")
};
let wasm_bytes = client
.get(&wasm_url)
.send()
.await
.with_context(|| format!("failed to fetch WASM from {wasm_url}"))?
.bytes()
.await
.context("failed to read WASM response")?;
write_manifest(&dest_dir, &manifest)?;
let wasm_dest = wasm_path(&dest_dir);
std::fs::write(&wasm_dest, &wasm_bytes)
.with_context(|| format!("cannot write {}", wasm_dest.display()))?;
load_installed_from_dest(&dest_dir)
}
fn ensure_not_already_installed(name: &str, dest_dir: &Path) -> Result<()> {
if dest_dir.exists() {
bail!(
"provider '{name}' is already installed at {}\n\
Run 'nab provider remove {name}' first to replace it.",
dest_dir.display()
);
}
Ok(())
}
fn copy_wasm(src: &Path, dest_dir: &Path) -> Result<()> {
let dest = wasm_path(dest_dir);
std::fs::copy(src, &dest).with_context(|| {
format!(
"cannot copy WASM from {} to {}",
src.display(),
dest.display()
)
})?;
Ok(())
}
fn load_installed_from_dest(dest_dir: &Path) -> Result<InstalledProvider> {
use nab::site::wasm_manifest::load_single_provider;
load_single_provider(dest_dir)
}
fn print_provider_table(providers: &[InstalledProvider]) {
const H_NAME: &str = "Name";
const H_VER: &str = "Version";
const H_PAT: &str = "URL Patterns";
let w_name = providers
.iter()
.map(|p| p.manifest.name.len())
.max()
.unwrap_or(0)
.max(H_NAME.len());
let w_ver = providers
.iter()
.map(|p| p.manifest.version.len())
.max()
.unwrap_or(0)
.max(H_VER.len());
println!("{H_NAME:<w_name$} {H_VER:<w_ver$} {H_PAT}");
let sep = format!(
"{} {} {}",
"─".repeat(w_name),
"─".repeat(w_ver),
"─".repeat(H_PAT.len())
);
println!("{sep}");
for p in providers {
let patterns = p.manifest.url_patterns.join(", ");
println!(
"{:<w_name$} {:<w_ver$} {patterns}",
p.manifest.name, p.manifest.version
);
}
println!("{sep}");
let n = providers.len();
println!("{n} provider{}", if n == 1 { "" } else { "s" });
}
#[cfg(test)]
mod tests {
use super::*;
use nab::site::wasm_manifest::{WasmManifest, wasm_path as wp, write_manifest};
fn sample_manifest(name: &str) -> WasmManifest {
WasmManifest {
name: name.to_string(),
version: "1.0.0".to_string(),
description: "Test provider".to_string(),
author: "test@example.com".to_string(),
url_patterns: vec![r"example\.com".to_string()],
}
}
fn make_provider_dir(base: &Path, name: &str) -> PathBuf {
let dir = provider_dir(base, name);
write_manifest(&dir, &sample_manifest(name)).expect("write manifest");
std::fs::write(wp(&dir), b"\x00asm\x01\x00\x00\x00").expect("write wasm");
dir
}
#[test]
fn list_empty_when_no_providers() {
let base = PathBuf::from("/tmp/nab_wasm_test_empty_xyz");
let providers = load_installed_providers(&base);
assert!(providers.is_empty());
}
#[test]
fn install_from_dir_copies_files() {
let src_base = tempfile::tempdir().expect("src tempdir");
let dest_base = tempfile::tempdir().expect("dest tempdir");
let src_dir = make_provider_dir(src_base.path(), "test-provider");
let result = install_from_dir(&src_dir, dest_base.path());
assert!(result.is_ok());
let installed = result.unwrap();
assert_eq!(installed.manifest.name, "test-provider");
let dest_dir = provider_dir(dest_base.path(), "test-provider");
assert!(manifest_path(&dest_dir).exists());
assert!(wp(&dest_dir).exists());
}
#[test]
fn install_from_dir_rejects_duplicate() {
let src_base = tempfile::tempdir().expect("src tempdir");
let dest_base = tempfile::tempdir().expect("dest tempdir");
let src_dir = make_provider_dir(src_base.path(), "duplicate");
install_from_dir(&src_dir, dest_base.path()).expect("first install");
let result = install_from_dir(&src_dir, dest_base.path());
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("already installed"));
}
#[test]
fn install_from_wasm_file_with_sidecar_manifest() {
let src_dir = tempfile::tempdir().expect("tempdir");
let wasm_file = src_dir.path().join("provider.wasm");
let manifest_file = src_dir.path().join("provider.manifest.toml");
let dest_base = tempfile::tempdir().expect("dest tempdir");
let manifest = sample_manifest("sidecar-provider");
let toml_str = toml::to_string_pretty(&manifest).expect("serialise");
std::fs::write(&manifest_file, &toml_str).expect("write manifest");
std::fs::write(&wasm_file, b"\x00asm\x01\x00\x00\x00").expect("write wasm");
let result = install_from_wasm_file(&wasm_file, dest_base.path());
assert!(result.is_ok());
assert_eq!(result.unwrap().manifest.name, "sidecar-provider");
}
#[test]
fn install_from_wasm_file_fails_without_sidecar() {
let src_dir = tempfile::tempdir().expect("tempdir");
let wasm_file = src_dir.path().join("provider.wasm");
std::fs::write(&wasm_file, b"\x00asm\x01\x00\x00\x00").expect("write wasm");
let dest_base = tempfile::tempdir().expect("dest tempdir");
let result = install_from_wasm_file(&wasm_file, dest_base.path());
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("manifest found"));
}
#[test]
fn remove_installed_provider_deletes_directory() {
let base = tempfile::tempdir().expect("tempdir");
make_provider_dir(base.path(), "removable");
let dir = provider_dir(base.path(), "removable");
assert!(dir.exists());
std::fs::remove_dir_all(&dir).expect("remove");
assert!(!dir.exists());
}
#[test]
fn ensure_not_already_installed_passes_for_new_name() {
let base = tempfile::tempdir().expect("tempdir");
let dir = provider_dir(base.path(), "new-provider");
assert!(ensure_not_already_installed("new-provider", &dir).is_ok());
}
#[test]
fn ensure_not_already_installed_fails_for_existing_dir() {
let base = tempfile::tempdir().expect("tempdir");
let dir = provider_dir(base.path(), "existing");
std::fs::create_dir_all(&dir).expect("mkdir");
let err = ensure_not_already_installed("existing", &dir).unwrap_err();
assert!(err.to_string().contains("already installed"));
}
#[tokio::test]
async fn install_provider_fails_for_nonexistent_path() {
let dest_base = tempfile::tempdir().expect("tempdir");
let result = install_provider("/tmp/nab_nonexistent_provider_xyz", dest_base.path()).await;
assert!(result.is_err());
}
#[tokio::test]
async fn install_provider_dispatches_to_dir_path() {
let src_base = tempfile::tempdir().expect("src tempdir");
let dest_base = tempfile::tempdir().expect("dest tempdir");
make_provider_dir(src_base.path(), "dispatched-provider");
let src_dir_str = provider_dir(src_base.path(), "dispatched-provider")
.to_string_lossy()
.to_string();
let result = install_provider(&src_dir_str, dest_base.path()).await;
assert!(result.is_ok());
}
}