tatara-rust-caixa 0.1.3

Wrap any generated proc-macro crate as a pleme-io caixa — emits a `caixa.lisp` of kind Biblioteca so the SDLC release pipeline (caixa-publish + crates.io) auto-publishes it on every merge.
Documentation
//! `tatara-rust-caixa` — wrap any generated proc-macro crate as a
//! pleme-io caixa.
//!
//! Decorates a [`CrateScaffold`] with a `caixa.lisp` of `:kind
//! Biblioteca` so the pleme-io SDLC release pipeline
//! (caixa-publish + crates.io auto-bump) picks the crate up on merge.
//!
//! Also wires the canonical `.github/workflows/auto-release.yml` shim
//! per `pleme-io-auto-release`.

use tatara_rust_ast::CrateScaffold;

/// Configuration for the caixa wrap.
#[derive(Clone, Debug, Default)]
pub struct CaixaConfig {
    /// Optional description — shown in the caixa catalog. Defaults to
    /// the scaffold's stored description if not set.
    pub description: Option<String>,
    /// Whether to attach the canonical pleme-io auto-release workflow.
    pub attach_auto_release: bool,
}

/// Decorate `scaffold` with a `caixa.lisp` (and optionally a
/// `.github/workflows/auto-release.yml`). Idempotent on both files.
pub fn attach_caixa_biblioteca(scaffold: &mut CrateScaffold, config: &CaixaConfig) {
    if !scaffold.files.iter().any(|f| f.path == "caixa.lisp") {
        scaffold.add_file(
            "caixa.lisp",
            render_caixa_biblioteca(
                &scaffold.name,
                &scaffold.version,
                config.description.as_deref(),
            ),
        );
    }
    if config.attach_auto_release
        && !scaffold
            .files
            .iter()
            .any(|f| f.path == ".github/workflows/auto-release.yml")
    {
        scaffold.add_file(
            ".github/workflows/auto-release.yml",
            render_auto_release_workflow(),
        );
    }
}

/// The canonical `(defcaixa <name> …)` form for a Biblioteca-kind
/// Rust crate. **Aligned with the pleme-doc-gen caixa.rs parser** so
/// every emitted repo is directly consumable by
/// `pleme-doc-gen publish-bulk` and the substrate's per-ecosystem
/// renderers. Shape:
///
/// ```text
/// (defcaixa <name>
///   :kind         "Biblioteca"
///   :ecosystem    "rust-single-crate"
///   :package      { :name "..." :version "..." :license "MIT"
///                   :description "..." :repository "..." }
///   :ci-config    { :bump { :default-type "patch" } }
///   :workflows    [ :auto-release ])
/// ```
#[must_use]
pub fn render_caixa_biblioteca(name: &str, version: &str, description: Option<&str>) -> String {
    let desc = description.unwrap_or("Generated by tatara-rust-ast.");
    let repo = format!("https://github.com/pleme-io/{name}");
    format!(
        r#";; caixa.lisp — generated by tatara-rust-caixa (tatara-rust-ast).
;;
;; Consumed by `pleme-doc-gen` for the SDLC pipeline (Cargo.toml +
;; .pleme-io-release.toml + CI shims + nix module trio + flake.nix).
;; Re-emit with `pleme-doc-gen caixa --source caixa.lisp --out .`.

(defcaixa {name}
  :kind         "Biblioteca"
  :ecosystem    "rust-single-crate"

  :package      {{ :name        "{name}"
                  :version     "{version}"
                  :license     "MIT"
                  :description "{desc}"
                  :repository  "{repo}"
                  :homepage    "{repo}"
                  :categories  [ "development-tools::procedural-macro-helpers" ]
                  :keywords    [ "tatara" "macro" "derive" "generated" ] }}

  :ci-config    {{ :bump    {{ :default-type "patch" }}
                  :publish {{ :no-verify false }} }}

  :workflows    [ :auto-release ]
  :stacks       [ ]
  :depends-on   [ ]
  :exposes      [ :rust-crate ]
  :publish-to-git true)
"#
    )
}

