torvyn_cli/templates/
mod.rs1pub mod content;
7
8use crate::cli::TemplateKind;
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11
12#[derive(Debug, Clone)]
14pub struct TemplateFile {
15 pub relative_path: PathBuf,
17 pub content: String,
19}
20
21#[derive(Debug, Clone)]
23pub struct Template {
24 #[allow(dead_code)]
26 pub description: String,
27 pub files: Vec<TemplateFile>,
29}
30
31#[derive(Debug, Clone)]
33pub struct TemplateVars {
34 pub project_name: String,
36 pub component_type: String,
38 pub date: String,
40 pub torvyn_version: String,
42 pub contract_version: String,
44}
45
46impl TemplateVars {
47 pub fn new(project_name: &str, contract_version: &str) -> Self {
51 Self {
52 project_name: project_name.to_string(),
53 component_type: to_pascal_case(project_name),
54 date: chrono::Utc::now().format("%Y-%m-%d").to_string(),
55 torvyn_version: env!("CARGO_PKG_VERSION").to_string(),
56 contract_version: contract_version.to_string(),
57 }
58 }
59
60 fn to_map(&self) -> HashMap<&'static str, &str> {
62 let mut m = HashMap::new();
63 m.insert("project_name", self.project_name.as_str());
64 m.insert("component_type", self.component_type.as_str());
65 m.insert("date", self.date.as_str());
66 m.insert("torvyn_version", self.torvyn_version.as_str());
67 m.insert("contract_version", self.contract_version.as_str());
68 m
69 }
70}
71
72pub fn to_pascal_case(s: &str) -> String {
82 s.split('-')
83 .filter(|p| !p.is_empty())
84 .map(|part| {
85 let mut chars = part.chars();
86 match chars.next() {
87 None => String::new(),
88 Some(c) => {
89 let upper: String = c.to_uppercase().collect();
90 upper + &chars.as_str().to_lowercase()
91 }
92 }
93 })
94 .collect()
95}
96
97pub fn substitute(template: &str, vars: &TemplateVars) -> String {
101 let map = vars.to_map();
102 let mut result = template.to_string();
103 for (key, value) in &map {
104 let token = format!("{{{{{key}}}}}");
105 result = result.replace(&token, value);
106 }
107 result
108}
109
110pub fn get_template(kind: TemplateKind) -> Template {
112 match kind {
113 TemplateKind::Transform => content::transform_template(),
114 TemplateKind::Source => content::source_template(),
115 TemplateKind::Sink => content::sink_template(),
116 TemplateKind::Filter => content::filter_template(),
117 TemplateKind::Router => content::router_template(),
118 TemplateKind::Aggregator => content::aggregator_template(),
119 TemplateKind::FullPipeline => content::full_pipeline_template(),
120 TemplateKind::Empty => content::empty_template(),
121 }
122}
123
124pub fn expand_template(
129 template: &Template,
130 vars: &TemplateVars,
131 target_dir: &Path,
132) -> Result<Vec<PathBuf>, std::io::Error> {
133 let mut created_files = Vec::new();
134 for tf in &template.files {
135 let content = substitute(&tf.content, vars);
136 let full_path = target_dir.join(&tf.relative_path);
137
138 if let Some(parent) = full_path.parent() {
139 std::fs::create_dir_all(parent)?;
140 }
141 std::fs::write(&full_path, &content)?;
142 created_files.push(tf.relative_path.clone());
143 }
144 Ok(created_files)
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn test_to_pascal_case_basic() {
153 assert_eq!(to_pascal_case("my-transform"), "MyTransform");
154 }
155
156 #[test]
157 fn test_to_pascal_case_single_word() {
158 assert_eq!(to_pascal_case("hello"), "Hello");
159 }
160
161 #[test]
162 fn test_to_pascal_case_multi_segment() {
163 assert_eq!(to_pascal_case("a-b-c"), "ABC");
164 }
165
166 #[test]
167 fn test_to_pascal_case_already_capitalized() {
168 assert_eq!(to_pascal_case("My-Thing"), "MyThing");
169 }
170
171 #[test]
172 fn test_substitute_basic() {
173 let vars = TemplateVars::new("my-project", "0.1.0");
174 let result = substitute("name = \"{{project_name}}\"", &vars);
175 assert_eq!(result, "name = \"my-project\"");
176 }
177
178 #[test]
179 fn test_substitute_multiple_vars() {
180 let vars = TemplateVars::new("my-project", "0.1.0");
181 let result = substitute("struct {{component_type}}; // v{{contract_version}}", &vars);
182 assert_eq!(result, "struct MyProject; // v0.1.0");
183 }
184
185 #[test]
186 fn test_substitute_unknown_token_preserved() {
187 let vars = TemplateVars::new("x", "0.1.0");
188 let result = substitute("{{unknown_token}}", &vars);
189 assert_eq!(result, "{{unknown_token}}");
190 }
191
192 #[test]
193 fn test_get_template_returns_nonempty() {
194 for kind in [
195 TemplateKind::Source,
196 TemplateKind::Sink,
197 TemplateKind::Transform,
198 TemplateKind::Filter,
199 TemplateKind::Empty,
200 TemplateKind::FullPipeline,
201 ] {
202 let t = get_template(kind);
203 assert!(!t.files.is_empty(), "Template {kind:?} has no files");
204 }
205 }
206}