From 342a364aed2e156519aca69b57589d62b0f80ccb Mon Sep 17 00:00:00 2001 From: bluepython508 Date: Fri, 13 Oct 2023 13:37:30 +0100 Subject: [PATCH] Functional templating --- Cargo.lock | 135 +++++++++++++++++++++++++++++++++++++ Cargo.toml | 4 ++ README.md | 3 + example-tmpl/abc | 1 + example-tmpl/manifest.toml | 9 +++ src/context.rs | 84 +++++++++++++++++++++++ src/main.rs | 46 +++++++++++-- src/manifest.rs | 105 +++++++++++++++++++++++++++++ 8 files changed, 382 insertions(+), 5 deletions(-) create mode 100644 README.md create mode 100644 example-tmpl/abc create mode 100644 example-tmpl/manifest.toml create mode 100644 src/context.rs create mode 100644 src/manifest.rs diff --git a/Cargo.lock b/Cargo.lock index cd97d85..7e95946 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", +] diff --git a/Cargo.toml b/Cargo.toml index e7a4d2b..51ec0e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md new file mode 100644 index 0000000..b1ab2c8 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Sablono +Generates directories based on templates. +Templates are defined by their manifest, which is a TOML file. diff --git a/example-tmpl/abc b/example-tmpl/abc new file mode 100644 index 0000000..694d149 --- /dev/null +++ b/example-tmpl/abc @@ -0,0 +1 @@ +abc 123 {{ name }} diff --git a/example-tmpl/manifest.toml b/example-tmpl/manifest.toml new file mode 100644 index 0000000..ce189cc --- /dev/null +++ b/example-tmpl/manifest.toml @@ -0,0 +1,9 @@ +[parameters] + +[files."abc"] + +[files."qwe"] +src = "abc" +transforms = [] + + diff --git a/src/context.rs b/src/context.rs new file mode 100644 index 0000000..013e333 --- /dev/null +++ b/src/context.rs @@ -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 { + 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) -> Result> { + match transform.r#type.as_str() { + "template" => run_template(&transform.args, &self.params, input), + _ => todo!(), + } + } +} + +#[instrument(ret)] +fn load_manifest(src: &Source) -> Result { + Ok(toml::from_str(std::str::from_utf8(&src.load()?)?)?) +} + + +#[instrument(skip(input))] +fn run_template( + args: &Params, + params: &Params, + input: Vec, +) -> Result> { + 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()) +} diff --git a/src/main.rs b/src/main.rs index 85d8493..1c21e24 100644 --- a/src/main.rs +++ b/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(()) } + diff --git a/src/manifest.rs b/src/manifest.rs new file mode 100644 index 0000000..9f22829 --- /dev/null +++ b/src/manifest.rs @@ -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, + pub files: HashMap, +} + +#[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, + #[serde(default = "Transform::defaults")] + pub transforms: Vec, +} + +#[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 { + match self { + Source::Path(path) => Some(Self::Path(path.parent()?.to_owned())), + } + } + + #[instrument] + pub fn load(&self) -> Result> { + match self { + Self::Path(path) => Ok(std::fs::read(path)?) + } + } +} + +impl<'a> Deserialize<'a> for Source { + fn deserialize(deserializer: D) -> std::result::Result + 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 { + 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 { + vec![Self { + r#type: "template".to_owned(), + args: Default::default(), + }] + } +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone, PartialEq)] +pub struct Params(pub HashMap); + +impl Params { + pub fn load_user() -> Result { + 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 + } +}