use crate::templates::{InfraTemplates, MetaTemplates, RlBundleTemplates, RustTemplates};
use anyhow::Result;
use std::path::Path;
macro_rules! delegate_create {
($(#[$meta:meta])* $name:ident($field:ident)) => {
$(#[$meta])*
pub async fn $name(&self, path: &Path) -> Result<()> {
self.$field.$name(path).await
}
};
($(#[$meta:meta])* $name:ident($field:ident, project_name)) => {
$(#[$meta])*
pub async fn $name(&self, path: &Path, project_name: &str) -> Result<()> {
self.$field.$name(path, project_name).await
}
};
($(#[$meta:meta])* $name:ident($field:ident, assets_root)) => {
$(#[$meta])*
pub async fn $name(&self, path: &Path, assets_root: &Path) -> Result<()> {
self.$field.$name(path, assets_root).await
}
};
($(#[$meta:meta])* $name:ident($field:ident, project_name, assets_root)) => {
$(#[$meta])*
pub async fn $name(&self, path: &Path, project_name: &str, assets_root: &Path) -> Result<()> {
self.$field.$name(path, project_name, assets_root).await
}
};
}
pub struct ProjectTemplateService {
rust: RustTemplates,
infra: InfraTemplates,
meta: MetaTemplates,
rl_bundle: RlBundleTemplates,
}
impl ProjectTemplateService {
pub fn new() -> Self {
Self {
rust: RustTemplates::new(),
infra: InfraTemplates::new(),
meta: MetaTemplates::new(),
rl_bundle: RlBundleTemplates::new(),
}
}
delegate_create!(
create_readme(meta, project_name, assets_root)
);
delegate_create!(
create_gitignore(meta, assets_root)
);
delegate_create!(
create_cargo_toml(rust, project_name, assets_root)
);
delegate_create!(
create_main_rs(rust, project_name, assets_root)
);
delegate_create!(
create_build_rs(rust, assets_root)
);
delegate_create!(
create_env_example(infra, project_name, assets_root)
);
pub async fn create_rustfmt_toml(&self, path: &Path, assets_root: &Path) -> Result<()> {
self.rust.create_rustfmt_toml(path, assets_root).await
}
delegate_create!(
create_docker_compose(infra, project_name, assets_root)
);
delegate_create!(
create_remote_docker_files(infra, assets_root)
);
delegate_create!(
create_rl_docker_files(infra, assets_root)
);
delegate_create!(
create_package_json(meta, project_name, assets_root)
);
delegate_create!(
create_requirements_txt(meta, assets_root)
);
delegate_create!(
create_python_version(meta, assets_root)
);
pub async fn create_rl_training_bundle(&self, path: &Path, assets_root: &Path) -> Result<()> {
self.rl_bundle.append_rl_requirements(path, assets_root).await?;
self.rl_bundle.create_rl_jobs(path, assets_root).await?;
Ok(())
}
}
impl Default for ProjectTemplateService {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn stage_assets_root_with(name: &str, content: &str) -> TempDir {
let assets_root = TempDir::new().unwrap();
let project_dir = assets_root.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
std::fs::write(project_dir.join(name), content).unwrap();
assets_root
}
#[tokio::test]
async fn create_readme_replaces_project_name_placeholder() {
let temp = TempDir::new().unwrap();
let assets_root = stage_assets_root_with("README.md.template", "# {{project_name}}\n");
let service = ProjectTemplateService::new();
service
.create_readme(temp.path(), "my-robot", assets_root.path())
.await
.unwrap();
let content = tokio::fs::read_to_string(temp.path().join("README.md")).await.unwrap();
assert!(content.contains("my-robot"));
assert!(!content.contains("{{project_name}}"));
}
#[tokio::test]
async fn create_gitignore_writes_file() {
let temp = TempDir::new().unwrap();
let assets_root = stage_assets_root_with(".gitignore.template", "target/\n");
let service = ProjectTemplateService::new();
service.create_gitignore(temp.path(), assets_root.path()).await.unwrap();
assert!(temp.path().join(".gitignore").exists());
}
#[tokio::test]
async fn create_python_version_writes_a_pin_mecha10_setup_can_read() {
let temp = TempDir::new().unwrap();
let assets_root = stage_assets_root_with(".python-version.template", "3.11\n");
let service = ProjectTemplateService::new();
service
.create_python_version(temp.path(), assets_root.path())
.await
.unwrap();
let content = tokio::fs::read_to_string(temp.path().join(".python-version"))
.await
.unwrap();
let mut parts = content.trim().split('.');
assert!(parts.next().unwrap().parse::<u32>().is_ok());
assert!(parts.next().unwrap().parse::<u32>().is_ok());
assert!(parts.next().is_none());
}
#[tokio::test]
async fn create_cargo_toml_includes_project_name() {
let temp = TempDir::new().unwrap();
let assets_root =
stage_assets_root_with("Cargo.toml.prod.template", "[package]\nname = \"{{project_name}}\"\n");
let service = ProjectTemplateService::new();
service
.create_cargo_toml(temp.path(), "my-robot", assets_root.path())
.await
.unwrap();
let content = tokio::fs::read_to_string(temp.path().join("Cargo.toml")).await.unwrap();
assert!(content.contains("my-robot"));
}
#[tokio::test]
async fn create_main_rs_writes_file() {
let temp = TempDir::new().unwrap();
let assets_root = stage_assets_root_with("main.rs.template", "fn main() {}\n");
let service = ProjectTemplateService::new();
service
.create_main_rs(temp.path(), "my-robot", assets_root.path())
.await
.unwrap();
assert!(temp.path().join("src/main.rs").exists());
}
#[tokio::test]
async fn create_rl_training_bundle_requires_requirements_txt_created_first() {
let temp = TempDir::new().unwrap();
let assets_root = TempDir::new().unwrap();
let jobs_dir = assets_root.path().join("rl/jobs");
std::fs::create_dir_all(&jobs_dir).unwrap();
std::fs::write(jobs_dir.join("train.yml.template"), "flow_type: train\n").unwrap();
std::fs::write(
jobs_dir.join("train-from-checkpoint.yml.template"),
"flow_type: resume-run\n",
)
.unwrap();
std::fs::write(jobs_dir.join("hypertune.yml.template"), "flow_type: tune\n").unwrap();
std::fs::write(
assets_root.path().join("rl/requirements-rl.txt.template"),
"mecha10-rl-task-runner\n",
)
.unwrap();
let service = ProjectTemplateService::new();
service
.create_rl_training_bundle(temp.path(), assets_root.path())
.await
.unwrap();
assert!(temp.path().join("configs/rl/jobs/train.yml").exists());
assert!(temp.path().join("requirements.txt").exists());
assert!(!temp.path().join("kestra").exists());
}
}