diff --git a/crates/forest/src/cli.rs b/crates/forest/src/cli.rs index 0c8ebc7..ce675e2 100644 --- a/crates/forest/src/cli.rs +++ b/crates/forest/src/cli.rs @@ -4,20 +4,20 @@ use clap::{Parser, Subcommand}; use colored_json::ToColoredJson; use kdl::KdlDocument; use rusty_s3::{Bucket, Credentials, S3Action}; -use tokio::io::AsyncWriteExt; use crate::{ - model::{Context, Plan, Project, TemplateType}, + model::{Context, Plan, Project}, plan_reconciler::PlanReconciler, state::SharedState, }; +mod template; + #[derive(Parser)] #[command(author, version, about, long_about = None, subcommand_required = true)] struct Command { #[command(subcommand)] command: Option, - #[arg( env = "FOREST_PROJECT_PATH", long = "project-path", @@ -30,7 +30,7 @@ struct Command { enum Commands { Init {}, - Template {}, + Template(template::Template), Info {}, @@ -101,109 +101,17 @@ pub async fn execute() -> anyhow::Result<()> { println!("{}", output.to_colored_json_auto().unwrap_or(output)); } - Commands::Template {} => { - tracing::info!("templating"); - - let Some(template) = context.project.templates else { - return Ok(()); - }; - - match template.ty { - TemplateType::Jinja2 => { - for entry in glob::glob(&format!( - "{}/{}", - project_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(project_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, - project_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)?; - 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?; - } - } - } + Commands::Template(template) => { + template.execute(project_path, &context).await?; } Commands::Serve { - host, s3_endpoint, s3_bucket, s3_region, s3_user, s3_password, + .. } => { tracing::info!("Starting server"); let creds = Credentials::new(s3_user, s3_password); diff --git a/crates/forest/src/cli/template.rs b/crates/forest/src/cli/template.rs new file mode 100644 index 0000000..a5caf0c --- /dev/null +++ b/crates/forest/src/cli/template.rs @@ -0,0 +1,105 @@ +use std::path::Path; + +use tokio::io::AsyncWriteExt; + +use crate::model::{Context, TemplateType}; + +#[derive(clap::Parser)] +pub struct Template {} + +impl Template { + pub async fn execute(self, project_path: &Path, context: &Context) -> anyhow::Result<()> { + tracing::info!("templating"); + + let Some(template) = &context.project.templates else { + return Ok(()); + }; + + match template.ty { + TemplateType::Jinja2 => { + for entry in glob::glob(&format!( + "{}/{}", + project_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(project_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, + project_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(()) + } +} diff --git a/crates/forest/src/model.rs b/crates/forest/src/model.rs index 3d1eb29..cc9fb0a 100644 --- a/crates/forest/src/model.rs +++ b/crates/forest/src/model.rs @@ -1,6 +1,6 @@ use std::{collections::BTreeMap, fmt::Debug, path::PathBuf}; -use kdl::{KdlDocument, KdlEntry, KdlNode, KdlValue}; +use kdl::{KdlDocument, KdlNode, KdlValue}; use serde::Serialize; #[derive(Debug, Clone, Serialize)] @@ -133,6 +133,12 @@ pub struct Global { items: BTreeMap, } +impl From<&Global> for minijinja::Value { + fn from(value: &Global) -> Self { + Self::from_serialize(&value.items) + } +} + impl TryFrom<&KdlNode> for Global { type Error = anyhow::Error; @@ -197,7 +203,10 @@ impl TryFrom<&KdlNode> for Templates { match val.to_lowercase().as_str() { "jinja2" => templates.ty = TemplateType::Jinja2, e => { - anyhow::bail!("failed to find a template matching the required type: {}, only 'jinja2' is supported", e); + anyhow::bail!( + "failed to find a template matching the required type: {}, only 'jinja2' is supported", + e + ); } } } diff --git a/examples/project/templates/something.yaml.jinja2 b/examples/project/templates/something.yaml.jinja2 index deba01f..8f64ed9 100644 --- a/examples/project/templates/something.yaml.jinja2 +++ b/examples/project/templates/something.yaml.jinja2 @@ -1 +1,3 @@ something + +val is mapping: {{ global.someKey.some.key.val is mapping }}