#[derive(Debug, Clone, Copy)]
pub struct Template {
pub dest_path: &'static str,
pub contents: &'static str,
}
pub const TEMPLATES: &[Template] = &[
Template {
dest_path: "Cargo.toml",
contents: include_str!("../templates/Cargo.toml.tmpl"),
},
Template {
dest_path: "src/main.rs",
contents: include_str!("../templates/src/main.rs.tmpl"),
},
Template {
dest_path: "assets/game.toml",
contents: include_str!("../templates/assets/game.toml.tmpl"),
},
Template {
dest_path: "assets/maps/start.txt",
contents: include_str!("../templates/assets/maps/start.txt"),
},
Template {
dest_path: ".gitignore",
contents: include_str!("../templates/gitignore.tmpl"),
},
Template {
dest_path: "README.md",
contents: include_str!("../templates/README.md.tmpl"),
},
];
pub fn substitute(contents: &str, name: &str) -> String {
contents.replace("{name}", name)
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE_NAME: &str = "test-game";
#[test]
fn templates_cover_required_files() {
let paths: Vec<&str> = TEMPLATES.iter().map(|t| t.dest_path).collect();
for required in [
"Cargo.toml",
"src/main.rs",
"assets/game.toml",
".gitignore",
"README.md",
] {
assert!(
paths.contains(&required),
"missing required template: {required} (have {paths:?})"
);
}
assert!(
paths.iter().any(|p| p.starts_with("assets/maps/")),
"missing starter map under assets/maps/ (have {paths:?})"
);
}
#[test]
fn template_paths_are_unique_and_relative() {
let mut seen = std::collections::HashSet::new();
for t in TEMPLATES {
assert!(
!t.dest_path.starts_with('/'),
"dest_path must be relative: {}",
t.dest_path
);
assert!(
!t.dest_path.contains(".."),
"dest_path must not escape root: {}",
t.dest_path
);
assert!(
seen.insert(t.dest_path),
"duplicate dest_path: {}",
t.dest_path
);
}
}
#[test]
fn substitute_replaces_every_name_token() {
let out = substitute("hello {name}, welcome {name}!", "world");
assert_eq!(out, "hello world, welcome world!");
}
#[test]
fn substitute_leaves_text_without_token_untouched() {
let out = substitute("no placeholders here", SAMPLE_NAME);
assert_eq!(out, "no placeholders here");
}
fn rendered(dest_path: &str) -> String {
let t = TEMPLATES
.iter()
.find(|t| t.dest_path == dest_path)
.unwrap_or_else(|| panic!("template missing: {dest_path}"));
let with_name = substitute(t.contents, SAMPLE_NAME);
with_name.replace("{foglet_game_dependency}", "foglet_game = \"0.1\"")
}
#[test]
fn cargo_toml_template_parses_as_toml_after_substitution() {
let body = rendered("Cargo.toml");
let parsed: toml::Value =
toml::from_str(&body).expect("rendered Cargo.toml should parse as TOML");
let pkg = parsed
.get("package")
.and_then(|v| v.as_table())
.expect("Cargo.toml has [package]");
assert_eq!(
pkg.get("name").and_then(|v| v.as_str()),
Some(SAMPLE_NAME),
"[package].name should be substituted with the project name"
);
}
#[test]
fn game_toml_template_parses_as_game_config_after_substitution() {
let body = rendered("assets/game.toml");
let cfg = foglet_game::GameConfig::from_toml_str(&body)
.expect("rendered game.toml should pass GameConfig validation");
assert_eq!(cfg.game.slug, SAMPLE_NAME);
assert_eq!(cfg.game.title, SAMPLE_NAME);
assert!(cfg.game.min_width >= 80);
assert!(cfg.game.min_height >= 24);
}
#[test]
fn main_rs_template_parses_via_syn_after_substitution() {
let body = rendered("src/main.rs");
let file = syn::parse_file(&body)
.unwrap_or_else(|e| panic!("rendered main.rs failed to parse: {e}"));
let has_main = file.items.iter().any(|item| match item {
syn::Item::Fn(f) => f.sig.ident == "main",
_ => false,
});
assert!(has_main, "main.rs template must define `fn main`");
}
#[test]
fn starter_map_uses_only_legend_glyphs() {
let legend =
foglet_game::TileLegend::from_pairs([("#", "wall"), (".", "floor"), ("@", "floor")])
.expect("starter legend constructs");
let map_text = TEMPLATES
.iter()
.find(|t| t.dest_path == "assets/maps/start.txt")
.expect("starter map present")
.contents;
let map = foglet_game::parse_map(map_text, &legend)
.expect("starter map should parse against its legend");
assert!(map.in_bounds(0, 0));
}
#[test]
fn gitignore_template_includes_target_and_save_dirs() {
let body = rendered(".gitignore");
for needle in ["/target/", "/.fgk/"] {
assert!(
body.contains(needle),
".gitignore should ignore {needle} (got: {body})"
);
}
}
#[test]
fn readme_template_substitutes_name() {
let body = rendered("README.md");
assert!(
body.contains(SAMPLE_NAME),
"README should mention the project name after substitution"
);
assert!(
!body.contains("{name}"),
"README should not have unsubstituted `{{name}}` tokens"
);
}
#[test]
fn readme_template_mentions_prompt_primitives() {
let body = rendered("README.md");
for needle in ["ChoicePrompt", "ConfirmPrompt", "AnyKeyPrompt"] {
assert!(
body.contains(needle),
"README should mention `{needle}` so authors discover the prompt API"
);
}
}
}