diff --git a/CHANGELOG.md b/CHANGELOG.md index a9cc3e8..14f3fd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.8.0] - 2022-09-16 + +### Added + +- Template helpers `#windows`, `#linx` and `#darwin` which work like `if`s for the respective os +- `eval` template helper which evaluates the given string on the shell + ## [0.7.1] - 2022-09-12 ### Fixed @@ -152,7 +159,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Dotfile linking - Error handling -[Unreleased]: https://github.com/volllly/rotz/compare/v0.7.1...HEAD +[Unreleased]: https://github.com/volllly/rotz/compare/v0.8.0...HEAD +[0.8.0]: https://github.com/volllly/rotz/releases/tag/v0.8.0 [0.7.1]: https://github.com/volllly/rotz/releases/tag/v0.7.1 [0.7.0]: https://github.com/volllly/rotz/releases/tag/v0.7.0 [0.6.1]: https://github.com/volllly/rotz/releases/tag/v0.6.1 diff --git a/Cargo.lock b/Cargo.lock index 32c4884..a44d192 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -148,9 +148,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.4.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" +checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" [[package]] name = "bytes" @@ -354,9 +354,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" dependencies = [ "block-buffer", "crypto-common", @@ -753,9 +753,9 @@ checksum = "72167d68f5fce3b8655487b8038691a3c9984ee769590f93f2a631f4ad64e4f5" [[package]] name = "js-sys" -version = "0.3.55" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" dependencies = [ "wasm-bindgen", ] @@ -1334,7 +1334,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -1344,7 +1344,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -1364,9 +1364,9 @@ checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] @@ -1434,7 +1434,7 @@ dependencies = [ [[package]] name = "rotz" -version = "0.7.1" +version = "0.8.0" dependencies = [ "baker", "clap", @@ -1570,9 +1570,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f6841e709003d68bb2deee8c343572bf446003ec20a583e76f7b15cebf3711" +checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" [[package]] name = "serde" @@ -1632,9 +1632,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.4" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "006769ba83e921b3085caa8334186b00cf92b4cb1a6cf4632fbccc8eff5c7549" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if", "cpufeatures", @@ -1886,9 +1886,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" +checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" [[package]] name = "unicode-linebreak" @@ -1910,21 +1910,21 @@ dependencies = [ [[package]] name = "unicode-width" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] name = "unicode-xid" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "unsafe-libyaml" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "931179334a56395bcf64ba5e0ff56781381c1a5832178280c7d7f91d1679aeb0" +checksum = "c1e5fa573d8ac5f1a856f8d7be41d390ee973daf97c806b2c1a465e4e1406e68" [[package]] name = "url" @@ -1998,9 +1998,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2008,13 +2008,13 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" dependencies = [ "bumpalo", - "lazy_static", "log", + "once_cell", "proc-macro2", "quote", "syn", @@ -2023,9 +2023,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2033,9 +2033,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", "quote", @@ -2046,9 +2046,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" [[package]] name = "wax" @@ -2071,9 +2071,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.55" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 51c7fae..b491852 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rotz" -version = "0.7.1" +version = "0.8.0" edition = "2021" authors = ["Paul Volavsek "] license = "MIT" diff --git a/docs/docs/configuration/templating.md b/docs/docs/configuration/templating.md index 967f47e..e7cb51b 100644 --- a/docs/docs/configuration/templating.md +++ b/docs/docs/configuration/templating.md @@ -18,7 +18,7 @@ This allows for e.g. access to environment variables. | `whoami` | A map of information about the environment (see [whoami](#whoami)). Provided by the [whoami](https://github.com/dirs-dev/directories-rs#features) crate. | `some.file: /home/{{ whoami.username }}/some.file` | | `directories` | A map of directories (see [directories](#directories)). Provided by the [directories](https://github.com/ardaku/whoami#features) crate | `some.file: {{ directories.home }}/some.file` | -## `whoami` +### `whoami` | Variable | Description | | ------------- | ----------------------------------------- | @@ -31,7 +31,7 @@ This allows for e.g. access to environment variables. | `realname` | The users full name | | `username` | The current users username | -## `directories` +### `directories` | Group | Variable | | ------ | ------------ | @@ -50,4 +50,22 @@ This allows for e.g. access to environment variables. | `user` | `picture` | | `user` | `public` | | `user` | `template` | -| `user` | `video` | \ No newline at end of file +| `user` | `video` | + +## Helpers + +Rotz comes with helpers provided by the [handlebars_misc_helpers](https://github.com/davidb/handlebars_misc_helpers) crate. + +Theres also a number of inbuilt helpers provided + +### `#windows`, `#linx` and `#darwin` + +These helpers are shorthands for checking the curent os. + +Instea of `{{ #if (eq os "windows") }}{{ else }}{{ /if }}` they can be used like this `{{ #windows }}{{ else }}{{ /windows }}`. + +### `eval` + +The eval helper can be used to evalate a string on the shell configured by [`shell_command`](config.yaml.mdx#shell_command). + +The helper can be used like this `{{ eval "some --shell command" }}` \ No newline at end of file diff --git a/src/commands/install.rs b/src/commands/install.rs index 47c8350..300213f 100644 --- a/src/commands/install.rs +++ b/src/commands/install.rs @@ -8,7 +8,7 @@ use velcro::hash_map; use wax::{Glob, Pattern}; use super::Command; -use crate::{config::Config, dot::Installs, helpers, templating::HANDLEBARS}; +use crate::{config::Config, dot::Installs, helpers, templating}; #[derive(thiserror::Error, Diagnostic, Debug)] enum Error { @@ -45,13 +45,14 @@ enum Error { ParseGlob(String, #[source] wax::BuildError<'static>), } -pub struct Install { +pub(crate) struct Install<'a> { config: Config, + engine: templating::Engine<'a>, } -impl Install { - pub const fn new(config: crate::config::Config) -> Self { - Self { config } +impl<'b> Install<'b> { + pub const fn new(config: crate::config::Config, engine: templating::Engine<'b>) -> Self { + Self { config, engine } } fn install<'a>( @@ -109,7 +110,8 @@ impl Install { let inner_cmd = installs.cmd.clone(); let cmd = if let Some(shell_command) = self.config.shell_command.as_ref() { - HANDLEBARS + self + .engine .render_template(shell_command, &hash_map! { "cmd": &inner_cmd }) .map_err(|err| Error::RenderingTemplate(entry.0.clone(), err))? } else { @@ -151,12 +153,12 @@ impl Install { type InstallsDots = (Option, Option>); -impl Command for Install { +impl Command for Install<'_> { type Args = (crate::cli::Globals, crate::cli::Install); type Result = Result<()>; fn execute(&self, (globals, install_command): Self::Args) -> Self::Result { - let dots = crate::dot::read_dots(&self.config.dotfiles, &["/**".to_owned()], &self.config)? + let dots = crate::dot::read_dots(&self.config.dotfiles, &["/**".to_owned()], &self.config, &self.engine)? .into_iter() .filter(|d| d.1.installs.is_some() || d.1.depends.is_some()) .map(|d| (d.0, (d.1.installs, d.1.depends))) diff --git a/src/commands/link.rs b/src/commands/link.rs index 42e3b28..ae5a57f 100644 --- a/src/commands/link.rs +++ b/src/commands/link.rs @@ -7,9 +7,10 @@ use crossterm::style::{Attribute, Stylize}; use miette::{Diagnostic, Report, Result}; use tap::Pipe; +use super::Command; use crate::{ config::{Config, LinkType}, - USER_DIRS, + templating, USER_DIRS, }; #[derive(thiserror::Error, Diagnostic, Debug)] @@ -24,22 +25,23 @@ enum Error { AlreadyExists(PathBuf), } -pub struct Link { +pub(crate) struct Link<'a> { config: Config, + engine: templating::Engine<'a>, } -impl Link { - pub const fn new(config: crate::config::Config) -> Self { - Self { config } +impl<'a> Link<'a> { + pub const fn new(config: crate::config::Config, engine: templating::Engine<'a>) -> Self { + Self { config, engine } } } -impl super::Command for Link { +impl<'a> Command for Link<'a> { type Args = (crate::cli::Globals, crate::cli::Link); type Result = Result<()>; fn execute(&self, (globals, link_command): Self::Args) -> Self::Result { - let links = crate::dot::read_dots(&self.config.dotfiles, &link_command.dots, &self.config)? + let links = crate::dot::read_dots(&self.config.dotfiles, &link_command.dots, &self.config, &self.engine)? .into_iter() .filter_map(|d| d.1.links.map(|l| (d.0, l))); diff --git a/src/commands/mod.rs b/src/commands/mod.rs index b959a1d..b99ee50 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -2,10 +2,10 @@ pub mod clone; pub use clone::Clone; pub mod install; -pub use install::Install; +pub(crate) use install::Install; pub mod link; -pub use link::Link; +pub(crate) use link::Link; pub mod sync; pub use sync::Sync; diff --git a/src/dot/mod.rs b/src/dot/mod.rs index 05ca83b..32ef704 100644 --- a/src/dot/mod.rs +++ b/src/dot/mod.rs @@ -390,7 +390,7 @@ pub enum Error { MultipleErrors(#[from] helpers::MultipleErrors), } -pub fn read_dots(dotfiles_path: &Path, dots: &[String], config: &Config) -> miette::Result> { +pub(crate) fn read_dots(dotfiles_path: &Path, dots: &[String], config: &Config, engine: &templating::Engine<'_>) -> miette::Result> { let defaults = get_defaults(dotfiles_path)?; let dots = helpers::glob_from_vec(dots, &format!("/dot.{FILE_EXTENSIONS_GLOB}"))?; @@ -428,7 +428,7 @@ pub fn read_dots(dotfiles_path: &Path, dots: &[String], config: &Config) -> miet let dots = dotfiles.filter_map(|f| match f { Ok((name, Ok((text, format)))) => { let parameters = Parameters { config, name: &name }; - let text = match templating::render(&text, ¶meters) { + let text = match engine.render(&text, ¶meters) { Ok(text) => text, Err(err) => { return Error::RenderDot(NamedSource::new(format!("{name}/dot.{format}"), text.clone()), (0, text.len()).into(), err) @@ -438,7 +438,7 @@ pub fn read_dots(dotfiles_path: &Path, dots: &[String], config: &Config) -> miet }; let defaults = if let Some((defaults, format)) = defaults.as_ref() { - match templating::render(defaults, ¶meters) { + match engine.render(defaults, ¶meters) { Ok(rendered) => match repr::Dot::parse(&rendered, *format) { Ok(parsed) => Into::::into(parsed).into(), Err(err) => return Error::ParseDot(NamedSource::new(defaults, defaults.to_string()), (0, defaults.len()).into(), err).pipe(Err).into(), diff --git a/src/dot/test/data/file_formats/dots.toml b/src/dot/test/data/file_formats/defaults.toml similarity index 100% rename from src/dot/test/data/file_formats/dots.toml rename to src/dot/test/data/file_formats/defaults.toml diff --git a/src/dot/test/mod.rs b/src/dot/test/mod.rs index 88f15db..6f31166 100644 --- a/src/dot/test/mod.rs +++ b/src/dot/test/mod.rs @@ -4,7 +4,7 @@ use speculoos::prelude::*; use tap::Tap; use super::read_dots; -use crate::helpers::Select; +use crate::{helpers::Select, templating::test::get_handlebars}; mod data; @@ -14,6 +14,7 @@ fn read_all_dots() { Path::new(file!()).parent().unwrap().join("data/directory_structure").as_path(), &["/**".to_owned()], &Default::default(), + &get_handlebars(), ) .unwrap(); @@ -49,6 +50,7 @@ fn read_sub_dots() { Path::new(file!()).parent().unwrap().join("data/directory_structure").as_path(), &["/test03/*".to_owned()], &Default::default(), + &get_handlebars(), ) .unwrap(); @@ -60,7 +62,13 @@ fn read_sub_dots() { #[test] fn read_non_sub_dots() { - let dots = read_dots(Path::new(file!()).parent().unwrap().join("data/directory_structure").as_path(), &["/*".to_owned()], &Default::default()).unwrap(); + let dots = read_dots( + Path::new(file!()).parent().unwrap().join("data/directory_structure").as_path(), + &["/*".to_owned()], + &Default::default(), + &get_handlebars(), + ) + .unwrap(); assert_that!(dots).has_length(2); assert_that!(dots).mapped_contains(|d| &d.0, &"/test01"); @@ -69,7 +77,13 @@ fn read_non_sub_dots() { #[test] fn read_all_file_formats() { - let dots = read_dots(Path::new(file!()).parent().unwrap().join("data/file_formats").as_path(), &["/**".to_owned()], &Default::default()).unwrap(); + let dots = read_dots( + Path::new(file!()).parent().unwrap().join("data/file_formats").as_path(), + &["/**".to_owned()], + &Default::default(), + &get_handlebars(), + ) + .unwrap(); assert_that!(dots).has_length(3); assert_that!(dots).mapped_contains(|d| &d.0, &"/test01"); diff --git a/src/helpers.rs b/src/helpers.rs index b293ad2..255e667 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -76,9 +76,9 @@ pub enum RunError { Write(#[from] io::Error), } -pub fn run_command(cmd: &str, args: &[impl AsRef], silent: bool, dry_run: bool) -> Result<(), RunError> { +pub fn run_command(cmd: &str, args: &[impl AsRef], silent: bool, dry_run: bool) -> Result { if dry_run { - return ().pipe(Ok); + return "".to_owned().pipe(Ok); } let output = process::Command::new(cmd).args(args).stdin(process::Stdio::null()).output().map_err(RunError::Spawn)?; @@ -96,7 +96,7 @@ pub fn run_command(cmd: &str, args: &[impl AsRef], silent: bool, dry_run: RunError::Execute(output.status.code()).pipe(Err)?; }; - ().pipe(Ok) + String::from_utf8_lossy(&output.stdout).to_string().pipe(Ok) } #[derive(thiserror::Error, Diagnostic, Debug)] diff --git a/src/main.rs b/src/main.rs index f6af270..fff9777 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ #![allow(clippy::multiple_crate_versions)] #![allow(clippy::use_self)] #![allow(clippy::default_trait_access)] +#![allow(clippy::redundant_pub_crate)] #![warn(clippy::filetype_is_file)] #![warn(clippy::string_to_string)] #![warn(clippy::unneeded_field_pattern)] @@ -148,10 +149,12 @@ fn main() -> Result<(), miette::Report> { let config = read_config(&cli)?; + let engine = templating::Engine::new(&config, &cli); + match cli.command.clone() { - cli::Command::Link { link } => commands::Link::new(config).execute((cli.bake(), link.bake())), + cli::Command::Link { link } => commands::Link::new(config, engine).execute((cli.bake(), link.bake())), cli::Command::Clone { repo } => commands::Clone::new(config).execute((cli, repo)), - cli::Command::Install { install } => commands::Install::new(config).execute((cli.bake(), install.bake())), + cli::Command::Install { install } => commands::Install::new(config, engine).execute((cli.bake(), install.bake())), cli::Command::Sync { sync } => commands::Sync::new(config).execute((cli.bake(), sync.bake())), cli::Command::Init { repo } => commands::Init::new(config).execute((cli, repo)), } diff --git a/src/templating/mod.rs b/src/templating/mod.rs index a26650b..6055de3 100644 --- a/src/templating/mod.rs +++ b/src/templating/mod.rs @@ -1,19 +1,20 @@ use std::{collections::HashMap, path::PathBuf}; use directories::BaseDirs; -use handlebars::Handlebars; +use handlebars::{Context, Handlebars, Helper, HelperDef, HelperResult, Output, RenderContext, RenderError, Renderable, ScopedJson}; use itertools::Itertools; use miette::Diagnostic; use once_cell::sync::Lazy; use serde::Serialize; +use tap::{Conv, Pipe}; +use velcro::hash_map; -use crate::{config::Config, helpers, USER_DIRS}; - -pub static HANDLEBARS: Lazy = Lazy::new(|| { - let mut hb = handlebars_misc_helpers::new_hbs(); - hb.set_strict_mode(false); - hb -}); +use crate::{ + cli::Cli, + config::Config, + helpers::{self, os}, + USER_DIRS, +}; pub static ENV: Lazy> = Lazy::new(|| std::env::vars().collect()); @@ -124,17 +125,106 @@ struct CompleteParameters<'a, T: Serialize> { pub dirs: &'static DirectoryPrameters, } -pub fn render(template: &str, parameters: &impl Serialize) -> Result { - let complete = CompleteParameters { - parameters, - env: &ENV, - whoami: &WHOAMI_PRAMETERS, - os: &helpers::os::OS.to_string().to_ascii_lowercase(), - dirs: &DIRECTORY_PRAMETERS, - }; +pub(crate) struct Engine<'a>(Handlebars<'a>); + +impl<'b> Engine<'b> { + pub fn new<'a>(config: &'a Config, cli: &'a Cli) -> Engine<'b> { + let mut hb = handlebars_misc_helpers::new_hbs::<'b>(); + hb.set_strict_mode(false); + + hb.register_helper("windows", WindowsHelper.conv::>()); + hb.register_helper("linux", LinuxHelper.conv::>()); + hb.register_helper("darwin", DarwinHelper.conv::>()); + + hb.register_helper( + "eval", + EvalHelper { + shell_command: config.shell_command.as_ref().cloned(), + dry_run: cli.dry_run, + } + .pipe(Box::new), + ); + + Self(hb) + } + + pub fn render(&self, template: &str, parameters: &impl Serialize) -> Result { + let complete = CompleteParameters { + parameters, + env: &ENV, + whoami: &WHOAMI_PRAMETERS, + os: &helpers::os::OS.to_string().to_ascii_lowercase(), + dirs: &DIRECTORY_PRAMETERS, + }; - HANDLEBARS.render_template(template, &complete).map_err(Error::RenderingTemplate) + self.render_template(template, &complete).map_err(Error::RenderingTemplate) + } + + pub fn render_template(&self, template_string: &str, data: &T) -> Result + where + T: Serialize, + { + self.0.render_template(template_string, data) + } +} + +pub struct WindowsHelper; + +impl HelperDef for WindowsHelper { + fn call<'reg: 'rc, 'rc>(&self, h: &Helper<'reg, 'rc>, r: &'reg Handlebars<'reg>, ctx: &'rc Context, rc: &mut RenderContext<'reg, 'rc>, out: &mut dyn Output) -> HelperResult { + if os::OS.is_windows() { h.template() } else { h.inverse() }.map(|t| t.render(r, ctx, rc, out)).map_or(Ok(()), |r| r) + } +} + +pub struct LinuxHelper; + +impl HelperDef for LinuxHelper { + fn call<'reg: 'rc, 'rc>(&self, h: &Helper<'reg, 'rc>, r: &'reg Handlebars<'reg>, ctx: &'rc Context, rc: &mut RenderContext<'reg, 'rc>, out: &mut dyn Output) -> HelperResult { + if os::OS.is_linux() { h.template() } else { h.inverse() }.map(|t| t.render(r, ctx, rc, out)).map_or(Ok(()), |r| r) + } +} + +pub struct DarwinHelper; + +impl HelperDef for DarwinHelper { + fn call<'reg: 'rc, 'rc>(&self, h: &Helper<'reg, 'rc>, r: &'reg Handlebars<'reg>, ctx: &'rc Context, rc: &mut RenderContext<'reg, 'rc>, out: &mut dyn Output) -> HelperResult { + if os::OS.is_darwin() { h.template() } else { h.inverse() }.map(|t| t.render(r, ctx, rc, out)).map_or(Ok(()), |r| r) + } +} + +pub struct EvalHelper { + shell_command: Option, + dry_run: bool, +} + +impl HelperDef for EvalHelper { + fn call_inner<'reg: 'rc, 'rc>(&self, h: &Helper<'reg, 'rc>, r: &'reg Handlebars<'reg>, _: &'rc Context, _: &mut RenderContext<'reg, 'rc>) -> Result, RenderError> { + let cmd = h + .param(0) + .ok_or_else(|| RenderError::new("Param not found for helper \"eval\""))? + .value() + .as_str() + .ok_or_else(|| RenderError::new("Param needs to be a string \"eval\""))?; + + if self.dry_run { + format!("{{{{ eval \"{cmd}\" }}}}").conv::().conv::().pipe(Ok) + } else { + let cmd = if let Some(shell_command) = self.shell_command.as_ref() { + r.render_template(shell_command, &hash_map! { "cmd": &cmd }) + .map_err(|err| RenderError::from_error("Could not render shell command", err))? + } else { + cmd.to_owned() + }; + + let cmd = shellwords::split(&cmd).map_err(|e| RenderError::from_error("Could not parse eval command", e))?; + + match helpers::run_command(&cmd[0], &cmd[1..], true, false) { + Err(err) => RenderError::from_error("Eval command did not run successfully", err).pipe(Err), + Ok(result) => result.trim().conv::().conv::().pipe(Ok), + } + } + } } #[cfg(test)] -mod test; +pub mod test; diff --git a/src/templating/test.rs b/src/templating/test.rs index 6703003..dfe4589 100644 --- a/src/templating/test.rs +++ b/src/templating/test.rs @@ -2,8 +2,23 @@ use figment::{util::map, value}; use rstest::rstest; use speculoos::prelude::*; -use super::{render, Parameters}; -use crate::config::{Config, LinkType}; +use super::{Engine, Parameters}; +use crate::{ + cli::{Cli, Command, PathBuf}, + config::{Config, LinkType}, + helpers::os, +}; + +pub(crate) fn get_handlebars<'a>() -> Engine<'a> { + let cli = Cli { + dry_run: true, + dotfiles: None, + config: PathBuf("".into()), + command: Command::Clone { repo: "".to_owned() }, + }; + + Engine::new(&Config::default(), &cli) +} #[rstest] #[case("{{ config.variables.test }}", "test")] @@ -12,23 +27,74 @@ use crate::config::{Config, LinkType}; #[case("{{ dirs.user.home }}", &directories::UserDirs::new().unwrap().home_dir().to_string_lossy().to_string())] #[case("{{ os }}", &crate::helpers::os::OS.to_string().to_ascii_lowercase())] fn templating(#[case] template: &str, #[case] expected: &str) { - assert_that!(render( - template, - &Parameters { - config: &Config { - dotfiles: "dotfiles".into(), - link_type: LinkType::Hard, - shell_command: "shell_command".to_owned().into(), - variables: map! { - "test".to_owned() => "test".into(), - "nested".to_owned() => map!{ - "nest" => value::Value::from("nest") - }.into() - } - }, - name: "name" - } - ) - .unwrap()) - .is_equal_to(expected.to_owned()); + let config = Config { + dotfiles: "dotfiles".into(), + link_type: LinkType::Hard, + shell_command: "shell_command".to_owned().into(), + variables: map! { + "test".to_owned() => "test".into(), + "nested".to_owned() => map!{ + "nest" => value::Value::from("nest") + }.into() + }, + }; + + let cli = Cli { + dry_run: true, + dotfiles: None, + config: PathBuf("".into()), + command: Command::Clone { repo: "".to_owned() }, + }; + + assert_that!(Engine::new(&config, &cli).render(template, &Parameters { config: &config, name: "name" }).unwrap()).is_equal_to(expected.to_owned()); +} + +#[test] +fn os_helpers() { + let config = Config::default(); + + assert_that!(get_handlebars() + .render( + "{{ #windows }}windows{{ /windows }}{{ #linux }}linux{{ /linux }}{{ #darwin }}darwin{{ /darwin }}", + &Parameters { config: &config, name: "" } + ) + .unwrap()) + .is_equal_to(os::OS.to_string().to_ascii_lowercase()); +} + +#[test] +fn os_else_helpers() { + let config = Config::default(); + + let mut expected = "".to_owned(); + if !os::OS.is_windows() { + expected += "else_windows"; + } + if !os::OS.is_linux() { + expected += "else_linux"; + } + if !os::OS.is_darwin() { + expected += "else_darwin"; + } + assert_that!(get_handlebars() + .render( + "{{ #windows }}{{ else }}else_windows{{ /windows }}{{ #linux }}{{ else }}else_linux{{ /linux }}{{ #darwin }}{{ else }}else_darwin{{ /darwin }}", + &Parameters { config: &config, name: "" } + ) + .unwrap()) + .is_equal_to(expected); +} + +#[test] +fn eval_helper() { + let config = Config::default(); + + let cli = Cli { + dry_run: false, + dotfiles: None, + config: PathBuf("".into()), + command: Command::Clone { repo: "".to_owned() }, + }; + + assert_that!(Engine::new(&config, &cli).render("{{ eval \"echo 'test'\" }}", &Parameters { config: &config, name: "" }).unwrap()).is_equal_to("test".to_owned()); }