use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use caixa_core::Caixa;
pub(crate) const CAIXA_MANIFEST_FILENAME: &str = "caixa.lisp";
pub(crate) const CAIXA_ROOT_DEFAULT_DIRNAME: &str = ".";
pub(crate) fn caixa_manifest_path(root: &Path) -> PathBuf {
root.join(CAIXA_MANIFEST_FILENAME)
}
pub(crate) fn caixa_root(path: Option<&Path>) -> PathBuf {
path.map_or_else(
|| PathBuf::from(CAIXA_ROOT_DEFAULT_DIRNAME),
Path::to_path_buf,
)
}
pub(crate) fn load_caixa(root: &Path) -> Result<Caixa> {
let manifest = caixa_manifest_path(root);
let src = std::fs::read_to_string(&manifest)
.with_context(|| format!("reading {}", manifest.display()))?;
Caixa::from_lisp(&src).with_context(|| format!("parsing {}", manifest.display()))
}
pub(crate) fn load_yaml(path: &Path) -> Result<serde_yaml::Value> {
let src =
std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
serde_yaml::from_str(&src).with_context(|| format!("parsing {}", path.display()))
}
pub(crate) fn validate_cluster_arg(cluster: &str) -> Result<()> {
if cluster.is_empty() {
bail!(
"--cluster value is empty (every `feira` verb that joins \
`<k8s-repo>/clusters/<cluster>/…` requires a non-empty \
cluster name — e.g. `--cluster rio`, `--cluster mar`, \
`--cluster plo`)"
);
}
caixa_core::is_dns_1123_label(cluster).map_err(|reason| {
anyhow::anyhow!(
"--cluster {cluster:?} is not a valid DNS-1123 label: {reason} \
(every cluster name lands as a path segment in \
`<k8s-repo>/clusters/<cluster>/…` and as a K8s `metadata.name` \
on downstream HelmRelease / Gateway / HTTPRoute consumers; the \
apiserver rejects any non-DNS-1123-label name at admission time)"
)
})
}
pub(crate) fn validate_namespace_arg(namespace: &str) -> Result<()> {
if namespace.is_empty() {
bail!(
"--namespace value is empty (every `feira` verb that scopes a \
`kube::Api` to a namespaced resource requires a non-empty \
namespace name — e.g. `--namespace default`, `--namespace \
pleme-system`, or omit the flag to inherit the canonical \
`\"default\"` fallback)"
);
}
caixa_core::is_dns_1123_label(namespace).map_err(|reason| {
anyhow::anyhow!(
"--namespace {namespace:?} is not a valid DNS-1123 label: {reason} \
(every K8s Namespace `metadata.name` is bounded by the DNS-1123 \
label grammar; the apiserver rejects any non-DNS-1123-label name \
at admission time on every namespaced CR `metadata.namespace` \
axis downstream — EphemeralPool, EphemeralAllocation, Process)"
)
})
}
pub(crate) fn validate_nome_arg(nome: &str) -> Result<()> {
if nome.is_empty() {
bail!(
"<nome> value is empty (every `feira` verb that consumes a positional \
caixa-name argument requires a non-empty DNS-1123 label — e.g. \
`feira init hello-rio`, `feira add caixa-teia`)"
);
}
caixa_core::is_dns_1123_label(nome).map_err(|reason| {
anyhow::anyhow!(
"<nome> {nome:?} is not a valid DNS-1123 label: {reason} \
(a caixa's `:nome` lands as the `lareira-<nome>` K8s `metadata.name` \
on every downstream chart/release/CR consumer; the apiserver rejects \
any non-DNS-1123-label name at admission time, and `feira init` joins \
the value verbatim into the `<nome>/lib/<nome>.lisp` scaffold path so \
a path-traversal shape escapes the target dir before any other I/O is \
observable)"
)
})
}
#[cfg(test)]
mod tests {
use super::*;
use caixa_core::CaixaKind;
use tempfile::tempdir;
#[test]
fn caixa_manifest_path_resolves_root_join_canonical_filename() {
let root = PathBuf::from("/tmp/some-caixa");
let p = caixa_manifest_path(&root);
assert!(p.starts_with(&root), "manifest path must live under root");
assert!(
p.ends_with(CAIXA_MANIFEST_FILENAME),
"manifest path must end with the canonical filename"
);
assert_eq!(p.file_name().and_then(|s| s.to_str()), Some("caixa.lisp"));
}
#[test]
fn load_caixa_accepts_well_formed_manifest() {
let dir = tempdir().expect("tempdir");
std::fs::write(
dir.path().join(CAIXA_MANIFEST_FILENAME),
r#"(defcaixa
:nome "hello"
:kind Biblioteca
:versao "0.1.0"
:bibliotecas ())"#,
)
.expect("write manifest");
let caixa = load_caixa(dir.path()).expect("well-formed manifest must load");
assert_eq!(caixa.nome, "hello");
assert_eq!(caixa.kind, CaixaKind::Biblioteca);
assert_eq!(caixa.versao, "0.1.0");
}
#[test]
fn load_caixa_missing_manifest_diagnostic_names_path_with_reading_context() {
let dir = tempdir().expect("tempdir");
let err = load_caixa(dir.path()).expect_err("missing manifest must error");
let rendered = format!("{err:?}");
assert!(
rendered.contains("reading"),
"diagnostic must name the reading axis (got: {rendered:?})"
);
assert!(
rendered.contains("caixa.lisp"),
"diagnostic must name the canonical manifest filename (got: {rendered:?})"
);
}
#[test]
fn load_caixa_parse_error_diagnostic_names_path_with_parsing_context() {
let dir = tempdir().expect("tempdir");
std::fs::write(
dir.path().join(CAIXA_MANIFEST_FILENAME),
"this is not a defcaixa form at all",
)
.expect("write malformed manifest");
let err = load_caixa(dir.path()).expect_err("malformed manifest must error");
let rendered = format!("{err:?}");
assert!(
rendered.contains("parsing"),
"diagnostic must name the parsing axis (got: {rendered:?})"
);
assert!(
rendered.contains("caixa.lisp"),
"diagnostic must name the canonical manifest filename (got: {rendered:?})"
);
}
#[test]
fn load_yaml_accepts_well_formed_document() {
let dir = tempdir().expect("tempdir");
let p = dir.path().join("hello.yaml");
std::fs::write(&p, "name: hello\nport: 8080\n").expect("write yaml");
let value = load_yaml(&p).expect("well-formed yaml must load");
assert_eq!(
value.get("name").and_then(serde_yaml::Value::as_str),
Some("hello"),
"parsed yaml must round-trip the canonical top-level scalar"
);
}
#[test]
fn load_yaml_missing_file_diagnostic_names_path_with_reading_context() {
let dir = tempdir().expect("tempdir");
let p = dir.path().join("absent.yaml");
let err = load_yaml(&p).expect_err("missing yaml must error");
let rendered = format!("{err:?}");
assert!(
rendered.contains("reading"),
"diagnostic must name the reading axis (got: {rendered:?})"
);
assert!(
rendered.contains("absent.yaml"),
"diagnostic must name the offending path (got: {rendered:?})"
);
}
#[test]
fn load_yaml_parse_error_diagnostic_names_path_with_parsing_context() {
let dir = tempdir().expect("tempdir");
let p = dir.path().join("torn.yaml");
std::fs::write(&p, "name: torn\n bad-indent: [unclosed\n").expect("write malformed yaml");
let err = load_yaml(&p).expect_err("malformed yaml must error");
let rendered = format!("{err:?}");
assert!(
rendered.contains("parsing"),
"diagnostic must name the parsing axis (got: {rendered:?})"
);
assert!(
rendered.contains("torn.yaml"),
"diagnostic must name the offending path (got: {rendered:?})"
);
}
#[test]
fn load_yaml_parse_error_preserves_underlying_serde_yaml_error_on_chain() {
let dir = tempdir().expect("tempdir");
let p = dir.path().join("torn.yaml");
std::fs::write(&p, "name: torn\n bad-indent: [unclosed\n").expect("write malformed yaml");
let err = load_yaml(&p).expect_err("malformed yaml must error");
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 caixa_root_with_none_resolves_to_canonical_cwd_dirname() {
let p = caixa_root(None);
assert_eq!(
p,
PathBuf::from(CAIXA_ROOT_DEFAULT_DIRNAME),
"absent --path must fall back to the canonical CWD-default dirname literal"
);
assert_eq!(
p.as_os_str(),
std::ffi::OsStr::new("."),
"the canonical CWD-default dirname literal must remain `.` verbatim"
);
}
#[test]
fn caixa_root_with_some_returns_caller_path_clone() {
let caller = PathBuf::from("/tmp/some/explicit/caixa-root");
let resolved = caixa_root(Some(&caller));
assert_eq!(
resolved, caller,
"explicit --path must pass through the resolver unchanged"
);
}
#[test]
fn caixa_root_with_some_relative_path_preserves_shape() {
let rel = PathBuf::from("./subcaixa");
let resolved = caixa_root(Some(&rel));
assert_eq!(
resolved, rel,
"relative --path must pass through the resolver verbatim, no canonicalize"
);
}
#[test]
fn caixa_root_canonical_dirname_constant_pins_dot_literal() {
assert_eq!(
CAIXA_ROOT_DEFAULT_DIRNAME, ".",
"canonical CWD-default dirname literal must remain `.` verbatim"
);
}
#[test]
fn validate_cluster_arg_accepts_canonical_dns_1123_label_names() {
for cluster in ["rio", "mar", "plo", "us-east-1", "eu-west-2", "a", "a0"] {
validate_cluster_arg(cluster)
.unwrap_or_else(|_| panic!("canonical cluster name {cluster:?} must accept"));
}
}
#[test]
fn validate_cluster_arg_rejects_empty_with_self_locating_diagnostic() {
let err = validate_cluster_arg("").expect_err("empty --cluster must reject");
let rendered = format!("{err:?}");
assert!(
rendered.contains("--cluster"),
"diagnostic must name the offending flag (got: {rendered:?})"
);
assert!(
rendered.contains("empty"),
"diagnostic must name the empty-value axis (got: {rendered:?})"
);
}
#[test]
fn validate_cluster_arg_rejects_path_traversal_with_named_value() {
for bad in ["rio/..", "../rio", "rio/sub", "."] {
let err = match validate_cluster_arg(bad) {
Err(e) => e,
Ok(()) => panic!("path-traversal {bad:?} must reject"),
};
let rendered = format!("{err:?}");
assert!(
rendered.contains("--cluster"),
"diagnostic must name the offending flag for {bad:?} (got: {rendered:?})"
);
assert!(
rendered.contains(bad),
"diagnostic must name the offending value verbatim for {bad:?} (got: {rendered:?})"
);
}
}
#[test]
fn validate_cluster_arg_rejects_uppercase_with_named_value_and_canonical_lowercase_hint() {
let err = validate_cluster_arg("Rio").expect_err("uppercase --cluster must reject");
let rendered = format!("{err:?}");
assert!(
rendered.contains("--cluster"),
"diagnostic must name the offending flag (got: {rendered:?})"
);
assert!(
rendered.contains("Rio"),
"diagnostic must name the offending value verbatim (got: {rendered:?})"
);
assert!(
rendered.contains("rio"),
"diagnostic must surface the canonical lowercase remediation (got: {rendered:?})"
);
}
#[test]
fn validate_cluster_arg_rejects_underscore_with_named_value_and_hyphen_hint() {
let err =
validate_cluster_arg("rio_cluster").expect_err("underscore --cluster must reject");
let rendered = format!("{err:?}");
assert!(
rendered.contains("--cluster"),
"diagnostic must name the offending flag (got: {rendered:?})"
);
assert!(
rendered.contains("rio_cluster"),
"diagnostic must name the offending value verbatim (got: {rendered:?})"
);
assert!(
rendered.contains('`'),
"diagnostic must surface the hyphen-separator remediation (got: {rendered:?})"
);
}
#[test]
fn validate_cluster_arg_rejects_leading_hyphen_with_named_value() {
for bad in ["-rio", "rio-"] {
let err = match validate_cluster_arg(bad) {
Err(e) => e,
Ok(()) => panic!("boundary-`-` {bad:?} must reject"),
};
let rendered = format!("{err:?}");
assert!(
rendered.contains("--cluster"),
"diagnostic must name the offending flag for {bad:?} (got: {rendered:?})"
);
assert!(
rendered.contains(bad),
"diagnostic must name the offending value verbatim for {bad:?} (got: {rendered:?})"
);
}
}
#[test]
fn validate_namespace_arg_accepts_canonical_dns_1123_label_names() {
for namespace in [
"default",
"kube-system",
"pleme-system",
"cert-manager",
"argocd",
"a",
"a0",
] {
validate_namespace_arg(namespace)
.unwrap_or_else(|_| panic!("canonical namespace {namespace:?} must accept"));
}
}
#[test]
fn validate_namespace_arg_rejects_empty_with_self_locating_diagnostic() {
let err = validate_namespace_arg("").expect_err("empty --namespace must reject");
let rendered = format!("{err:?}");
assert!(
rendered.contains("--namespace"),
"diagnostic must name the offending flag (got: {rendered:?})"
);
assert!(
rendered.contains("empty"),
"diagnostic must name the empty-value axis (got: {rendered:?})"
);
}
#[test]
fn validate_namespace_arg_rejects_path_traversal_with_named_value() {
for bad in ["default/..", "../default", "default/sub", "."] {
let err = match validate_namespace_arg(bad) {
Err(e) => e,
Ok(()) => panic!("path-traversal {bad:?} must reject"),
};
let rendered = format!("{err:?}");
assert!(
rendered.contains("--namespace"),
"diagnostic must name the offending flag for {bad:?} (got: {rendered:?})"
);
assert!(
rendered.contains(bad),
"diagnostic must name the offending value verbatim for {bad:?} (got: {rendered:?})"
);
}
}
#[test]
fn validate_namespace_arg_rejects_uppercase_with_named_value_and_canonical_lowercase_hint() {
let err = validate_namespace_arg("MyTeam").expect_err("uppercase --namespace must reject");
let rendered = format!("{err:?}");
assert!(
rendered.contains("--namespace"),
"diagnostic must name the offending flag (got: {rendered:?})"
);
assert!(
rendered.contains("MyTeam"),
"diagnostic must name the offending value verbatim (got: {rendered:?})"
);
assert!(
rendered.contains("myteam"),
"diagnostic must surface the canonical lowercase remediation (got: {rendered:?})"
);
}
#[test]
fn validate_namespace_arg_rejects_underscore_with_named_value_and_hyphen_hint() {
let err = validate_namespace_arg("team_a").expect_err("underscore --namespace must reject");
let rendered = format!("{err:?}");
assert!(
rendered.contains("--namespace"),
"diagnostic must name the offending flag (got: {rendered:?})"
);
assert!(
rendered.contains("team_a"),
"diagnostic must name the offending value verbatim (got: {rendered:?})"
);
assert!(
rendered.contains('`'),
"diagnostic must surface the hyphen-separator remediation (got: {rendered:?})"
);
}
#[test]
fn validate_namespace_arg_rejects_leading_hyphen_with_named_value() {
for bad in ["-default", "default-"] {
let err = match validate_namespace_arg(bad) {
Err(e) => e,
Ok(()) => panic!("boundary-`-` {bad:?} must reject"),
};
let rendered = format!("{err:?}");
assert!(
rendered.contains("--namespace"),
"diagnostic must name the offending flag for {bad:?} (got: {rendered:?})"
);
assert!(
rendered.contains(bad),
"diagnostic must name the offending value verbatim for {bad:?} (got: {rendered:?})"
);
}
}
#[test]
fn validate_namespace_arg_accepts_canonical_default_fallback_literal() {
validate_namespace_arg("default").expect("canonical default literal must accept");
}
#[test]
fn validate_nome_arg_accepts_canonical_dns_1123_label_names() {
for nome in [
"hello-rio",
"caixa-teia",
"iac-forge-ir",
"checkout",
"cart",
"catalog",
"payment",
"a",
"a0",
] {
validate_nome_arg(nome)
.unwrap_or_else(|_| panic!("canonical caixa name {nome:?} must accept"));
}
}
#[test]
fn validate_nome_arg_rejects_empty_with_self_locating_diagnostic() {
let err = validate_nome_arg("").expect_err("empty <nome> must reject");
let rendered = format!("{err:?}");
assert!(
rendered.contains("<nome>"),
"diagnostic must name the offending positional (got: {rendered:?})"
);
assert!(
rendered.contains("empty"),
"diagnostic must name the empty-value axis (got: {rendered:?})"
);
}
#[test]
fn validate_nome_arg_rejects_path_traversal_with_named_value() {
for bad in ["../escape", "lib/../escape", "foo/bar", ".", ".."] {
let err = match validate_nome_arg(bad) {
Err(e) => e,
Ok(()) => panic!("path-traversal {bad:?} must reject"),
};
let rendered = format!("{err:?}");
assert!(
rendered.contains("<nome>"),
"diagnostic must name the offending positional for {bad:?} (got: {rendered:?})"
);
assert!(
rendered.contains(bad),
"diagnostic must name the offending value verbatim for {bad:?} (got: {rendered:?})"
);
}
}
#[test]
fn validate_nome_arg_rejects_uppercase_with_named_value_and_canonical_lowercase_hint() {
let err = validate_nome_arg("MyCaixa").expect_err("uppercase <nome> must reject");
let rendered = format!("{err:?}");
assert!(
rendered.contains("<nome>"),
"diagnostic must name the offending positional (got: {rendered:?})"
);
assert!(
rendered.contains("MyCaixa"),
"diagnostic must name the offending value verbatim (got: {rendered:?})"
);
assert!(
rendered.contains("mycaixa"),
"diagnostic must surface the canonical lowercase remediation (got: {rendered:?})"
);
}
#[test]
fn validate_nome_arg_rejects_underscore_with_named_value_and_hyphen_hint() {
let err = validate_nome_arg("caixa_teia").expect_err("underscore <nome> must reject");
let rendered = format!("{err:?}");
assert!(
rendered.contains("<nome>"),
"diagnostic must name the offending positional (got: {rendered:?})"
);
assert!(
rendered.contains("caixa_teia"),
"diagnostic must name the offending value verbatim (got: {rendered:?})"
);
assert!(
rendered.contains('`'),
"diagnostic must surface the hyphen-separator remediation (got: {rendered:?})"
);
}
#[test]
fn validate_nome_arg_rejects_leading_hyphen_with_named_value() {
for bad in ["-hello", "hello-"] {
let err = match validate_nome_arg(bad) {
Err(e) => e,
Ok(()) => panic!("boundary-`-` {bad:?} must reject"),
};
let rendered = format!("{err:?}");
assert!(
rendered.contains("<nome>"),
"diagnostic must name the offending positional for {bad:?} (got: {rendered:?})"
);
assert!(
rendered.contains(bad),
"diagnostic must name the offending value verbatim for {bad:?} (got: {rendered:?})"
);
}
}
#[test]
fn validate_nome_arg_rejects_space_with_named_value() {
let err = validate_nome_arg("my caixa").expect_err("space <nome> must reject");
let rendered = format!("{err:?}");
assert!(
rendered.contains("<nome>"),
"diagnostic must name the offending positional (got: {rendered:?})"
);
assert!(
rendered.contains("my caixa"),
"diagnostic must name the offending value verbatim (got: {rendered:?})"
);
}
#[test]
fn load_caixa_parse_error_preserves_underlying_lisp_error_on_chain() {
let dir = tempdir().expect("tempdir");
std::fs::write(dir.path().join(CAIXA_MANIFEST_FILENAME), "(((((")
.expect("write malformed manifest");
let err = load_caixa(dir.path()).expect_err("malformed manifest must error");
let typed_reachable = err
.chain()
.any(|e| e.downcast_ref::<tatara_lisp::LispError>().is_some());
assert!(
typed_reachable,
"underlying tatara_lisp::LispError must remain reachable on the anyhow chain"
);
}
}