1pub struct Vars<'a> {
13 pub project_name: &'a str,
16 pub project_name_snake: &'a str,
19}
20
21pub const FILES: &[(&str, &str)] = &[
29 (
30 "Cargo.toml",
31 include_str!("../templates/new/Cargo.toml.tmpl"),
32 ),
33 (
34 "src/main.rs",
35 include_str!("../templates/new/src/main.rs.tmpl"),
36 ),
37 (
38 "src/modules/mod.rs",
39 include_str!("../templates/new/src/modules/mod.rs.tmpl"),
40 ),
41 (
42 "src/modules/hello/mod.rs",
43 include_str!("../templates/new/src/modules/hello/mod.rs.tmpl"),
44 ),
45 (
46 "src/modules/hello/handlers.rs",
47 include_str!("../templates/new/src/modules/hello/handlers.rs.tmpl"),
48 ),
49 (
50 ".env.example",
51 include_str!("../templates/new/.env.example.tmpl"),
52 ),
53 (
54 ".gitignore",
55 include_str!("../templates/new/.gitignore.tmpl"),
56 ),
57 ("README.md", include_str!("../templates/new/README.md.tmpl")),
58];
59
60pub fn render(template: &str, vars: &Vars<'_>) -> String {
64 template
65 .replace("{{project_name_snake}}", vars.project_name_snake)
66 .replace("{{project_name}}", vars.project_name)
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn render_substitutes_known_vars() {
75 let v = Vars {
76 project_name: "my-app",
77 project_name_snake: "my_app",
78 };
79 let s = render(
80 "name = \"{{project_name}}\"\ntarget = \"{{project_name_snake}}\"",
81 &v,
82 );
83 assert_eq!(s, "name = \"my-app\"\ntarget = \"my_app\"");
84 }
85
86 #[test]
87 fn render_leaves_unknown_vars_alone() {
88 let v = Vars {
89 project_name: "x",
90 project_name_snake: "x",
91 };
92 let s = render("{{unknown_thing}}", &v);
93 assert_eq!(s, "{{unknown_thing}}");
94 }
95
96 #[test]
97 fn file_manifest_is_non_empty_and_relative() {
98 assert!(!FILES.is_empty(), "scaffold must emit at least one file");
99 for (path, _) in FILES {
100 assert!(
101 !path.starts_with('/') && !path.contains(".."),
102 "manifest paths must be project-relative without traversal: {path}"
103 );
104 }
105 }
106}