use std::path::PathBuf;
use anyhow::{Context, Result, bail};
use caixa_core::{Caixa, CaixaKind};
use clap::Args;
use super::load::{caixa_root, load_caixa};
#[derive(Args)]
pub struct Chart {
#[arg(long, default_value = ".caixa/chart")]
pub out: PathBuf,
#[arg(long)]
pub path: Option<PathBuf>,
}
impl Chart {
pub fn run(self) -> Result<()> {
let root = caixa_root(self.path.as_deref());
let caixa = load_caixa(&root)?;
let cu_yaml = load_first_servico_yaml(&caixa, &root)?;
let dir = caixa_helm::render_chart_for_servico(&caixa, &cu_yaml)?;
dir.write_to(&self.out)
.with_context(|| format!("writing chart to {}", self.out.display()))?;
eprintln!(
"rendered {} → {}",
dir.name,
self.out.join(&dir.name).display()
);
Ok(())
}
}
pub(crate) fn load_first_servico_yaml(
caixa: &Caixa,
root: &std::path::Path,
) -> Result<serde_yaml::Value> {
let cu_path = first_servico_path(caixa, root)?;
super::load::load_yaml(&cu_path)
}
pub(crate) fn first_servico_path(caixa: &Caixa, root: &std::path::Path) -> Result<PathBuf> {
caixa_core::require_kind(caixa, CaixaKind::Servico)
.with_context(|| "feira chart / feira deploy require :kind Servico")?;
caixa_core::require_single_servico(caixa)
.with_context(|| "feira chart / feira deploy require exactly one :servicos entry")?;
let s = caixa
.servicos()
.first()
.expect("require_single_servico guarantees one entry");
let p = root.join(s);
if !p.exists() {
bail!(
"caixa {:?}: declared :servicos entry not found on disk: {}",
caixa.nome(),
p.display()
);
}
Ok(p)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
fn parse(src: &str) -> Caixa {
Caixa::from_lisp(src).expect("parse")
}
#[test]
fn first_servico_path_accepts_singleton_servico_kind() {
let dir = tempdir().expect("tempdir");
std::fs::create_dir(dir.path().join("servicos")).expect("mkdir servicos");
std::fs::write(
dir.path().join("servicos/hello.computeunit.yaml"),
"name: hello\n",
)
.expect("write cu");
let caixa = parse(
r#"(defcaixa
:nome "hello"
:kind Servico
:versao "0.1.0"
:servicos ("servicos/hello.computeunit.yaml"))"#,
);
let p = first_servico_path(&caixa, dir.path()).expect("singleton must pass");
assert!(p.ends_with("servicos/hello.computeunit.yaml"));
}
#[test]
fn first_servico_path_rejects_non_servico_kind_with_named_caixa() {
let dir = tempdir().expect("tempdir");
let caixa = parse(
r#"(defcaixa
:nome "lib-shape"
:kind Biblioteca
:versao "0.1.0"
:bibliotecas ())"#,
);
let err = first_servico_path(&caixa, dir.path()).expect_err("Biblioteca must reject");
let rendered = format!("{err:?}");
assert!(
rendered.contains("lib-shape"),
"diagnostic must name the offending caixa nome (got: {rendered:?})"
);
assert!(
rendered.contains("Servico"),
"diagnostic must name the expected kind (got: {rendered:?})"
);
assert!(
rendered.contains("Biblioteca"),
"diagnostic must name the actual kind (got: {rendered:?})"
);
assert!(
rendered.contains("feira chart") || rendered.contains("feira deploy"),
"diagnostic must keep the per-Servico verb context (got: {rendered:?})"
);
}
#[test]
fn first_servico_path_kind_rejection_carries_typed_view() {
let dir = tempdir().expect("tempdir");
let caixa = parse(
r#"(defcaixa
:nome "app-shape"
:kind Aplicacao
:versao "0.1.0"
:membros ())"#,
);
let err = first_servico_path(&caixa, dir.path()).expect_err("Aplicacao must reject");
let km = err
.chain()
.find_map(|e| e.downcast_ref::<caixa_core::KindMismatch>())
.expect("KindMismatch must be reachable through the anyhow chain");
assert_eq!(km.nome, "app-shape");
assert_eq!(km.expected, CaixaKind::Servico);
assert_eq!(km.actual, CaixaKind::Aplicacao);
}
#[test]
fn first_servico_path_rejects_empty_servicos_with_named_caixa() {
let dir = tempdir().expect("tempdir");
let caixa = parse(
r#"(defcaixa
:nome "no-servicos"
:kind Servico
:versao "0.1.0"
:servicos ())"#,
);
let err = first_servico_path(&caixa, dir.path()).expect_err("empty :servicos must reject");
let rendered = format!("{err:?}");
assert!(
rendered.contains("no-servicos"),
"diagnostic must name the offending caixa nome (got: {rendered:?})"
);
assert!(
rendered.contains(":servicos"),
"diagnostic must name the :servicos axis (got: {rendered:?})"
);
let scm = err
.chain()
.find_map(|e| e.downcast_ref::<caixa_core::ServicoCountMismatch>())
.expect("ServicoCountMismatch must be reachable through the chain");
assert_eq!(scm.nome, "no-servicos");
assert_eq!(scm.count, 0);
}
#[test]
fn first_servico_path_rejects_multi_entry_servicos_with_named_caixa() {
let dir = tempdir().expect("tempdir");
let caixa = parse(
r#"(defcaixa
:nome "two-servicos"
:kind Servico
:versao "0.1.0"
:servicos ("servicos/a.computeunit.yaml"
"servicos/b.computeunit.yaml"))"#,
);
let err = first_servico_path(&caixa, dir.path()).expect_err("multi :servicos must reject");
let rendered = format!("{err:?}");
assert!(
rendered.contains("two-servicos"),
"diagnostic must name the offending caixa nome (got: {rendered:?})"
);
let scm = err
.chain()
.find_map(|e| e.downcast_ref::<caixa_core::ServicoCountMismatch>())
.expect("ServicoCountMismatch must be reachable through the chain");
assert_eq!(scm.nome, "two-servicos");
assert_eq!(scm.count, 2);
}
#[test]
fn first_servico_path_missing_file_diagnostic_routes_through_caixa_nome_accessor() {
let dir = tempdir().expect("tempdir");
let caixa = parse(
r#"(defcaixa
:nome "missing-file-nome"
:kind Servico
:versao "0.1.0"
:servicos ("servicos/missing-file-nome.computeunit.yaml"))"#,
);
let err = first_servico_path(&caixa, dir.path()).expect_err("missing file must reject");
let missing_path = dir
.path()
.join("servicos/missing-file-nome.computeunit.yaml");
assert_eq!(
format!("{err}"),
format!(
"caixa {:?}: declared :servicos entry not found on disk: {}",
caixa.nome(),
missing_path.display()
),
"first_servico_path's file-existence bail! diagnostic must \
derive its :nome scalar through the typed Caixa::nome \
accessor — a regression that re-inlines the raw caixa.nome \
field-access at the emit site silently splits the offending-\
caixa diagnostic both `feira chart` and `feira deploy` route \
through this helper from the peer accessor-derived \
substrate-side emit"
);
}
#[test]
fn first_servico_path_missing_file_diagnostic_names_offending_caixa_nome() {
let dir = tempdir().expect("tempdir");
let caixa = parse(
r#"(defcaixa
:nome "absent-file"
:kind Servico
:versao "0.1.0"
:servicos ("servicos/absent-file.computeunit.yaml"))"#,
);
let err = first_servico_path(&caixa, dir.path()).expect_err("missing file must reject");
let rendered = format!("{err:?}");
assert!(
rendered.contains("absent-file"),
"diagnostic must name the offending caixa nome (got: {rendered:?})"
);
assert!(
rendered.contains("not found"),
"diagnostic must name the file-existence axis (got: {rendered:?})"
);
}
#[test]
fn load_first_servico_yaml_accepts_well_formed_computeunit() {
let dir = tempdir().expect("tempdir");
std::fs::create_dir(dir.path().join("servicos")).expect("mkdir servicos");
std::fs::write(
dir.path().join("servicos/hello.computeunit.yaml"),
"name: hello\nimage: ghcr.io/pleme-io/hello:0.1.0\n",
)
.expect("write cu");
let caixa = parse(
r#"(defcaixa
:nome "hello"
:kind Servico
:versao "0.1.0"
:servicos ("servicos/hello.computeunit.yaml"))"#,
);
let cu_yaml = load_first_servico_yaml(&caixa, dir.path()).expect("well-formed must load");
assert_eq!(
cu_yaml.get("name").and_then(serde_yaml::Value::as_str),
Some("hello"),
"parsed YAML must round-trip the canonical top-level scalar"
);
}
#[test]
fn load_first_servico_yaml_parse_error_diagnostic_names_path_with_parsing_context() {
let dir = tempdir().expect("tempdir");
std::fs::create_dir(dir.path().join("servicos")).expect("mkdir servicos");
std::fs::write(
dir.path().join("servicos/broken.computeunit.yaml"),
"name: broken\n bad-indent: [unclosed\n",
)
.expect("write malformed cu");
let caixa = parse(
r#"(defcaixa
:nome "broken"
:kind Servico
:versao "0.1.0"
:servicos ("servicos/broken.computeunit.yaml"))"#,
);
let err =
load_first_servico_yaml(&caixa, dir.path()).expect_err("malformed YAML must reject");
let rendered = format!("{err:?}");
assert!(
rendered.contains("parsing"),
"diagnostic must name the parsing axis (got: {rendered:?})"
);
assert!(
rendered.contains("broken.computeunit.yaml"),
"diagnostic must name the offending computeunit path (got: {rendered:?})"
);
}
#[test]
fn load_first_servico_yaml_parse_error_preserves_underlying_serde_yaml_error_on_chain() {
let dir = tempdir().expect("tempdir");
std::fs::create_dir(dir.path().join("servicos")).expect("mkdir servicos");
std::fs::write(
dir.path().join("servicos/torn.computeunit.yaml"),
"name: torn\n bad-indent: [unclosed\n",
)
.expect("write malformed cu");
let caixa = parse(
r#"(defcaixa
:nome "torn"
:kind Servico
:versao "0.1.0"
:servicos ("servicos/torn.computeunit.yaml"))"#,
);
let err =
load_first_servico_yaml(&caixa, dir.path()).expect_err("malformed YAML must reject");
let typed_reachable = err
.chain()
.any(|e| e.downcast_ref::<serde_yaml::Error>().is_some());
assert!(
typed_reachable,
"underlying serde_yaml::Error must remain reachable on the anyhow chain"
);
}
#[test]
fn load_first_servico_yaml_routes_kind_mismatch_through_first_servico_path() {
let dir = tempdir().expect("tempdir");
let caixa = parse(
r#"(defcaixa
:nome "lib-shape"
:kind Biblioteca
:versao "0.1.0"
:bibliotecas ())"#,
);
let err = load_first_servico_yaml(&caixa, dir.path()).expect_err("Biblioteca must reject");
let km = err
.chain()
.find_map(|e| e.downcast_ref::<caixa_core::KindMismatch>())
.expect("KindMismatch must remain reachable through the helper's anyhow chain");
assert_eq!(km.nome, "lib-shape");
assert_eq!(km.expected, CaixaKind::Servico);
assert_eq!(km.actual, CaixaKind::Biblioteca);
}
}