feat: enable templating
This commit is contained in:
parent
dcf459462e
commit
9b6996c261
@ -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);
|
||||||
|
105
crates/forest/src/cli/template.rs
Normal file
105
crates/forest/src/cli/template.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1,3 @@
|
|||||||
something
|
something
|
||||||
|
|
||||||
|
val is mapping: {{ global.someKey.some.key.val is mapping }}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user