Skip to content

Commit

Permalink
feat: add rest of tera features for templates (#2582)
Browse files Browse the repository at this point in the history
* chore: use same testing convention in tera.rs

* feat: add mise_bin and mise_pid in template context

* feat: add datetime, random, error in tera.rs

* chore: add a few tests for path related tera filters

* feat: add xdg paths to tera context

* chore: fix markdown trailing space

* fixup! feat: add datetime, random, error in tera.rs

* fixup! feat: add datetime, random, error in tera.rs

Removed because `tera` supports `now`.

* fixup! feat: add datetime, random, error in tera.rs

Remove in favor of tera built-in functions

---------

Co-authored-by: Erick Guan <erickguan@users.noreply.github.com>
  • Loading branch information
erickguan and erickguan committed Sep 15, 2024
1 parent 0c88ede commit 146a52f
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 69 deletions.
18 changes: 18 additions & 0 deletions docs/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ The following context objects are available inside templates:
- `cwd: PathBuf` – current working directory
- `config_root: PathBuf` – directory containing the `mise.toml` file or directory containing
`.mise` directory with config file.
- `mise_bin` - the path to the current mise executable
- `mise_pid` - the pid of the current mise process
- `xdg_cache_home` - the directory of XDG cache home
- `xdg_config_home` - the directory of XDG config home
- `xdg_data_home` - the directory of XDG data home
- `xdg_state_home` - the directory of XDG state home

As well as these functions:

Expand All @@ -20,6 +26,18 @@ As well as these functions:
- `os() -> String` – return the operating system, e.g. `linux`, `macos`, `windows`
- `os_family() -> String` – return the operating system family, e.g. `unix`, `windows`
- `num_cpus() -> usize` – return the number of CPUs on the system
- `error(message) -> String` - Abort execution and report error `message` to user.
- `choice(n, alphabet)` - Generate a string of `n` with random sample with replacement
of `alphabet`. For example, `choice('64', HEX)` will generate a random
64-character lowercase hex string.
- `datetime()` - Return local time with ISO 8601 format
- `datetime(format)` - Return local time with `format`. Read the
[`chrono` library docs](https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
for the format
- `datetime_utc()` - Return UTC time with ISO 8601 format
- `datetime_utc(format)` - Return UTC time with `format`. Read the
[`chrono` library docs](https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
for the format

And these filters:

Expand Down
2 changes: 2 additions & 0 deletions src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::collections::{HashMap, HashSet};
pub use std::env::*;
use std::path;
use std::path::PathBuf;
use std::process;
use std::string::ToString;
use std::sync::RwLock;
use std::time::Duration;
Expand Down Expand Up @@ -126,6 +127,7 @@ pub static MISE_BIN: Lazy<PathBuf> = Lazy::new(|| {
.or_else(|| current_exe().ok())
.unwrap_or_else(|| "mise".into())
});
pub static MISE_PID: Lazy<String> = Lazy::new(|| process::id().to_string());
pub static __MISE_SCRIPT: Lazy<bool> = Lazy::new(|| var_is_true("__MISE_SCRIPT"));
pub static __MISE_DIFF: Lazy<EnvDiff> = Lazy::new(get_env_diff);
pub static __MISE_ORIG_PATH: Lazy<Option<String>> = Lazy::new(|| var("__MISE_ORIG_PATH").ok());
Expand Down
221 changes: 152 additions & 69 deletions src/tera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use heck::{
ToUpperCamelCase,
};
use once_cell::sync::Lazy;
use rand::{seq::SliceRandom, thread_rng};
use tera::{Context, Tera, Value};
use versions::{Requirement, Versioning};

Expand All @@ -15,9 +16,15 @@ use crate::{env, hash};
pub static BASE_CONTEXT: Lazy<Context> = Lazy::new(|| {
let mut context = Context::new();
context.insert("env", &*env::PRISTINE_ENV);
context.insert("mise_bin", &*env::MISE_BIN);
context.insert("mise_pid", &*env::MISE_PID);
if let Ok(dir) = env::current_dir() {
context.insert("cwd", &dir);
}
context.insert("xdg_cache_home", &*env::XDG_CACHE_HOME);
context.insert("xdg_config_home", &*env::XDG_CONFIG_HOME);
context.insert("xdg_data_home", &*env::XDG_DATA_HOME);
context.insert("xdg_state_home", &*env::XDG_STATE_HOME);
context
});

Expand Down Expand Up @@ -72,6 +79,27 @@ pub fn get_tera(dir: Option<&Path>) -> Tera {
Ok(Value::String(env::consts::FAMILY.to_string()))
},
);
tera.register_function(
"choice",
move |args: &HashMap<String, Value>| -> tera::Result<Value> {
match args.get("n") {
Some(Value::Number(n)) => {
let n = n.as_u64().unwrap();
match args.get("alphabet") {
Some(Value::String(alphabet)) => {
let alphabet = alphabet.chars().collect::<Vec<char>>();
let mut rng = thread_rng();
let result =
(0..n).map(|_| alphabet.choose(&mut rng).unwrap()).collect();
Ok(Value::String(result))
}
_ => Err("choice alphabet must be an string".into()),
}
}
_ => Err("choice n must be an integer".into()),
}
},
);
tera.register_filter(
"hash_file",
move |input: &Value, args: &HashMap<String, Value>| match input {
Expand Down Expand Up @@ -99,6 +127,8 @@ pub fn get_tera(dir: Option<&Path>) -> Tera {
_ => Err("hash input must be a string".into()),
},
);
// TODO: add `absolute` feature.
// wait until #![feature(absolute_path)] hits Rust stable release channel
tera.register_filter(
"canonicalize",
move |input: &Value, _args: &HashMap<String, Value>| match input {
Expand Down Expand Up @@ -287,7 +317,68 @@ mod tests {
use insta::assert_snapshot;

#[test]
fn test_render_with_custom_function_arch() {
fn test_config_root() {
reset();
assert_eq!(render("{{config_root}}"), "/");
}

#[test]
fn test_cwd() {
reset();
assert_eq!(render("{{cwd}}"), "/");
}

#[test]
fn test_mise_bin() {
reset();
assert_eq!(
render("{{mise_bin}}"),
env::current_exe()
.unwrap()
.into_os_string()
.into_string()
.unwrap()
);
}

#[test]
fn test_mise_pid() {
reset();
let s = render("{{mise_pid}}");
let pid = s.trim().parse::<u32>().unwrap();
assert!(pid > 0);
}

#[test]
fn test_xdg_cache_home() {
reset();
let s = render("{{xdg_cache_home}}");
assert!(s.ends_with("/.cache")); // test dir is not deterministic
}

#[test]
fn test_xdg_config_home() {
reset();
let s = render("{{xdg_config_home}}");
assert!(s.ends_with("/.config")); // test dir is not deterministic
}

#[test]
fn test_xdg_data_home() {
reset();
let s = render("{{xdg_data_home}}");
assert!(s.ends_with("/.local/share")); // test dir is not deterministic
}

#[test]
fn test_xdg_state_home() {
reset();
let s = render("{{xdg_state_home}}");
assert!(s.ends_with("/.local/state")); // test dir is not deterministic
}

#[test]
fn test_arch() {
reset();
if cfg!(target_arch = "x86_64") {
assert_eq!(render("{{arch()}}"), "x64");
Expand All @@ -299,20 +390,15 @@ mod tests {
}

#[test]
fn test_render_with_custom_function_num_cpus() {
fn test_num_cpus() {
reset();
let mut tera = get_tera(Option::default());

let result = tera
.render_str("{{ num_cpus() }}", &Context::default())
.unwrap();

let num = result.parse::<u32>().unwrap();
let s = render("{{ num_cpus() }}");
let num = s.parse::<u32>().unwrap();
assert!(num > 0);
}

#[test]
fn test_render_with_custom_function_os() {
fn test_os() {
reset();
if cfg!(target_os = "linux") {
assert_eq!(render("{{os()}}"), "linux");
Expand All @@ -324,7 +410,7 @@ mod tests {
}

#[test]
fn test_render_with_custom_function_os_family() {
fn test_os_family() {
reset();
if cfg!(target_family = "unix") {
assert_eq!(render("{{os_family()}}"), "unix");
Expand All @@ -334,87 +420,59 @@ mod tests {
}

#[test]
fn test_render_with_custom_filter_quote() {
fn test_choice() {
reset();
let mut tera = get_tera(Option::default());

let result = tera
.render_str("{{ \"quoted'str\" | quote }}", &Context::default())
.unwrap();

assert_eq!("'quoted\\'str'", result);
let result = render("{{choice(n=8, alphabet=\"abcdefgh\")}}");
assert_eq!(result.trim().len(), 8);
}

#[test]
fn test_render_with_custom_filter_kebabcase() {
fn test_quote() {
reset();
let mut tera = get_tera(Option::default());

let result = tera
.render_str("{{ \"thisFilter\" | kebabcase }}", &Context::default())
.unwrap();

assert_eq!("this-filter", result);
let s = render("{{ \"quoted'str\" | quote }}");
assert_eq!(s, "'quoted\\'str'");
}

#[test]
fn test_render_with_custom_filter_lowercamelcase() {
fn test_kebabcase() {
reset();
let mut tera = get_tera(Option::default());

let result = tera
.render_str("{{ \"Camel-case\" | lowercamelcase }}", &Context::default())
.unwrap();

assert_eq!("camelCase", result);
let s = render("{{ \"thisFilter\" | kebabcase }}");
assert_eq!(s, "this-filter");
}

#[test]
fn test_render_with_custom_filter_shoutykebabcase() {
fn test_lowercamelcase() {
reset();
let mut tera = get_tera(Option::default());

let result = tera
.render_str("{{ \"kebabCase\" | shoutykebabcase }}", &Context::default())
.unwrap();

assert_eq!("KEBAB-CASE", result);
let s = render("{{ \"Camel-case\" | lowercamelcase }}");
assert_eq!(s, "camelCase");
}

#[test]
fn test_render_with_custom_filter_shoutysnakecase() {
fn test_shoutykebabcase() {
reset();
let mut tera = get_tera(Option::default());

let result = tera
.render_str("{{ \"snakeCase\" | shoutysnakecase }}", &Context::default())
.unwrap();

assert_eq!("SNAKE_CASE", result);
let s = render("{{ \"kebabCase\" | shoutykebabcase }}");
assert_eq!(s, "KEBAB-CASE");
}

#[test]
fn test_render_with_custom_filter_snakecase() {
fn test_shoutysnakecase() {
reset();
let mut tera = get_tera(Option::default());

let result = tera
.render_str("{{ \"snakeCase\" | snakecase }}", &Context::default())
.unwrap();

assert_eq!("snake_case", result);
let s = render("{{ \"snakeCase\" | shoutysnakecase }}");
assert_eq!(s, "SNAKE_CASE");
}

#[test]
fn test_render_with_custom_filter_uppercamelcase() {
fn test_snakecase() {
reset();
let mut tera = get_tera(Option::default());

let result = tera
.render_str("{{ \"CamelCase\" | uppercamelcase }}", &Context::default())
.unwrap();
let s = render("{{ \"snakeCase\" | snakecase }}");
assert_eq!(s, "snake_case");
}

assert_eq!("CamelCase", result);
#[test]
fn test_uppercamelcase() {
reset();
let s = render("{{ \"CamelCase\" | uppercamelcase }}");
assert_eq!(s, "CamelCase");
}

#[test]
Expand All @@ -431,6 +489,13 @@ mod tests {
assert_snapshot!(s, @"518349c5734814ff9a21ab8d00ed2da6464b1699910246e763a4e6d5feb139fa");
}

#[test]
fn test_canonicalize() {
reset();
let s = render("{{ \"../fixtures/shorthands.toml\" | canonicalize }}");
assert!(s.ends_with("/fixtures/shorthands.toml")); // test dir is not deterministic
}

#[test]
fn test_dirname() {
reset();
Expand Down Expand Up @@ -466,6 +531,21 @@ mod tests {
assert_eq!(s, "48");
}

#[test]
fn test_last_modified() {
reset();
let s = render(r#"{{ "../fixtures/shorthands.toml" | last_modified }}"#);
let timestamp = s.parse::<u64>().unwrap();
assert!(timestamp >= 1725000000 && timestamp <= 2725000000);
}

#[test]
fn test_join_path() {
reset();
let s = render(r#"{{ ["..", "fixtures", "shorthands.toml"] | join_path }}"#);
assert_eq!(s, "../fixtures/shorthands.toml");
}

#[test]
fn test_is_dir() {
reset();
Expand Down Expand Up @@ -497,8 +577,11 @@ mod tests {
}

fn render(s: &str) -> String {
let mut tera = get_tera(Option::default());

tera.render_str(s, &Context::default()).unwrap()
let config_root = Path::new("/");
let mut tera_ctx = BASE_CONTEXT.clone();
tera_ctx.insert("config_root", &config_root);
tera_ctx.insert("cwd", "/");
let mut tera = get_tera(Option::from(config_root));
tera.render_str(s, &tera_ctx).unwrap()
}
}

0 comments on commit 146a52f

Please sign in to comment.