use anyhow::Result;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct InstantiateReport {
pub source: PathBuf,
pub out: PathBuf,
pub initial_rendered: usize,
pub deps_resolved: usize,
pub deps_failed: usize,
pub final_rendered: usize,
pub verify_passed: Option<bool>,
pub final_status: String,
}
impl InstantiateReport {
pub fn is_success(&self) -> bool {
self.deps_failed == 0
&& self.verify_passed.unwrap_or(true)
&& self.final_status.starts_with("instantiated")
}
pub fn to_json(&self) -> String {
use crate::json_ast::Value;
let mut o = Value::obj();
o.insert("source", Value::s(self.source.to_string_lossy().to_string()));
o.insert("out", Value::s(self.out.to_string_lossy().to_string()));
o.insert("initial-rendered", Value::i(self.initial_rendered as i64));
o.insert("deps-resolved", Value::i(self.deps_resolved as i64));
o.insert("deps-failed", Value::i(self.deps_failed as i64));
o.insert("final-rendered", Value::i(self.final_rendered as i64));
if let Some(v) = self.verify_passed { o.insert("verify-passed", Value::b(v)); }
o.insert("final-status", Value::s(&self.final_status));
o.insert("success", Value::b(self.is_success()));
crate::json_ast::render(&o)
}
}
#[derive(Debug, Clone)]
pub struct InstantiateConfig {
pub verify: bool,
pub skip_resolve: bool,
pub force_resolve: bool,
}
impl Default for InstantiateConfig {
fn default() -> Self { Self { verify: true, skip_resolve: false, force_resolve: false } }
}
pub fn instantiate(source_lisp: &Path, out: &Path, cfg: &InstantiateConfig) -> Result<InstantiateReport> {
let mut report = InstantiateReport {
source: source_lisp.to_path_buf(),
out: out.to_path_buf(),
initial_rendered: 0,
deps_resolved: 0,
deps_failed: 0,
final_rendered: 0,
verify_passed: None,
final_status: "starting".to_string(),
};
let lisp_src = std::fs::read_to_string(source_lisp)?;
std::fs::create_dir_all(out)?;
let initial = crate::caixa::render(&lisp_src, out, true)?;
report.initial_rendered = initial.len();
report.final_status = "rendered-initial".to_string();
if !cfg.skip_resolve {
let resolve_cfg = crate::caixa_deps::ResolveConfig {
force: cfg.force_resolve,
transitive: true,
max_depth: 5,
};
let resolved = crate::caixa_deps::resolve(out, &resolve_cfg)?;
report.deps_resolved = resolved.success_count;
report.deps_failed = resolved.failure_count;
if report.deps_failed > 0 {
report.final_status = "deps-resolve-failed".to_string();
return Ok(report);
}
report.final_status = "deps-resolved".to_string();
}
let final_rendered = crate::caixa::render(&lisp_src, out, true)?;
report.final_rendered = final_rendered.len();
report.final_status = "re-rendered".to_string();
if cfg.verify {
if let Some(eco) = detect_ecosystem_from_lisp(&lisp_src) {
if let Some(cmd) = test_command_for(&eco) {
let status = std::process::Command::new(&cmd[0])
.args(&cmd[1..])
.current_dir(out)
.status();
match status {
Ok(s) if s.success() => {
report.verify_passed = Some(true);
report.final_status = "instantiated-verified".to_string();
}
Ok(_) => {
report.verify_passed = Some(false);
report.final_status = "instantiated-verify-failed".to_string();
}
Err(e) => {
report.verify_passed = Some(false);
report.final_status = format!("instantiated-verify-error: {e}");
}
}
} else {
report.final_status = "instantiated-no-test-runner".to_string();
}
} else {
report.final_status = "instantiated-ecosystem-unknown".to_string();
}
} else {
report.final_status = "instantiated".to_string();
}
Ok(report)
}
fn detect_ecosystem_from_lisp(src: &str) -> Option<String> {
for (i, _) in src.match_indices(":ecosystem") {
let after = src[i + ":ecosystem".len()..].trim_start();
let after = after.strip_prefix(':')?;
let end = after.find(|c: char| c.is_whitespace() || c == ')' || c == '}')
.unwrap_or(after.len());
return Some(after[..end].to_string());
}
None
}
fn test_command_for(eco: &str) -> Option<Vec<String>> {
let argv: &[&str] = match eco {
"rust-single-crate" | "rust-workspace" => &["cargo", "build", "--release", "--quiet"],
"npm" | "js-pnpm" => &["npm", "test"],
"python" | "python-pdm" | "python-pipenv" => &["python", "-m", "pytest", "-q"],
"go" => &["go", "build", "./..."],
"nix-flake" => &["nix", "flake", "check", "--no-build", "--accept-flake-config"],
_ => return None,
};
Some(argv.iter().map(|s| s.to_string()).collect())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_ecosystem_from_lisp_finds_keyword() {
let src = "(defcaixa :name \"x\" :ecosystem :rust-single-crate :package {})";
assert_eq!(detect_ecosystem_from_lisp(src), Some("rust-single-crate".to_string()));
}
#[test]
fn detect_ecosystem_handles_multiline_lisp() {
let src = "(defcaixa\n :name \"x\"\n :ecosystem :nix-flake\n :package {})";
assert_eq!(detect_ecosystem_from_lisp(src), Some("nix-flake".to_string()));
}
}