Skip to main content

tatara_rust_caixa/
lib.rs

1//! `tatara-rust-caixa` — wrap any generated proc-macro crate as a
2//! pleme-io caixa.
3//!
4//! Decorates a [`CrateScaffold`] with a `caixa.lisp` of `:kind
5//! Biblioteca` so the pleme-io SDLC release pipeline
6//! (caixa-publish + crates.io auto-bump) picks the crate up on merge.
7//!
8//! Also wires the canonical `.github/workflows/auto-release.yml` shim
9//! per `pleme-io-auto-release`.
10
11use tatara_rust_ast::CrateScaffold;
12
13/// Configuration for the caixa wrap.
14#[derive(Clone, Debug, Default)]
15pub struct CaixaConfig {
16    /// Optional description — shown in the caixa catalog. Defaults to
17    /// the scaffold's stored description if not set.
18    pub description: Option<String>,
19    /// Whether to attach the canonical pleme-io auto-release workflow.
20    pub attach_auto_release: bool,
21}
22
23/// Decorate `scaffold` with a `caixa.lisp` (and optionally a
24/// `.github/workflows/auto-release.yml`). Idempotent on both files.
25pub fn attach_caixa_biblioteca(scaffold: &mut CrateScaffold, config: &CaixaConfig) {
26    if !scaffold.files.iter().any(|f| f.path == "caixa.lisp") {
27        scaffold.add_file(
28            "caixa.lisp",
29            render_caixa_biblioteca(
30                &scaffold.name,
31                &scaffold.version,
32                config.description.as_deref(),
33            ),
34        );
35    }
36    if config.attach_auto_release
37        && !scaffold
38            .files
39            .iter()
40            .any(|f| f.path == ".github/workflows/auto-release.yml")
41    {
42        scaffold.add_file(
43            ".github/workflows/auto-release.yml",
44            render_auto_release_workflow(),
45        );
46    }
47}
48
49/// The canonical `(defcaixa <name> …)` form for a Biblioteca-kind
50/// Rust crate. **Aligned with the pleme-doc-gen caixa.rs parser** so
51/// every emitted repo is directly consumable by
52/// `pleme-doc-gen publish-bulk` and the substrate's per-ecosystem
53/// renderers. Shape:
54///
55/// ```text
56/// (defcaixa <name>
57///   :kind         "Biblioteca"
58///   :ecosystem    "rust-single-crate"
59///   :package      { :name "..." :version "..." :license "MIT"
60///                   :description "..." :repository "..." }
61///   :ci-config    { :bump { :default-type "patch" } }
62///   :workflows    [ :auto-release ])
63/// ```
64#[must_use]
65pub fn render_caixa_biblioteca(name: &str, version: &str, description: Option<&str>) -> String {
66    let desc = description.unwrap_or("Generated by tatara-rust-ast.");
67    let repo = format!("https://github.com/pleme-io/{name}");
68    format!(
69        r#";; caixa.lisp — generated by tatara-rust-caixa (tatara-rust-ast).
70;;
71;; Consumed by `pleme-doc-gen` for the SDLC pipeline (Cargo.toml +
72;; .pleme-io-release.toml + CI shims + nix module trio + flake.nix).
73;; Re-emit with `pleme-doc-gen caixa --source caixa.lisp --out .`.
74
75(defcaixa {name}
76  :kind         "Biblioteca"
77  :ecosystem    "rust-single-crate"
78
79  :package      {{ :name        "{name}"
80                  :version     "{version}"
81                  :license     "MIT"
82                  :description "{desc}"
83                  :repository  "{repo}"
84                  :homepage    "{repo}"
85                  :categories  [ "development-tools::procedural-macro-helpers" ]
86                  :keywords    [ "tatara" "macro" "derive" "generated" ] }}
87
88  :ci-config    {{ :bump    {{ :default-type "patch" }}
89                  :publish {{ :no-verify false }} }}
90
91  :workflows    [ :auto-release ]
92  :stacks       [ ]
93  :depends-on   [ ]
94  :exposes      [ :rust-crate ]
95  :publish-to-git true)
96"#
97    )
98}
99
100/// The canonical pleme-io auto-release workflow shim for a Biblioteca
101/// (single-crate) — points directly at substrate's
102/// `cargo-auto-release.yml` rather than the polymorphic
103/// `auto-release.yml`. The polymorphic dispatcher's detect→branch
104/// shape startup-fails on freshly-created repos; the per-language
105/// reusable is the canonical working shape used by every existing
106/// pleme-io Rust crate (engenho, etc.).
107#[must_use]
108pub fn render_auto_release_workflow() -> String {
109    r#"# Auto-emitted by tatara-rust-caixa.
110# The reusable substrate workflow does auto-bump → tag → publish to crates.io.
111on:
112  push:
113    branches: [main]
114  workflow_dispatch:
115    inputs:
116      bump-type:
117        description: "patch | minor | major"
118        required: false
119        default: patch
120
121jobs:
122  release:
123    uses: pleme-io/substrate/.github/workflows/cargo-auto-release.yml@main
124    with:
125      bump-type: ${{ inputs.bump-type || 'patch' }}
126      regenerate-cargo-nix: "false"
127    secrets: inherit
128"#
129    .to_string()
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn attaches_caixa_lisp() {
138        let mut s = CrateScaffold::new("my-derive", "0.1.0");
139        attach_caixa_biblioteca(
140            &mut s,
141            &CaixaConfig {
142                description: Some("Test crate".into()),
143                attach_auto_release: false,
144            },
145        );
146        let files = s.to_files();
147        assert!(files.contains_key("caixa.lisp"));
148        let lisp = files.get("caixa.lisp").unwrap();
149        // pleme-doc-gen parser contract:
150        assert!(lisp.contains("(defcaixa my-derive"));
151        assert!(lisp.contains(r#":kind         "Biblioteca""#));
152        assert!(lisp.contains(r#":ecosystem    "rust-single-crate""#));
153        assert!(lisp.contains(r#":name        "my-derive""#));
154        assert!(lisp.contains(r#":version     "0.1.0""#));
155        assert!(lisp.contains("Test crate"));
156    }
157
158    /// **Contract test** — every keyword pleme-doc-gen's `caixa.rs`
159    /// parser reads MUST appear in the emitted form. Mirrors the
160    /// `out.kind = read_keyword(body, ":kind")` /
161    /// `read_dict_block(body, ":package")` set in
162    /// `pleme-doc-gen/src/caixa.rs:1787`.
163    #[test]
164    fn emits_every_keyword_pleme_doc_gen_parser_consumes() {
165        let lisp = render_caixa_biblioteca("foo-derive", "1.2.3", Some("Foo macro."));
166
167        // Top-level form name (`defcaixa <name>`).
168        assert!(lisp.starts_with(";;") || lisp.contains("(defcaixa foo-derive"));
169
170        // Required top-level keywords.
171        for kw in [":kind", ":ecosystem", ":package", ":ci-config", ":workflows"] {
172            assert!(
173                lisp.contains(kw),
174                "caixa.lisp missing required keyword `{kw}` for pleme-doc-gen parser"
175            );
176        }
177
178        // Required :package dict keys (per pleme-doc-gen's
179        // parse_dict-on-:package-block path).
180        for pkg_key in [":name", ":version", ":license", ":description", ":repository"] {
181            assert!(
182                lisp.contains(pkg_key),
183                "caixa.lisp :package missing `{pkg_key}`"
184            );
185        }
186
187        // Ecosystem must be one pleme-doc-gen's emitter table knows.
188        // (per ecosystems.rs line 58: "rust-single-crate" is supported)
189        assert!(lisp.contains(r#":ecosystem    "rust-single-crate""#));
190    }
191
192    #[test]
193    fn attaches_workflow_when_enabled() {
194        let mut s = CrateScaffold::new("x", "0.1.0");
195        attach_caixa_biblioteca(
196            &mut s,
197            &CaixaConfig {
198                attach_auto_release: true,
199                ..Default::default()
200            },
201        );
202        assert!(
203            s.to_files()
204                .contains_key(".github/workflows/auto-release.yml")
205        );
206    }
207
208    #[test]
209    fn idempotent() {
210        let mut s = CrateScaffold::new("x", "0.1.0");
211        s.add_file("caixa.lisp", "custom");
212        attach_caixa_biblioteca(&mut s, &CaixaConfig::default());
213        assert_eq!(s.to_files()["caixa.lisp"], "custom");
214    }
215
216    #[test]
217    fn workflow_references_substrate_reusable() {
218        let w = render_auto_release_workflow();
219        // Per the diagnosed startup_failure: must call the per-language
220        // reusable, not the polymorphic dispatcher.
221        assert!(w.contains("uses: pleme-io/substrate/.github/workflows/cargo-auto-release.yml"));
222        assert!(w.contains(r#"regenerate-cargo-nix: "false""#));
223        assert!(w.contains("secrets: inherit"));
224    }
225}