mecha10_templates/services/
project_template_service.rs1use crate::templates::{InfraTemplates, MetaTemplates, RlBundleTemplates, RustTemplates};
6use anyhow::Result;
7use std::path::Path;
8
9macro_rules! delegate_create {
10 ($(#[$meta:meta])* $name:ident($field:ident)) => {
11 $(#[$meta])*
12 pub async fn $name(&self, path: &Path) -> Result<()> {
13 self.$field.$name(path).await
14 }
15 };
16 ($(#[$meta:meta])* $name:ident($field:ident, project_name)) => {
17 $(#[$meta])*
18 pub async fn $name(&self, path: &Path, project_name: &str) -> Result<()> {
19 self.$field.$name(path, project_name).await
20 }
21 };
22 ($(#[$meta:meta])* $name:ident($field:ident, assets_root)) => {
23 $(#[$meta])*
24 pub async fn $name(&self, path: &Path, assets_root: &Path) -> Result<()> {
25 self.$field.$name(path, assets_root).await
26 }
27 };
28 ($(#[$meta:meta])* $name:ident($field:ident, project_name, assets_root)) => {
29 $(#[$meta])*
30 pub async fn $name(&self, path: &Path, project_name: &str, assets_root: &Path) -> Result<()> {
31 self.$field.$name(path, project_name, assets_root).await
32 }
33 };
34}
35
36pub struct ProjectTemplateService {
37 rust: RustTemplates,
38 infra: InfraTemplates,
39 meta: MetaTemplates,
40 rl_bundle: RlBundleTemplates,
41}
42
43impl ProjectTemplateService {
44 pub fn new() -> Self {
46 Self {
47 rust: RustTemplates::new(),
48 infra: InfraTemplates::new(),
49 meta: MetaTemplates::new(),
50 rl_bundle: RlBundleTemplates::new(),
51 }
52 }
53
54 delegate_create!(
55 create_readme(meta, project_name, assets_root)
58 );
59
60 delegate_create!(
61 create_gitignore(meta, assets_root)
64 );
65
66 delegate_create!(
67 create_cargo_toml(rust, project_name, assets_root)
70 );
71
72 delegate_create!(
73 create_main_rs(rust, project_name, assets_root)
76 );
77
78 delegate_create!(
79 create_build_rs(rust, assets_root)
84 );
85
86 delegate_create!(
87 create_env_example(infra, project_name, assets_root)
90 );
91
92 pub async fn create_rustfmt_toml(&self, path: &Path, assets_root: &Path) -> Result<()> {
95 self.rust.create_rustfmt_toml(path, assets_root).await
96 }
97
98 delegate_create!(
99 create_docker_compose(infra, project_name, assets_root)
103 );
104
105 delegate_create!(
106 create_remote_docker_files(infra, assets_root)
110 );
111
112 delegate_create!(
113 create_rl_docker_files(infra, assets_root)
117 );
118
119 delegate_create!(
120 create_package_json(meta, project_name, assets_root)
123 );
124
125 delegate_create!(
126 create_requirements_txt(meta, assets_root)
129 );
130
131 delegate_create!(
132 create_python_version(meta, assets_root)
136 );
137
138 pub async fn create_rl_training_bundle(&self, path: &Path, assets_root: &Path) -> Result<()> {
144 self.rl_bundle.append_rl_requirements(path, assets_root).await?;
145 self.rl_bundle.create_rl_jobs(path, assets_root).await?;
146 Ok(())
147 }
148}
149
150impl Default for ProjectTemplateService {
151 fn default() -> Self {
152 Self::new()
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use tempfile::TempDir;
160
161 fn stage_assets_root_with(name: &str, content: &str) -> TempDir {
164 let assets_root = TempDir::new().unwrap();
165 let project_dir = assets_root.path().join("project");
166 std::fs::create_dir_all(&project_dir).unwrap();
167 std::fs::write(project_dir.join(name), content).unwrap();
168 assets_root
169 }
170
171 #[tokio::test]
172 async fn create_readme_replaces_project_name_placeholder() {
173 let temp = TempDir::new().unwrap();
174 let assets_root = stage_assets_root_with("README.md.template", "# {{project_name}}\n");
175 let service = ProjectTemplateService::new();
176
177 service
178 .create_readme(temp.path(), "my-robot", assets_root.path())
179 .await
180 .unwrap();
181
182 let content = tokio::fs::read_to_string(temp.path().join("README.md")).await.unwrap();
183 assert!(content.contains("my-robot"));
184 assert!(!content.contains("{{project_name}}"));
185 }
186
187 #[tokio::test]
188 async fn create_gitignore_writes_file() {
189 let temp = TempDir::new().unwrap();
190 let assets_root = stage_assets_root_with(".gitignore.template", "target/\n");
191 let service = ProjectTemplateService::new();
192
193 service.create_gitignore(temp.path(), assets_root.path()).await.unwrap();
194
195 assert!(temp.path().join(".gitignore").exists());
196 }
197
198 #[tokio::test]
199 async fn create_python_version_writes_a_pin_mecha10_setup_can_read() {
200 let temp = TempDir::new().unwrap();
201 let assets_root = stage_assets_root_with(".python-version.template", "3.11\n");
202 let service = ProjectTemplateService::new();
203
204 service
205 .create_python_version(temp.path(), assets_root.path())
206 .await
207 .unwrap();
208
209 let content = tokio::fs::read_to_string(temp.path().join(".python-version"))
210 .await
211 .unwrap();
212 let mut parts = content.trim().split('.');
213 assert!(parts.next().unwrap().parse::<u32>().is_ok());
214 assert!(parts.next().unwrap().parse::<u32>().is_ok());
215 assert!(parts.next().is_none());
216 }
217
218 #[tokio::test]
219 async fn create_cargo_toml_includes_project_name() {
220 let temp = TempDir::new().unwrap();
221 let assets_root =
222 stage_assets_root_with("Cargo.toml.prod.template", "[package]\nname = \"{{project_name}}\"\n");
223 let service = ProjectTemplateService::new();
224
225 service
226 .create_cargo_toml(temp.path(), "my-robot", assets_root.path())
227 .await
228 .unwrap();
229
230 let content = tokio::fs::read_to_string(temp.path().join("Cargo.toml")).await.unwrap();
231 assert!(content.contains("my-robot"));
232 }
233
234 #[tokio::test]
235 async fn create_main_rs_writes_file() {
236 let temp = TempDir::new().unwrap();
237 let assets_root = stage_assets_root_with("main.rs.template", "fn main() {}\n");
238 let service = ProjectTemplateService::new();
239
240 service
241 .create_main_rs(temp.path(), "my-robot", assets_root.path())
242 .await
243 .unwrap();
244
245 assert!(temp.path().join("src/main.rs").exists());
246 }
247
248 #[tokio::test]
249 async fn create_rl_training_bundle_requires_requirements_txt_created_first() {
250 let temp = TempDir::new().unwrap();
251 let assets_root = TempDir::new().unwrap();
252 let jobs_dir = assets_root.path().join("rl/jobs");
253 std::fs::create_dir_all(&jobs_dir).unwrap();
254 std::fs::write(jobs_dir.join("train.yml.template"), "flow_type: train\n").unwrap();
255 std::fs::write(
256 jobs_dir.join("train-from-checkpoint.yml.template"),
257 "flow_type: resume-run\n",
258 )
259 .unwrap();
260 std::fs::write(jobs_dir.join("hypertune.yml.template"), "flow_type: tune\n").unwrap();
261 std::fs::write(
262 assets_root.path().join("rl/requirements-rl.txt.template"),
263 "mecha10-rl-task-runner\n",
264 )
265 .unwrap();
266
267 let service = ProjectTemplateService::new();
268
269 service
270 .create_rl_training_bundle(temp.path(), assets_root.path())
271 .await
272 .unwrap();
273
274 assert!(temp.path().join("configs/rl/jobs/train.yml").exists());
275 assert!(temp.path().join("requirements.txt").exists());
276 assert!(!temp.path().join("kestra").exists());
277 }
278}