From 9be64b74b20aed96f04df894c1e2f8c4a559a782 Mon Sep 17 00:00:00 2001 From: kjuulh Date: Fri, 28 Feb 2025 21:21:34 +0100 Subject: [PATCH] feat: implement template plan --- crates/forest/src/cli/template.rs | 101 ++++++++++++++++++ crates/forest/src/model.rs | 10 ++ examples/plan/forest.kdl | 10 ++ examples/plan/scripts/hello.sh | 5 + examples/plan/scripts/world.sh | 5 + .../plan/templates/something.plan.yaml.jinja2 | 3 + examples/plan/templates/something.yaml.jinja2 | 3 + 7 files changed, 137 insertions(+) create mode 100755 examples/plan/scripts/hello.sh create mode 100755 examples/plan/scripts/world.sh create mode 100644 examples/plan/templates/something.plan.yaml.jinja2 create mode 100644 examples/plan/templates/something.yaml.jinja2 diff --git a/crates/forest/src/cli/template.rs b/crates/forest/src/cli/template.rs index a5caf0c..2f756da 100644 --- a/crates/forest/src/cli/template.rs +++ b/crates/forest/src/cli/template.rs @@ -11,6 +11,107 @@ impl Template { pub async fn execute(self, project_path: &Path, context: &Context) -> anyhow::Result<()> { tracing::info!("templating"); + self.execute_plan(project_path, context).await?; + self.execute_project(project_path, context).await?; + + Ok(()) + } + + async fn execute_plan(&self, project_path: &Path, context: &Context) -> anyhow::Result<()> { + let plan_path = &project_path.join(".forest").join("plan"); + + let Some(Some(template)) = &context.plan.as_ref().map(|p| &p.templates) else { + return Ok(()); + }; + + match template.ty { + TemplateType::Jinja2 => { + for entry in glob::glob(&format!( + "{}/{}", + plan_path.display().to_string().trim_end_matches("/"), + template.path.trim_start_matches("./"), + )) + .map_err(|e| anyhow::anyhow!("failed to read glob pattern: {}", e))? + { + let entry = entry.map_err(|e| anyhow::anyhow!("failed to read path: {}", e))?; + let entry_name = entry.display().to_string(); + + let entry_rel = if entry.is_absolute() { + entry.strip_prefix(plan_path).map(|e| e.to_path_buf()) + } else { + Ok(entry.clone()) + }; + + let rel_file_path = entry_rel + .map(|p| { + if p.file_name() + .map(|f| f.to_string_lossy().ends_with(".jinja2")) + .unwrap_or(false) + { + p.with_file_name( + p.file_stem().expect("to be able to find a filename"), + ) + } else { + p.to_path_buf() + } + }) + .map_err(|e| { + anyhow::anyhow!( + "failed to find relative file: {}, project: {}, file: {}", + e, + plan_path.display(), + entry_name + ) + })?; + + let output_file_path = project_path + .join(".forest/temp") + .join(&template.output) + .join(rel_file_path); + + let contents = tokio::fs::read_to_string(&entry).await.map_err(|e| { + anyhow::anyhow!("failed to read template: {}, err: {}", entry.display(), e) + })?; + + let mut env = minijinja::Environment::new(); + env.add_template(&entry_name, &contents)?; + env.add_global("global", &context.project.global); + + let tmpl = env.get_template(&entry_name)?; + + let output = tmpl + .render(minijinja::context! {}) + .map_err(|e| anyhow::anyhow!("failed to render template: {}", e))?; + + tracing::info!("rendered template: {}", output); + + if let Some(parent) = output_file_path.parent() { + tokio::fs::create_dir_all(parent).await.map_err(|e| { + anyhow::anyhow!( + "failed to create directory (path: {}) for output: {}", + parent.display(), + e + ) + })?; + } + + let mut output_file = tokio::fs::File::create(&output_file_path) + .await + .map_err(|e| { + anyhow::anyhow!( + "failed to create file: {}, error: {}", + output_file_path.display(), + e + ) + })?; + output_file.write_all(output.as_bytes()).await?; + } + } + } + + Ok(()) + } + async fn execute_project(&self, project_path: &Path, context: &Context) -> anyhow::Result<()> { let Some(template) = &context.project.templates else { return Ok(()); }; diff --git a/crates/forest/src/model.rs b/crates/forest/src/model.rs index fc3b4ff..2971e7a 100644 --- a/crates/forest/src/model.rs +++ b/crates/forest/src/model.rs @@ -12,6 +12,8 @@ pub struct Context { #[derive(Debug, Clone, Serialize)] pub struct Plan { pub name: String, + pub templates: Option, + pub scripts: Option, } impl TryFrom for Plan { @@ -35,6 +37,14 @@ impl TryFrom for Plan { }) .cloned() .ok_or(anyhow::anyhow!("a forest kuddle plan must have a name"))?, + templates: plan_children + .get("templates") + .map(|t| t.try_into()) + .transpose()?, + scripts: plan_children + .get("scripts") + .map(|m| m.try_into()) + .transpose()?, }) } } diff --git a/examples/plan/forest.kdl b/examples/plan/forest.kdl index 7c91a55..c87ae35 100644 --- a/examples/plan/forest.kdl +++ b/examples/plan/forest.kdl @@ -1,3 +1,13 @@ plan { name project + + templates type=jinja2 { + path "templates/*.jinja2" + output "output/" + } + + scripts { + world type=shell {} + hello type=shell {} + } } diff --git a/examples/plan/scripts/hello.sh b/examples/plan/scripts/hello.sh new file mode 100755 index 0000000..bdada0b --- /dev/null +++ b/examples/plan/scripts/hello.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env zsh + +set -e + +echo "hello plan" diff --git a/examples/plan/scripts/world.sh b/examples/plan/scripts/world.sh new file mode 100755 index 0000000..66ef67c --- /dev/null +++ b/examples/plan/scripts/world.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env zsh + +set -e + +echo "hello plan world" diff --git a/examples/plan/templates/something.plan.yaml.jinja2 b/examples/plan/templates/something.plan.yaml.jinja2 new file mode 100644 index 0000000..6895d6e --- /dev/null +++ b/examples/plan/templates/something.plan.yaml.jinja2 @@ -0,0 +1,3 @@ +something plan + +val is mapping: {{ global.someKey.some.key.val is mapping }} diff --git a/examples/plan/templates/something.yaml.jinja2 b/examples/plan/templates/something.yaml.jinja2 new file mode 100644 index 0000000..6895d6e --- /dev/null +++ b/examples/plan/templates/something.yaml.jinja2 @@ -0,0 +1,3 @@ +something plan + +val is mapping: {{ global.someKey.some.key.val is mapping }}