mecha10_templates/services/
project_template_service.rs1use crate::templates::{DatasetBundleTemplates, 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 dataset_bundle: DatasetBundleTemplates,
42}
43
44impl ProjectTemplateService {
45 pub fn new() -> Self {
47 Self {
48 rust: RustTemplates::new(),
49 infra: InfraTemplates::new(),
50 meta: MetaTemplates::new(),
51 rl_bundle: RlBundleTemplates::new(),
52 dataset_bundle: DatasetBundleTemplates::new(),
53 }
54 }
55
56 delegate_create!(
57 create_readme(meta, project_name, assets_root)
60 );
61
62 delegate_create!(
63 create_gitignore(meta, assets_root)
66 );
67
68 delegate_create!(
69 create_cargo_toml(rust, project_name, assets_root)
72 );
73
74 delegate_create!(
75 create_main_rs(rust, project_name, assets_root)
78 );
79
80 delegate_create!(
81 create_build_rs(rust, assets_root)
86 );
87
88 delegate_create!(
89 create_env_example(infra, project_name, assets_root)
92 );
93
94 pub async fn create_rustfmt_toml(&self, path: &Path, assets_root: &Path) -> Result<()> {
97 self.rust.create_rustfmt_toml(path, assets_root).await
98 }
99
100 delegate_create!(
101 create_docker_compose(infra, project_name, assets_root)
105 );
106
107 delegate_create!(
108 create_remote_docker_files(infra, assets_root)
112 );
113
114 delegate_create!(
115 create_rl_docker_files(infra, assets_root)
119 );
120
121 delegate_create!(
122 create_package_json(meta, project_name, assets_root)
125 );
126
127 delegate_create!(
128 create_requirements_txt(meta, assets_root)
131 );
132
133 delegate_create!(
134 create_python_version(meta, assets_root)
138 );
139
140 pub async fn create_rl_training_bundle(&self, path: &Path, assets_root: &Path) -> Result<()> {
146 self.rl_bundle.append_rl_requirements(path, assets_root).await?;
147 self.rl_bundle.create_rl_jobs(path, assets_root).await?;
148 Ok(())
149 }
150
151 pub async fn create_dataset_build_job(&self, path: &Path, assets_root: &Path) -> Result<bool> {
160 self.dataset_bundle.create_dataset_jobs(path, assets_root).await
161 }
162}
163
164impl Default for ProjectTemplateService {
165 fn default() -> Self {
166 Self::new()
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use tempfile::TempDir;
174
175 fn stage_assets_root_with(name: &str, content: &str) -> TempDir {
178 let assets_root = TempDir::new().unwrap();
179 let project_dir = assets_root.path().join("project");
180 std::fs::create_dir_all(&project_dir).unwrap();
181 std::fs::write(project_dir.join(name), content).unwrap();
182 assets_root
183 }
184
185 #[tokio::test]
186 async fn create_readme_replaces_project_name_placeholder() {
187 let temp = TempDir::new().unwrap();
188 let assets_root = stage_assets_root_with("README.md.template", "# {{project_name}}\n");
189 let service = ProjectTemplateService::new();
190
191 service
192 .create_readme(temp.path(), "my-robot", assets_root.path())
193 .await
194 .unwrap();
195
196 let content = tokio::fs::read_to_string(temp.path().join("README.md")).await.unwrap();
197 assert!(content.contains("my-robot"));
198 assert!(!content.contains("{{project_name}}"));
199 }
200
201 #[tokio::test]
202 async fn create_gitignore_writes_file() {
203 let temp = TempDir::new().unwrap();
204 let assets_root = stage_assets_root_with(".gitignore.template", "target/\n");
205 let service = ProjectTemplateService::new();
206
207 service.create_gitignore(temp.path(), assets_root.path()).await.unwrap();
208
209 assert!(temp.path().join(".gitignore").exists());
210 }
211
212 #[tokio::test]
213 async fn create_python_version_writes_a_pin_mecha10_setup_can_read() {
214 let temp = TempDir::new().unwrap();
215 let assets_root = stage_assets_root_with(".python-version.template", "3.11\n");
216 let service = ProjectTemplateService::new();
217
218 service
219 .create_python_version(temp.path(), assets_root.path())
220 .await
221 .unwrap();
222
223 let content = tokio::fs::read_to_string(temp.path().join(".python-version"))
224 .await
225 .unwrap();
226 let mut parts = content.trim().split('.');
227 assert!(parts.next().unwrap().parse::<u32>().is_ok());
228 assert!(parts.next().unwrap().parse::<u32>().is_ok());
229 assert!(parts.next().is_none());
230 }
231
232 #[tokio::test]
233 async fn create_cargo_toml_includes_project_name() {
234 let temp = TempDir::new().unwrap();
235 let assets_root =
236 stage_assets_root_with("Cargo.toml.prod.template", "[package]\nname = \"{{project_name}}\"\n");
237 let service = ProjectTemplateService::new();
238
239 service
240 .create_cargo_toml(temp.path(), "my-robot", assets_root.path())
241 .await
242 .unwrap();
243
244 let content = tokio::fs::read_to_string(temp.path().join("Cargo.toml")).await.unwrap();
245 assert!(content.contains("my-robot"));
246 }
247
248 #[tokio::test]
249 async fn create_main_rs_writes_file() {
250 let temp = TempDir::new().unwrap();
251 let assets_root = stage_assets_root_with("main.rs.template", "fn main() {}\n");
252 let service = ProjectTemplateService::new();
253
254 service
255 .create_main_rs(temp.path(), "my-robot", assets_root.path())
256 .await
257 .unwrap();
258
259 assert!(temp.path().join("src/main.rs").exists());
260 }
261
262 #[tokio::test]
263 async fn create_rl_training_bundle_requires_requirements_txt_created_first() {
264 let temp = TempDir::new().unwrap();
265 let assets_root = TempDir::new().unwrap();
266 let jobs_dir = assets_root.path().join("rl/jobs");
267 std::fs::create_dir_all(&jobs_dir).unwrap();
268 std::fs::write(jobs_dir.join("train.yml.template"), "flow_type: train\n").unwrap();
269 std::fs::write(jobs_dir.join("resume-train.yml.template"), "flow_type: resume-run\n").unwrap();
270 std::fs::write(jobs_dir.join("hypertune.yml.template"), "flow_type: tune\n").unwrap();
271 std::fs::write(jobs_dir.join("train_act.yml.template"), "flow_type: train\n").unwrap();
272 std::fs::write(
273 jobs_dir.join("resume-train-act.yml.template"),
274 "flow_type: resume-run\n",
275 )
276 .unwrap();
277 std::fs::write(
278 assets_root.path().join("rl/requirements-rl.txt.template"),
279 "mecha10-rl-task-runner\n",
280 )
281 .unwrap();
282
283 let service = ProjectTemplateService::new();
284
285 service
286 .create_rl_training_bundle(temp.path(), assets_root.path())
287 .await
288 .unwrap();
289
290 assert!(temp.path().join("configs/rl/jobs/train.yml").exists());
291 assert!(temp.path().join("requirements.txt").exists());
292 assert!(!temp.path().join("kestra").exists());
293 }
294
295 #[tokio::test]
296 async fn create_dataset_build_job_writes_the_starter_job_outside_the_rl_train_catalog() {
297 let temp = TempDir::new().unwrap();
298 let assets_root = TempDir::new().unwrap();
299 let jobs_dir = assets_root.path().join("dataset/jobs");
300 std::fs::create_dir_all(&jobs_dir).unwrap();
301 std::fs::write(
302 jobs_dir.join("build_dataset.yml.template"),
303 "name: build-dataset\nflow_type: build-dataset\n",
304 )
305 .unwrap();
306
307 let service = ProjectTemplateService::new();
308
309 let written = service
310 .create_dataset_build_job(temp.path(), assets_root.path())
311 .await
312 .unwrap();
313
314 assert!(written);
315 assert!(temp.path().join("configs/datasets/jobs/build-dataset.yml").exists());
316 assert!(!temp.path().join("configs/rl/jobs").exists());
317 }
318}