/// The canonical pleme-io auto-release workflow shim for a Biblioteca
/// (single-crate) — points directly at substrate's
/// `cargo-auto-release.yml` rather than the polymorphic
/// `auto-release.yml`. The polymorphic dispatcher's detect→branch
/// shape startup-fails on freshly-created repos; the per-language
/// reusable is the canonical working shape used by every existing
/// pleme-io Rust crate (engenho, etc.).
#[must_use]
pub fn render_auto_release_workflow() -> String {
    r#"# Auto-emitted by tatara-rust-caixa.
# The reusable substrate workflow does auto-bump → tag → publish to crates.io.
on:
  push:
    branches: [main]
  workflow_dispatch:
    inputs:
      bump-type:
        description: "patch | minor | major"
        required: false
        default: patch

jobs:
  release:
    uses: pleme-io/substrate/.github/workflows/cargo-auto-release.yml@main
    with:
      bump-type: ${{ inputs.bump-type || 'patch' }}
      regenerate-cargo-nix: "false"
    secrets: inherit
"#
    .to_string()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn attaches_caixa_lisp() {
        let mut s = CrateScaffold::new("my-derive", "0.1.0");
        attach_caixa_biblioteca(
            &mut s,
            &CaixaConfig {
                description: Some("Test crate".into()),
                attach_auto_release: false,
            },
        );
        let files = s.to_files();
        assert!(files.contains_key("caixa.lisp"));
        let lisp = files.get("caixa.lisp").unwrap();
        // pleme-doc-gen parser contract:
        assert!(lisp.contains("(defcaixa my-derive"));
        assert!(lisp.contains(r#":kind         "Biblioteca""#));
        assert!(lisp.contains(r#":ecosystem    "rust-single-crate""#));
        assert!(lisp.contains(r#":name        "my-derive""#));
        assert!(lisp.contains(r#":version     "0.1.0""#));
        assert!(lisp.contains("Test crate"));
    }

    /// **Contract test** — every keyword pleme-doc-gen's `caixa.rs`
    /// parser reads MUST appear in the emitted form. Mirrors the
    /// `out.kind = read_keyword(body, ":kind")` /
    /// `read_dict_block(body, ":package")` set in
    /// `pleme-doc-gen/src/caixa.rs:1787`.
    #[test]
    fn emits_every_keyword_pleme_doc_gen_parser_consumes() {
        let lisp = render_caixa_biblioteca("foo-derive", "1.2.3", Some("Foo macro."));

        // Top-level form name (`defcaixa <name>`).
        assert!(lisp.starts_with(";;") || lisp.contains("(defcaixa foo-derive"));

        // Required top-level keywords.
        for kw in [":kind", ":ecosystem", ":package", ":ci-config", ":workflows"] {
            assert!(
                lisp.contains(kw),
                "caixa.lisp missing required keyword `{kw}` for pleme-doc-gen parser"
            );
        }

        // Required :package dict keys (per pleme-doc-gen's
        // parse_dict-on-:package-block path).
        for pkg_key in [":name", ":version", ":license", ":description", ":repository"] {
            assert!(
                lisp.contains(pkg_key),
                "caixa.lisp :package missing `{pkg_key}`"
            );
        }

        // Ecosystem must be one pleme-doc-gen's emitter table knows.
        // (per ecosystems.rs line 58: "rust-single-crate" is supported)
        assert!(lisp.contains(r#":ecosystem    "rust-single-crate""#));
    }

    #[test]
    fn attaches_workflow_when_enabled() {
        let mut s = CrateScaffold::new("x", "0.1.0");
        attach_caixa_biblioteca(
            &mut s,
            &CaixaConfig {
                attach_auto_release: true,
                ..Default::default()
            },
        );
        assert!(
            s.to_files()
                .contains_key(".github/workflows/auto-release.yml")
        );
    }

    #[test]
    fn idempotent() {
        let mut s = CrateScaffold::new("x", "0.1.0");
        s.add_file("caixa.lisp", "custom");
        attach_caixa_biblioteca(&mut s, &CaixaConfig::default());
        assert_eq!(s.to_files()["caixa.lisp"], "custom");
    }

    #[test]
    fn workflow_references_substrate_reusable() {
        let w = render_auto_release_workflow();
        // Per the diagnosed startup_failure: must call the per-language
        // reusable, not the polymorphic dispatcher.
        assert!(w.contains("uses: pleme-io/substrate/.github/workflows/cargo-auto-release.yml"));
        assert!(w.contains(r#"regenerate-cargo-nix: "false""#));
        assert!(w.contains("secrets: inherit"));
    }
}