use crate::sexp_ast::{Forms, SExp};
use std::collections::BTreeMap;
#[derive(Debug, Clone)]
pub struct ScaffoldSpec {
pub name: String,
pub ecosystem: String,
pub kind: Option<String>,
pub version: Option<String>,
pub description: Option<String>,
pub license: Option<String>,
pub repository: Option<String>,
pub homepage: Option<String>,
pub authors: Option<String>,
pub group_id: Option<String>,
pub extra_package: BTreeMap<String, String>,
pub workflows: Vec<String>,
pub keywords: Vec<String>,
pub categories: Vec<String>,
}
impl ScaffoldSpec {
pub fn new(name: impl Into<String>, ecosystem: impl Into<String>) -> Self {
Self {
name: name.into(),
ecosystem: ecosystem.into(),
kind: None,
version: None,
description: None,
license: None,
repository: None,
homepage: None,
authors: None,
group_id: None,
extra_package: BTreeMap::new(),
workflows: Vec::new(),
keywords: Vec::new(),
categories: Vec::new(),
}
}
}
struct EcosystemDefaults {
kind: &'static str,
version: &'static str,
license: &'static str,
workflows: &'static [&'static str],
}
fn defaults_for(eco: &str) -> EcosystemDefaults {
match eco {
"rust-single-crate" | "rust-workspace" | "npm" | "python" |
"go" | "java-maven" | "java-gradle-kts" | "dotnet-csproj" |
"ocaml-dune" | "swift-spm" | "elixir-mix" | "ruby-gem" |
"nim-nimble" | "scala-sbt" | "clojure-deps" | "r-description" |
"lua-rockspec" | "cpp-conan" | "python-conda" | "python-pipenv" |
"python-pdm" | "js-deno" | "js-pnpm" | "cpp-vcpkg" | "cpp-meson" |
"cpp-cmake" | "fortran-fpm" | "gleam" | "ada-alire" | "haskell-cabal" |
"racket-info" | "crystal" | "dart" | "composer" | "julia" => EcosystemDefaults {
kind: "Biblioteca",
version: "0.1.0",
license: "MIT",
workflows: &["auto-release", "pre-merge-gate", "security-gate"],
},
"zig" => EcosystemDefaults {
kind: "Binario",
version: "0.1.0",
license: "MIT",
workflows: &["auto-release", "pre-merge-gate"],
},
"helm" => EcosystemDefaults {
kind: "Aplicacao",
version: "0.1.0",
license: "Apache-2.0",
workflows: &["auto-release", "pre-merge-gate"],
},
"github-action" => EcosystemDefaults {
kind: "Aplicacao",
version: "0.1.0",
license: "MIT",
workflows: &["auto-release", "pre-merge-gate"],
},
_ => EcosystemDefaults {
kind: "Biblioteca",
version: "0.1.0",
license: "MIT",
workflows: &["auto-release", "pre-merge-gate", "security-gate"],
},
}
}
fn default_repository(name: &str) -> String {
let mut url = String::from("https://github.com/pleme-io/");
url.push_str(name);
url
}
pub fn build(spec: &ScaffoldSpec) -> Forms {
let d = defaults_for(&spec.ecosystem);
let kind = spec.kind.clone().unwrap_or_else(|| d.kind.to_string());
let version = spec.version.clone().unwrap_or_else(|| d.version.to_string());
let license = spec.license.clone().unwrap_or_else(|| d.license.to_string());
let repo = spec.repository.clone()
.unwrap_or_else(|| default_repository(&spec.name));
let mut package_map: Vec<(SExp, SExp)> = vec![
(SExp::kw("name"), SExp::str(&spec.name)),
(SExp::kw("version"), SExp::str(version)),
];
if let Some(desc) = &spec.description {
package_map.push((SExp::kw("description"), SExp::str(desc)));
}
package_map.push((SExp::kw("license"), SExp::str(license)));
package_map.push((SExp::kw("repository"), SExp::str(repo)));
if let Some(home) = &spec.homepage {
package_map.push((SExp::kw("homepage"), SExp::str(home)));
}
if let Some(authors) = &spec.authors {
package_map.push((SExp::kw("authors"), SExp::str(authors)));
}
if let Some(gid) = &spec.group_id {
package_map.push((SExp::kw("group-id"), SExp::str(gid)));
}
for (k, v) in &spec.extra_package {
package_map.push((SExp::kw(k), SExp::str(v)));
}
if !spec.keywords.is_empty() {
package_map.push((SExp::kw("keywords"),
SExp::Vector(spec.keywords.iter().map(SExp::str).collect())));
}
if !spec.categories.is_empty() {
package_map.push((SExp::kw("categories"),
SExp::Vector(spec.categories.iter().map(SExp::str).collect())));
}
let workflows_src: Vec<&str> = if spec.workflows.is_empty() {
d.workflows.iter().copied().collect()
} else {
spec.workflows.iter().map(String::as_str).collect()
};
let workflow_kws: Vec<SExp> = workflows_src.iter().map(|w| SExp::kw(*w)).collect();
let ci_config = SExp::Map(vec![
(
SExp::kw("bump"),
SExp::Map(vec![(SExp::kw("default-type"), SExp::str("patch"))]),
),
(
SExp::kw("publish"),
SExp::Map(vec![(SExp::kw("no-verify"), SExp::sym("true"))]),
),
]);
let form = SExp::list([
SExp::sym("defcaixa"),
SExp::kw("name"), SExp::str(&spec.name),
SExp::kw("kind"), SExp::kw(kind),
SExp::kw("ecosystem"), SExp::kw(&spec.ecosystem),
SExp::kw("package"), SExp::Map(package_map),
SExp::kw("ci-config"), ci_config,
SExp::kw("workflows"), SExp::Vector(workflow_kws),
]);
Forms(vec![form])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::Render;
#[test]
fn minimal_spec_renders_complete_caixa() {
let spec = ScaffoldSpec::new("demo", "rust-single-crate");
let out = build(&spec).render();
assert!(out.contains("defcaixa"));
assert!(out.contains("\"demo\""));
assert!(out.contains(":kind") && out.contains(":Biblioteca"),
"default kind for rust-single-crate should be Biblioteca; got: {out}");
assert!(out.contains(":ecosystem") && out.contains(":rust-single-crate"));
assert!(out.contains("\"MIT\""), "default license");
assert!(out.contains("\"0.1.0\""), "default version");
assert!(out.contains(":auto-release"), "default workflow");
}
#[test]
fn description_threads_through_with_typed_escape() {
let mut spec = ScaffoldSpec::new("escape-test", "rust-single-crate");
spec.description = Some(r#"A "quoted" test"#.to_string());
let out = build(&spec).render();
assert!(out.contains(r#":description "A \"quoted\" test""#),
"description should be typed-escaped; got: {out}");
}
#[test]
fn helm_picks_aplicacao_kind_and_apache_default() {
let spec = ScaffoldSpec::new("my-chart", "helm");
let out = build(&spec).render();
assert!(out.contains(":kind") && out.contains(":Aplicacao"));
assert!(out.contains("\"Apache-2.0\""));
}
#[test]
fn zig_defaults_to_binario_kind() {
let spec = ScaffoldSpec::new("tool", "zig");
let out = build(&spec).render();
assert!(out.contains(":kind") && out.contains(":Binario"));
}
#[test]
fn custom_workflows_override_defaults() {
let mut spec = ScaffoldSpec::new("x", "rust-single-crate");
spec.workflows = vec!["custom-flow".to_string()];
let out = build(&spec).render();
assert!(out.contains(":custom-flow"));
assert!(!out.contains(":auto-release"), "should not include defaults");
}
#[test]
fn round_trip_scaffold_then_render_emits_artifacts() {
let spec = ScaffoldSpec::new("scaffold-rt", "rust-single-crate");
let src = build(&spec).render();
let tmp = tempdir::TempDir::new("scaffold-rt")
.expect("tempdir");
let files = crate::caixa::render(&src, tmp.path(), true)
.expect("caixa::render of scaffolded source");
assert!(files.len() > 5,
"scaffold→render should emit >5 files; got {}", files.len());
let names: Vec<String> = files.iter()
.filter_map(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
.collect();
assert!(names.iter().any(|n| n == "Cargo.toml"),
"scaffolded rust-single-crate should produce Cargo.toml; got {names:?}");
}
}