feat: enable templating

This commit is contained in:
Kasper Juul Hermansen 2025-02-27 17:19:56 +01:00
parent dcf459462e
commit 9b6996c261
Signed by: kjuulh
SSH Key Fingerprint: SHA256:RjXh0p7U6opxnfd3ga/Y9TCo18FYlHFdSpRIV72S/QM
4 changed files with 125 additions and 101 deletions

View File

@ -4,20 +4,20 @@ use clap::{Parser, Subcommand};
use colored_json::ToColoredJson; use colored_json::ToColoredJson;
use kdl::KdlDocument; use kdl::KdlDocument;
use rusty_s3::{Bucket, Credentials, S3Action}; use rusty_s3::{Bucket, Credentials, S3Action};
use tokio::io::AsyncWriteExt;
use crate::{ use crate::{
model::{Context, Plan, Project, TemplateType}, model::{Context, Plan, Project},
plan_reconciler::PlanReconciler, plan_reconciler::PlanReconciler,
state::SharedState, state::SharedState,
}; };
mod template;
#[derive(Parser)] #[derive(Parser)]
#[command(author, version, about, long_about = None, subcommand_required = true)] #[command(author, version, about, long_about = None, subcommand_required = true)]
struct Command { struct Command {
#[command(subcommand)] #[command(subcommand)]
command: Option<Commands>, command: Option<Commands>,
#[arg( #[arg(
env = "FOREST_PROJECT_PATH", env = "FOREST_PROJECT_PATH",
long = "project-path", long = "project-path",
@ -30,7 +30,7 @@ struct Command {
enum Commands { enum Commands {
Init {}, Init {},
Template {}, Template(template::Template),
Info {}, Info {},
@ -101,109 +101,17 @@ pub async fn execute() -> anyhow::Result<()> {
println!("{}", output.to_colored_json_auto().unwrap_or(output)); println!("{}", output.to_colored_json_auto().unwrap_or(output));
} }
Commands::Template {} => { Commands::Template(template) => {
tracing::info!("templating"); template.execute(project_path, &context).await?;
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::Serve { Commands::Serve {
host,
s3_endpoint, s3_endpoint,
s3_bucket, s3_bucket,
s3_region, s3_region,
s3_user, s3_user,
s3_password, s3_password,
..
} => { } => {
tracing::info!("Starting server"); tracing::info!("Starting server");
let creds = Credentials::new(s3_user, s3_password); let creds = Credentials::new(s3_user, s3_password);

View File

@ -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(())
}
}

View File

@ -1,6 +1,6 @@
use std::{collections::BTreeMap, fmt::Debug, path::PathBuf}; use std::{collections::BTreeMap, fmt::Debug, path::PathBuf};
use kdl::{KdlDocument, KdlEntry, KdlNode, KdlValue}; use kdl::{KdlDocument, KdlNode, KdlValue};
use serde::Serialize; use serde::Serialize;
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
@ -133,6 +133,12 @@ pub struct Global {
items: BTreeMap<String, GlobalVariable>, items: BTreeMap<String, GlobalVariable>,
} }
impl From<&Global> for minijinja::Value {
fn from(value: &Global) -> Self {
Self::from_serialize(&value.items)
}
}
impl TryFrom<&KdlNode> for Global { impl TryFrom<&KdlNode> for Global {
type Error = anyhow::Error; type Error = anyhow::Error;
@ -197,7 +203,10 @@ impl TryFrom<&KdlNode> for Templates {
match val.to_lowercase().as_str() { match val.to_lowercase().as_str() {
"jinja2" => templates.ty = TemplateType::Jinja2, "jinja2" => templates.ty = TemplateType::Jinja2,
e => { 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
);
} }
} }
} }

View File

@ -1 +1,3 @@
something something
val is mapping: {{ global.someKey.some.key.val is mapping }}