use anyhow::{anyhow, Result};
use std::path::{Path, PathBuf};
use crate::file_capture::CapturedFile;
use crate::sexp_ast::{Forms, SExp};
pub fn inject_files_into_forms(forms: Forms, files: &[CapturedFile]) -> Forms {
inject_files_and_symlinks_into_forms(forms, files, &[])
}
pub fn inject_files_and_symlinks_into_forms(
forms: Forms,
files: &[CapturedFile],
symlinks: &[crate::file_capture::CapturedSymlink],
) -> Forms {
inject_all_captures_into_forms(forms, files, symlinks, &[])
}
pub fn inject_all_captures_into_forms(
forms: Forms,
files: &[CapturedFile],
symlinks: &[crate::file_capture::CapturedSymlink],
binaries: &[crate::file_capture::CapturedBinary],
) -> Forms {
if files.is_empty() && symlinks.is_empty() && binaries.is_empty() {
return forms;
}
let Forms(items) = forms;
let out_items: Vec<SExp> = items.into_iter().map(|form| match form {
SExp::List(mut inner) => {
if !files.is_empty() {
let files_vec: Vec<SExp> = files.iter().map(|f| {
SExp::Map(vec![
(SExp::kw("path"), SExp::str(&f.path)),
(SExp::kw("sha256"), SExp::str(&f.sha256)),
(SExp::kw("size"), SExp::sym(f.size.to_string())),
(SExp::kw("body"), SExp::str(&f.body)),
])
}).collect();
inner.push(SExp::kw("files"));
inner.push(SExp::Vector(files_vec));
}
if !symlinks.is_empty() {
let links_vec: Vec<SExp> = symlinks.iter().map(|s| {
SExp::Map(vec![
(SExp::kw("path"), SExp::str(&s.path)),
(SExp::kw("target"), SExp::str(&s.target)),
])
}).collect();
inner.push(SExp::kw("symlinks"));
inner.push(SExp::Vector(links_vec));
}
if !binaries.is_empty() {
let bins_vec: Vec<SExp> = binaries.iter().map(|b| {
SExp::Map(vec![
(SExp::kw("path"), SExp::str(&b.path)),
(SExp::kw("sha256"), SExp::str(&b.sha256)),
(SExp::kw("size"), SExp::sym(b.size.to_string())),
(SExp::kw("base64"), SExp::str(&b.base64)),
])
}).collect();
inner.push(SExp::kw("binaries"));
inner.push(SExp::Vector(bins_vec));
}
SExp::List(inner)
}
other => other,
}).collect();
Forms(out_items)
}
#[derive(Debug, Clone)]
pub struct EatReport {
pub source_path: PathBuf,
pub ecosystem: Option<String>,
pub caixa_name: String,
pub caixa_lisp_path: PathBuf,
pub files_manifest_path: PathBuf,
pub rendered_path: PathBuf,
pub captured_file_count: usize,
pub captured_bytes: usize,
pub rendered_artifact_count: usize,
pub restored_file_count: usize,
}
impl EatReport {
pub fn to_json(&self) -> String {
use crate::json_ast::Value;
let mut o = Value::obj();
o.insert("source", Value::s(self.source_path.to_string_lossy().to_string()));
if let Some(eco) = &self.ecosystem {
o.insert("ecosystem", Value::s(eco));
}
o.insert("caixa-name", Value::s(&self.caixa_name));
o.insert("caixa-lisp", Value::s(self.caixa_lisp_path.to_string_lossy().to_string()));
o.insert("files-manifest", Value::s(self.files_manifest_path.to_string_lossy().to_string()));
o.insert("rendered", Value::s(self.rendered_path.to_string_lossy().to_string()));
o.insert("captured-files", Value::i(self.captured_file_count as i64));
o.insert("captured-bytes", Value::i(self.captured_bytes as i64));
o.insert("rendered-artifacts", Value::i(self.rendered_artifact_count as i64));
o.insert("restored-files", Value::i(self.restored_file_count as i64));
crate::json_ast::render(&o)
}
}
pub fn eat(
source: &Path,
out: &Path,
capture_cfg: &crate::file_capture::CaptureConfig,
) -> Result<EatReport> {
use crate::ast::Render;
std::fs::create_dir_all(out)?;
let detected = crate::discover::detect(source)
.ok_or_else(|| anyhow!("no ecosystem detected at {}", source.display()))?;
let caixa_name = detected.name.clone();
let ecosystem = detected.ecosystem.to_string();
let forms = crate::reverse::reverse_from_path(source)?;
let cap = crate::file_capture::capture(source, capture_cfg)?;
let forms_with_files = inject_all_captures_into_forms(
forms, &cap.files, &cap.symlinks, &cap.binaries);
let caixa_lisp_path = out.join(format!("{caixa_name}.caixa.lisp"));
let lisp_src = forms_with_files.render();
std::fs::write(&caixa_lisp_path, &lisp_src)?;
let files_manifest_path = caixa_lisp_path.clone();
let rendered_path = out.join(format!("{caixa_name}-rendered"));
std::fs::create_dir_all(&rendered_path)?;
let rendered = crate::caixa::render(&lisp_src, &rendered_path, true)?;
let restored = crate::file_capture::restore(&rendered_path, &cap.files)?;
let in_repo_lisp = rendered_path.join(format!("{caixa_name}.caixa.lisp"));
std::fs::write(&in_repo_lisp, &lisp_src)?;
Ok(EatReport {
source_path: source.to_path_buf(),
ecosystem: Some(ecosystem),
caixa_name,
caixa_lisp_path,
files_manifest_path,
rendered_path,
captured_file_count: cap.files.len(),
captured_bytes: cap.total_bytes,
rendered_artifact_count: rendered.len(),
restored_file_count: restored.len(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn mk_repo(files: &[(&str, &str)]) -> tempdir::TempDir {
let tmp = tempdir::TempDir::new("eat-src").unwrap();
for (rel, body) in files {
let p = tmp.path().join(rel);
if let Some(parent) = p.parent() { fs::create_dir_all(parent).unwrap(); }
fs::write(&p, body).unwrap();
}
tmp
}
#[test]
fn eat_rust_crate_produces_caixa_files_manifest_rendered() {
let src = mk_repo(&[
("Cargo.toml",
"[package]\nname = \"my-crate\"\nversion = \"1.0.0\"\nlicense = \"MIT\"\ndescription = \"x\"\n"),
("src/lib.rs", "// original code\n#[test] fn original_test() {}"),
]);
let out = tempdir::TempDir::new("eat-out").unwrap();
let cfg = crate::file_capture::CaptureConfig::default();
let report = eat(src.path(), out.path(), &cfg).unwrap();
assert!(report.caixa_lisp_path.is_file());
assert!(report.files_manifest_path.is_file());
assert!(report.rendered_path.is_dir());
assert_eq!(report.captured_file_count, 2);
assert!(report.rendered_path.join(".github/workflows/auto-release.yml").is_file());
let restored_lib = fs::read_to_string(report.rendered_path.join("src/lib.rs")).unwrap();
assert!(restored_lib.contains("original code"),
"expected restored original; got: {restored_lib}");
let restored_cargo = fs::read_to_string(report.rendered_path.join("Cargo.toml")).unwrap();
assert!(restored_cargo.contains("name = \"my-crate\""));
}
#[test]
fn eat_embeds_files_in_caixa_lisp() {
let src = mk_repo(&[
("Cargo.toml", "[package]\nname = \"x\"\n"),
("README.md", "# Title"),
]);
let out = tempdir::TempDir::new("eat-out").unwrap();
let cfg = crate::file_capture::CaptureConfig::default();
let report = eat(src.path(), out.path(), &cfg).unwrap();
assert_eq!(report.files_manifest_path, report.caixa_lisp_path,
"manifest path should point at the canonical .caixa.lisp");
let lisp = fs::read_to_string(&report.caixa_lisp_path).unwrap();
assert!(lisp.contains(":files"),
"expected :files slot in lisp source; got:\n{lisp}");
assert!(lisp.contains("README.md"));
assert!(lisp.contains("Cargo.toml"));
assert!(lisp.contains(":sha256"));
}
#[test]
fn eat_fails_on_undetectable_dir() {
let src = mk_repo(&[("just-data.txt", "no manifest here")]);
let out = tempdir::TempDir::new("eat-out").unwrap();
let cfg = crate::file_capture::CaptureConfig::default();
let err = eat(src.path(), out.path(), &cfg).unwrap_err();
assert!(err.to_string().contains("no ecosystem detected"));
}
}