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

135
Cargo.lock generated
View File

@@ -177,6 +177,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "eyre"
version = "0.6.8"
@@ -193,6 +199,12 @@ version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
[[package]]
name = "hashbrown"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12"
[[package]]
name = "heck"
version = "0.4.1"
@@ -205,6 +217,16 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
[[package]]
name = "indexmap"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
@@ -352,6 +374,26 @@ version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "rprompt"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ac661565a32b2874200835012773dd9f97e9113a0ab78b69792480a8d9b28e"
dependencies = [
"rtoolbox",
"winapi",
]
[[package]]
name = "rtoolbox"
version = "0.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "034e22c514f5c0cb8a10ff341b9b048b5ceb21591f31c8f44c43b960f9b3524a"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "rustc-demangle"
version = "0.1.23"
@@ -365,9 +407,42 @@ dependencies = [
"clap",
"color-eyre",
"eyre",
"rprompt",
"serde",
"toml",
"tracing",
"tracing-error",
"tracing-subscriber",
"upon",
]
[[package]]
name = "serde"
version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_spanned"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
dependencies = [
"serde",
]
[[package]]
@@ -412,6 +487,40 @@ dependencies = [
"once_cell",
]
[[package]]
name = "toml"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "tracing"
version = "0.1.37"
@@ -490,6 +599,23 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-width"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
[[package]]
name = "upon"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21a9260fe394dfd8ab204a8eab40f88eb9a331bb852147d24fc0aff6b30daa02"
dependencies = [
"serde",
"unicode-ident",
"unicode-width",
]
[[package]]
name = "utf8parse"
version = "0.2.1"
@@ -589,3 +715,12 @@ name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "winnow"
version = "0.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711d82167854aff2018dfd193aa0fef5370f456732f0d5a0c59b0f1b4b907"
dependencies = [
"memchr",
]

View File

@@ -9,6 +9,10 @@ edition = "2021"
clap = { version = "4.4.6", features = ["derive"] }
color-eyre = "0.6.2"
eyre = "0.6.8"
rprompt = "2.0.2"
serde = { version = "1.0.188", features = ["derive"] }
toml = "0.8.2"
tracing = "0.1.37"
tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
upon = "0.7.1"

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# Sablono
Generates directories based on templates.
Templates are defined by their manifest, which is a TOML file.

1
example-tmpl/abc Normal file
View File

@@ -0,0 +1 @@
abc 123 {{ name }}

View File

@@ -0,0 +1,9 @@
[parameters]
[files."abc"]
[files."qwe"]
src = "abc"
transforms = []

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
}
}