pub struct Template {
pub name: &'static str,
pub description: &'static str,
pub dockerfile: &'static str,
}
pub const TEMPLATES: &[Template] = &[Template {
name: "claude",
description: "Claude Code on Node.js 22 with git, running as a non-root user",
dockerfile: include_str!("../assets/templates/claude.Dockerfile"),
}];
#[must_use]
pub fn find(name: &str) -> Option<&'static Template> {
TEMPLATES.iter().find(|template| template.name == name)
}
#[must_use]
pub fn names() -> String {
TEMPLATES
.iter()
.map(|template| template.name)
.collect::<Vec<_>>()
.join(", ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_resolves_known_names_only() {
assert_eq!(find("claude").map(|t| t.name), Some("claude"));
assert!(find("nope").is_none());
assert!(find("").is_none());
}
#[test]
fn template_names_are_unique() {
let mut names: Vec<_> = TEMPLATES.iter().map(|t| t.name).collect();
let total = names.len();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), total, "template names must be unique");
}
#[test]
fn every_template_is_a_buildable_dockerfile() {
for template in TEMPLATES {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("Dockerfile");
std::fs::write(&path, template.dockerfile).unwrap();
assert!(
crate::rootfs_builder::looks_like_dockerfile(&path),
"template {} is not a Dockerfile",
template.name
);
assert!(
!template.description.is_empty(),
"template {} needs a description",
template.name
);
}
}
#[test]
fn templates_do_not_need_a_build_context() {
for template in TEMPLATES {
for line in template.dockerfile.lines() {
let directive = line.trim().to_ascii_uppercase();
assert!(
!directive.starts_with("COPY ") && !directive.starts_with("ADD "),
"template {} uses a build context: {line}",
template.name
);
}
}
}
}