Functional templating

This commit is contained in:
bluepython508
2023-10-13 13:37:30 +01:00
parent 953dc55b32
commit 342a364aed
8 changed files with 382 additions and 5 deletions

84
src/context.rs Normal file
View File

@@ -0,0 +1,84 @@
use std::path::{Path, PathBuf};
use crate::manifest::{FileDefinition, Manifest, Source, Transform, Params};
use eyre::Result;
use tracing::instrument;
#[derive(Debug)]
pub struct Context {
manifest: Manifest,
root: Source,
params: Params,
}
impl Context {
#[instrument(ret)]
pub fn load(source: Source, params: Params) -> Result<Self> {
let params = Params::load_user()?.merge(params);
Ok(Context {
manifest: load_manifest(&source)?,
root: source.parent().ok_or_else(|| eyre::eyre!("Expect manifest to have a parent"))?,
params,
})
}
#[instrument]
pub fn generate(self, dest: PathBuf) -> Result<()> {
std::fs::create_dir(&dest)?;
for (path, def) in &self.manifest.files {
self.generate_file(&dest, path, def)?;
}
Ok(())
}
#[instrument]
fn generate_file(&self, dest: &Path, path: &PathBuf, def: &FileDefinition) -> Result<()> {
std::fs::create_dir_all(dest.join(path).parent().expect("`dest` should be an absolute path with a parent"))?;
let mut input = self
.root
.join(
&def.src
.clone()
.unwrap_or_else(|| Source::Path(path.to_owned())),
)
.load()?;
for transform in &def.transforms {
input = self.run_transform(transform, input)?;
}
std::fs::write(dest.join(path), input)?;
Ok(())
}
#[instrument(skip(input))]
fn run_transform(&self, transform: &Transform, input: Vec<u8>) -> Result<Vec<u8>> {
match transform.r#type.as_str() {
"template" => run_template(&transform.args, &self.params, input),
_ => todo!(),
}
}
}
#[instrument(ret)]
fn load_manifest(src: &Source) -> Result<Manifest> {
Ok(toml::from_str(std::str::from_utf8(&src.load()?)?)?)
}
#[instrument(skip(input))]
fn run_template(
args: &Params,
params: &Params,
input: Vec<u8>,
) -> Result<Vec<u8>> {
let engine = upon::Engine::new();
let context = args.clone().merge(params.clone());
Ok(engine
.compile(std::str::from_utf8(&input)?)?
.render(context)
.to_string()?
.into_bytes())
}

View File

@@ -1,15 +1,34 @@
use std::io;
use std::{
io,
path::PathBuf,
};
use clap::Parser;
use eyre::Result;
use eyre::{ensure, Result};
use tracing::instrument;
use tracing_subscriber::{prelude::*, fmt::{self, format::FmtSpan}, EnvFilter};
use tracing_subscriber::{
fmt::{self, format::FmtSpan},
prelude::*,
EnvFilter,
};
use crate::manifest::Params;
mod context;
mod manifest;
fn split_equals(s: &str) -> Result<(String, String)> {
s.split_once('=').ok_or_else(|| eyre::eyre!("Expected =")).map(|(k, v)| (k.to_owned(), v.to_owned()))
}
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
template: manifest::Source,
destination: PathBuf,
#[arg(short, long, value_parser = split_equals)]
params: Vec<(String, String)>
}
fn main() -> Result<()> {
@@ -30,6 +49,23 @@ fn main() -> Result<()> {
}
#[instrument]
fn run(args: Args) -> Result<()> {
fn run(
Args {
template,
destination,
params
}: Args,
) -> Result<()> {
ensure!(
!destination.exists() || (destination.is_dir() && destination.read_dir()?.next().is_none()),
"Expected {} to be empty or not exist",
destination.display()
);
// let destination = destination.canonicalize()?;
let params = Params(params.into_iter().collect());
context::Context::load(template, params)?.generate(destination)?;
Ok(())
}

105
src/manifest.rs Normal file
View File

@@ -0,0 +1,105 @@
use eyre::Result;
use serde::{Deserialize, Serialize};
use tracing::instrument;
use std::{collections::HashMap, convert::Infallible, path::PathBuf, str::FromStr};
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Manifest {
pub parameters: HashMap<String, ParameterDefinition>,
pub files: HashMap<PathBuf, FileDefinition>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct ParameterDefinition {
pub description: String,
pub default: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct FileDefinition {
pub src: Option<Source>,
#[serde(default = "Transform::defaults")]
pub transforms: Vec<Transform>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Source {
Path(PathBuf),
}
impl Source {
#[instrument(ret)]
pub fn join(&self, other: &Source) -> Self {
match other {
Self::Path(relative) if relative.is_relative() => match self {
Self::Path(root) => Self::Path(root.join(relative)),
},
other => other.clone()
}
}
#[instrument]
pub fn parent(&self) -> Option<Self> {
match self {
Source::Path(path) => Some(Self::Path(path.parent()?.to_owned())),
}
}
#[instrument]
pub fn load(&self) -> Result<Vec<u8>> {
match self {
Self::Path(path) => Ok(std::fs::read(path)?)
}
}
}
impl<'a> Deserialize<'a> for Source {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'a>,
{
Ok(String::deserialize(deserializer)?
.parse()
.unwrap_or_else(|e| match e {}))
}
}
impl FromStr for Source {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Infallible> {
Ok(Self::Path(s.to_owned().into()))
}
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Transform {
pub r#type: String,
#[serde(flatten)]
pub args: Params,
}
impl Transform {
pub fn defaults() -> Vec<Self> {
vec![Self {
r#type: "template".to_owned(),
args: Default::default(),
}]
}
}
#[derive(Debug, Serialize, Deserialize, Default, Clone, PartialEq)]
pub struct Params(pub HashMap<String, String>);
impl Params {
pub fn load_user() -> Result<Self> {
Ok(Self(toml::from_str(
&std::fs::read_to_string("~/.config/sablono/defaults.toml").unwrap_or_default(),
)?))
}
pub fn merge(mut self, other: Self) -> Self {
self.0.extend(other.0);
self
}
}