Functional templating
This commit is contained in:
84
src/context.rs
Normal file
84
src/context.rs
Normal 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())
|
||||
}
|
||||
46
src/main.rs
46
src/main.rs
@@ -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
105
src/manifest.rs
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user