feat: create support for templates
This commit is contained in:
parent
e19bf14a43
commit
61cd888620
17
Cargo.lock
generated
17
Cargo.lock
generated
@ -236,7 +236,9 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
|
"glob",
|
||||||
"kdl",
|
"kdl",
|
||||||
|
"minijinja",
|
||||||
"rusty-s3",
|
"rusty-s3",
|
||||||
"serde",
|
"serde",
|
||||||
"tokio",
|
"tokio",
|
||||||
@ -284,6 +286,12 @@ version = "0.31.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glob"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@ -535,6 +543,15 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minijinja"
|
||||||
|
version = "2.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cff7b8df5e85e30b87c2b0b3f58ba3a87b68e133738bf512a7713769326dbca9"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
|
@ -17,3 +17,5 @@ rusty-s3 = "0.7.0"
|
|||||||
url = "2.5.4"
|
url = "2.5.4"
|
||||||
kdl = "6.3.3"
|
kdl = "6.3.3"
|
||||||
walkdir = "2.5.0"
|
walkdir = "2.5.0"
|
||||||
|
minijinja = "2.7.0"
|
||||||
|
glob = "0.3.2"
|
||||||
|
@ -3,9 +3,10 @@ use std::{net::SocketAddr, path::PathBuf};
|
|||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
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},
|
model::{Context, Plan, Project, TemplateType},
|
||||||
plan_reconciler::PlanReconciler,
|
plan_reconciler::PlanReconciler,
|
||||||
state::SharedState,
|
state::SharedState,
|
||||||
};
|
};
|
||||||
@ -15,18 +16,20 @@ use crate::{
|
|||||||
struct Command {
|
struct Command {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Option<Commands>,
|
command: Option<Commands>,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
env = "FOREST_PROJECT_PATH",
|
||||||
|
long = "project-path",
|
||||||
|
default_value = "."
|
||||||
|
)]
|
||||||
|
project_path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum Commands {
|
enum Commands {
|
||||||
Init {
|
Init {},
|
||||||
#[arg(
|
|
||||||
env = "FOREST_PROJECT_PATH",
|
Template {},
|
||||||
long = "project-path",
|
|
||||||
default_value = "."
|
|
||||||
)]
|
|
||||||
project_path: PathBuf,
|
|
||||||
},
|
|
||||||
|
|
||||||
Serve {
|
Serve {
|
||||||
#[arg(env = "FOREST_HOST", long, default_value = "127.0.0.1:3000")]
|
#[arg(env = "FOREST_HOST", long, default_value = "127.0.0.1:3000")]
|
||||||
@ -52,42 +55,138 @@ enum Commands {
|
|||||||
pub async fn execute() -> anyhow::Result<()> {
|
pub async fn execute() -> anyhow::Result<()> {
|
||||||
let cli = Command::parse();
|
let cli = Command::parse();
|
||||||
|
|
||||||
|
let project_path = &cli.project_path.canonicalize()?;
|
||||||
|
let project_file_path = project_path.join("forest.kdl");
|
||||||
|
if !project_file_path.exists() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"no 'forest.kdl' file was found at: {}",
|
||||||
|
project_file_path.display().to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let project_file = tokio::fs::read_to_string(&project_file_path).await?;
|
||||||
|
let project_doc: KdlDocument = project_file.parse()?;
|
||||||
|
|
||||||
|
let project: Project = project_doc.try_into()?;
|
||||||
|
tracing::trace!("found a project name: {}", project.name);
|
||||||
|
|
||||||
|
let plan = if let Some(plan_file_path) = PlanReconciler::new()
|
||||||
|
.reconcile(&project, project_path)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
let plan_file = tokio::fs::read_to_string(&plan_file_path).await?;
|
||||||
|
let plan_doc: KdlDocument = plan_file.parse()?;
|
||||||
|
|
||||||
|
let plan: Plan = plan_doc.try_into()?;
|
||||||
|
tracing::trace!("found a plan name: {}", project.name);
|
||||||
|
|
||||||
|
Some(plan)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let context = Context { project, plan };
|
||||||
|
|
||||||
match cli.command.unwrap() {
|
match cli.command.unwrap() {
|
||||||
Commands::Init { project_path } => {
|
Commands::Init {} => {
|
||||||
tracing::info!("initializing project");
|
tracing::info!("initializing project");
|
||||||
|
tracing::trace!("found context: {:?}", context);
|
||||||
|
}
|
||||||
|
|
||||||
let project_file_path = project_path.join("forest.kdl");
|
Commands::Template {} => {
|
||||||
if !project_file_path.exists() {
|
tracing::info!("templating");
|
||||||
anyhow::bail!(
|
|
||||||
"no 'forest.kdl' file was found at: {}",
|
|
||||||
project_file_path.display().to_string()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let project_file = tokio::fs::read_to_string(&project_file_path).await?;
|
let Some(template) = context.project.templates else {
|
||||||
let project_doc: KdlDocument = project_file.parse()?;
|
return Ok(());
|
||||||
|
|
||||||
let project: Project = project_doc.try_into()?;
|
|
||||||
tracing::trace!("found a project name: {}", project.name);
|
|
||||||
|
|
||||||
let plan = if let Some(plan_file_path) = PlanReconciler::new()
|
|
||||||
.reconcile(&project, &project_path)
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
let plan_file = tokio::fs::read_to_string(&plan_file_path).await?;
|
|
||||||
let plan_doc: KdlDocument = plan_file.parse()?;
|
|
||||||
|
|
||||||
let plan: Plan = plan_doc.try_into()?;
|
|
||||||
tracing::trace!("found a plan name: {}", project.name);
|
|
||||||
|
|
||||||
Some(plan)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let context = Context { project, plan };
|
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();
|
||||||
|
|
||||||
tracing::info!("context: {:+?}", context);
|
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 {
|
||||||
|
@ -153,12 +153,79 @@ impl TryFrom<&KdlNode> for Global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub enum TemplateType {
|
||||||
|
#[default]
|
||||||
|
Jinja2,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Templates {
|
||||||
|
pub ty: TemplateType,
|
||||||
|
pub path: String,
|
||||||
|
pub output: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Templates {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
ty: TemplateType::default(),
|
||||||
|
path: "./templates/*.jinja2".into(),
|
||||||
|
output: "output/".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&KdlNode> for Templates {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(value: &KdlNode) -> Result<Self, Self::Error> {
|
||||||
|
let mut templates = Templates::default();
|
||||||
|
|
||||||
|
for entry in value.entries() {
|
||||||
|
let Some(name) = entry.name() else { continue };
|
||||||
|
match name.value() {
|
||||||
|
"type" => {
|
||||||
|
let Some(val) = entry.value().as_string() else {
|
||||||
|
anyhow::bail!("type is not a valid string")
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"path" => {
|
||||||
|
let Some(val) = entry.value().as_string() else {
|
||||||
|
anyhow::bail!("failed to parse path as a valid string")
|
||||||
|
};
|
||||||
|
|
||||||
|
templates.path = val.to_string();
|
||||||
|
}
|
||||||
|
"output" => {
|
||||||
|
let Some(val) = entry.value().as_string() else {
|
||||||
|
anyhow::bail!("failed to parse val as a valid string")
|
||||||
|
};
|
||||||
|
|
||||||
|
templates.output = PathBuf::from(val);
|
||||||
|
}
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(templates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Project {
|
pub struct Project {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub plan: Option<ProjectPlan>,
|
pub plan: Option<ProjectPlan>,
|
||||||
pub global: Global,
|
pub global: Global,
|
||||||
|
pub templates: Option<Templates>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<KdlDocument> for Project {
|
impl TryFrom<KdlDocument> for Project {
|
||||||
@ -203,6 +270,10 @@ impl TryFrom<KdlDocument> for Project {
|
|||||||
}),
|
}),
|
||||||
plan: project_plan,
|
plan: project_plan,
|
||||||
global: global.unwrap_or_default(),
|
global: global.unwrap_or_default(),
|
||||||
|
templates: project_children
|
||||||
|
.get("templates")
|
||||||
|
.map(|t| t.try_into())
|
||||||
|
.transpose()?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,4 +20,10 @@ project {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templates type=jinja2 {
|
||||||
|
path "templates/*.jinja2"
|
||||||
|
output "output/"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
1
examples/project/templates/something.yaml.jinja2
Normal file
1
examples/project/templates/something.yaml.jinja2
Normal file
@ -0,0 +1 @@
|
|||||||
|
something
|
Loading…
x
Reference in New Issue
Block a user