From 2aeb7b038bf8b428e6cdd6920e6ffc2156d20a88 Mon Sep 17 00:00:00 2001 From: Erick Guan Date: Sat, 14 Sep 2024 21:31:25 +0200 Subject: [PATCH 1/9] chore: use same testing convention in tera.rs --- src/tera.rs | 94 +++++++++++++++-------------------------------------- 1 file changed, 27 insertions(+), 67 deletions(-) diff --git a/src/tera.rs b/src/tera.rs index c53e39d47..2c8c25f51 100644 --- a/src/tera.rs +++ b/src/tera.rs @@ -287,7 +287,7 @@ mod tests { use insta::assert_snapshot; #[test] - fn test_render_with_custom_function_arch() { + fn test_arch() { reset(); if cfg!(target_arch = "x86_64") { assert_eq!(render("{{arch()}}"), "x64"); @@ -299,20 +299,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::().unwrap(); + let s = render("{{ num_cpus() }}"); + let num = s.parse::().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"); @@ -324,7 +319,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"); @@ -334,87 +329,52 @@ mod tests { } #[test] - fn test_render_with_custom_filter_quote() { + fn test_quote() { 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 s = render("{{ \"quoted'str\" | quote }}"); + assert_eq!(s, "'quoted\\'str'"); } #[test] - fn test_render_with_custom_filter_kebabcase() { + fn test_kebabcase() { 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("{{ \"thisFilter\" | kebabcase }}"); + assert_eq!(s, "this-filter"); } #[test] - fn test_render_with_custom_filter_lowercamelcase() { + fn test_lowercamelcase() { 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("{{ \"Camel-case\" | lowercamelcase }}"); + assert_eq!(s, "camelCase"); } #[test] - fn test_render_with_custom_filter_shoutykebabcase() { + fn test_shoutykebabcase() { 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("{{ \"kebabCase\" | shoutykebabcase }}"); + assert_eq!(s, "KEBAB-CASE"); } #[test] - fn test_render_with_custom_filter_shoutysnakecase() { + fn test_shoutysnakecase() { 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("{{ \"snakeCase\" | shoutysnakecase }}"); + assert_eq!(s, "SNAKE_CASE"); } #[test] - fn test_render_with_custom_filter_snakecase() { + fn test_snakecase() { 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\" | snakecase }}"); + assert_eq!(s, "snake_case"); } #[test] - fn test_render_with_custom_filter_uppercamelcase() { + fn test_uppercamelcase() { reset(); - let mut tera = get_tera(Option::default()); - - let result = tera - .render_str("{{ \"CamelCase\" | uppercamelcase }}", &Context::default()) - .unwrap(); - - assert_eq!("CamelCase", result); + let s = render("{{ \"CamelCase\" | uppercamelcase }}"); + assert_eq!(s, "CamelCase"); } #[test] From 43b9a0b014cb3f3b01587b98672bce240edbfc03 Mon Sep 17 00:00:00 2001 From: Erick Guan Date: Sun, 15 Sep 2024 16:26:07 +0200 Subject: [PATCH 2/9] feat: add mise_bin and mise_pid in template context --- docs/templates.md | 2 ++ src/env.rs | 2 ++ src/tera.rs | 37 ++++++++++++++++++++++++++++++++++--- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/docs/templates.md b/docs/templates.md index 83f137c4b..189a724d5 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -12,6 +12,8 @@ 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 As well as these functions: diff --git a/src/env.rs b/src/env.rs index 196450aaf..da43a1a62 100644 --- a/src/env.rs +++ b/src/env.rs @@ -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; @@ -126,6 +127,7 @@ pub static MISE_BIN: Lazy = Lazy::new(|| { .or_else(|| current_exe().ok()) .unwrap_or_else(|| "mise".into()) }); +pub static MISE_PID: Lazy = Lazy::new(|| process::id().to_string()); pub static __MISE_SCRIPT: Lazy = Lazy::new(|| var_is_true("__MISE_SCRIPT")); pub static __MISE_DIFF: Lazy = Lazy::new(get_env_diff); pub static __MISE_ORIG_PATH: Lazy> = Lazy::new(|| var("__MISE_ORIG_PATH").ok()); diff --git a/src/tera.rs b/src/tera.rs index 2c8c25f51..e7ce4d239 100644 --- a/src/tera.rs +++ b/src/tera.rs @@ -15,6 +15,8 @@ use crate::{env, hash}; pub static BASE_CONTEXT: Lazy = 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); } @@ -286,6 +288,32 @@ mod tests { use crate::test::reset; use insta::assert_snapshot; + #[test] + 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::().unwrap(); + assert!(pid > 0); + } + #[test] fn test_arch() { reset(); @@ -457,8 +485,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() } } From c7438a5719b994bdacb3dbe58912d44d4b3e4e51 Mon Sep 17 00:00:00 2001 From: Erick Guan Date: Sun, 15 Sep 2024 17:21:36 +0200 Subject: [PATCH 3/9] feat: add datetime, random, error in tera.rs --- docs/templates.md | 12 ++++++ src/tera.rs | 96 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 105 insertions(+), 3 deletions(-) diff --git a/docs/templates.md b/docs/templates.md index 189a724d5..e0a1cecfc 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -22,6 +22,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: diff --git a/src/tera.rs b/src/tera.rs index e7ce4d239..e1b0d0f9c 100644 --- a/src/tera.rs +++ b/src/tera.rs @@ -1,11 +1,13 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; +use chrono::{Local, Utc}; use heck::{ ToKebabCase, ToLowerCamelCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase, }; use once_cell::sync::Lazy; +use rand::{seq::SliceRandom, thread_rng}; use tera::{Context, Tera, Value}; use versions::{Requirement, Versioning}; @@ -74,6 +76,58 @@ pub fn get_tera(dir: Option<&Path>) -> Tera { Ok(Value::String(env::consts::FAMILY.to_string())) }, ); + tera.register_function( + "error", + move |args: &HashMap| -> tera::Result { + match args.get("message") { + Some(Value::String(message)) => Err(message.clone().into()), + _ => Err("error message must be a string".into()), + } + }, + ); + tera.register_function( + "choice", + move |args: &HashMap| -> tera::Result { + 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::>(); + let mut rng = thread_rng(); + let result = + (0..n).map(|_| alphabet.choose(&mut rng).unwrap()).collect(); + Ok(Value::String(result)) + } + _ => Err("choice alphtbet must be an string".into()), + } + } + _ => Err("choice n must be an integer".into()), + } + }, + ); + tera.register_function( + "datetime", + move |args: &HashMap| -> tera::Result { + let format = match args.get("format") { + Some(Value::String(format)) => format, + _ => "%+", + }; + let result = Local::now().format(format).to_string(); + Ok(Value::String(result)) + }, + ); + tera.register_function( + "datetime_utc", + move |args: &HashMap| -> tera::Result { + let format = match args.get("format") { + Some(Value::String(format)) => format, + _ => "%+", + }; + let result = Utc::now().format(format).to_string(); + Ok(Value::String(result)) + }, + ); tera.register_filter( "hash_file", move |input: &Value, args: &HashMap| match input { @@ -286,6 +340,7 @@ pub fn get_tera(dir: Option<&Path>) -> Tera { mod tests { use super::*; use crate::test::reset; + use chrono::Datelike; use insta::assert_snapshot; #[test] @@ -293,17 +348,24 @@ mod tests { 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()); + assert_eq!( + render("{{mise_bin}}"), + env::current_exe() + .unwrap() + .into_os_string() + .into_string() + .unwrap() + ); } #[test] @@ -356,6 +418,34 @@ mod tests { } } + #[test] + #[should_panic] + fn test_error() { + reset(); + render("{{error(\"message\")}}"); + } + + #[test] + fn test_choice() { + reset(); + let result = render("{{choice(n=8, alphabet=\"abcdefgh\")}}"); + assert_eq!(result.trim().len(), 8); + } + + #[test] + fn test_datetime() { + reset(); + let result = render("{{datetime(format=\"%Y\")}}"); + assert_eq!(result, Local::now().year().to_string()); + } + + #[test] + fn test_datetime_utc() { + reset(); + let result = render("{{datetime_utc(format=\"%m\")}}"); + assert_eq!(result, format!("{:0>2}", Utc::now().month().to_string())); + } + #[test] fn test_quote() { reset(); From 3704e3f8871f7551d1f54c04a7991b7fa94708ec Mon Sep 17 00:00:00 2001 From: Erick Guan Date: Sun, 15 Sep 2024 17:56:03 +0200 Subject: [PATCH 4/9] chore: add a few tests for path related tera filters --- src/tera.rs | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/tera.rs b/src/tera.rs index e1b0d0f9c..bae79a300 100644 --- a/src/tera.rs +++ b/src/tera.rs @@ -22,6 +22,10 @@ pub static BASE_CONTEXT: Lazy = Lazy::new(|| { 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 }); @@ -155,6 +159,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| match input { @@ -376,6 +382,34 @@ mod tests { 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")); // test dir is not deterministic + } + + #[test] + fn test_xdg_state_home() { + reset(); + let s = render("{{xdg_state_home}}"); + assert!(s.ends_with("/.local")); // test dir is not deterministic + } + #[test] fn test_arch() { reset(); @@ -509,6 +543,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(); @@ -544,6 +585,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::().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(); From cdbd8a94711bd065dc9778d0a067cd7090ffaab6 Mon Sep 17 00:00:00 2001 From: Erick Guan Date: Sun, 15 Sep 2024 18:04:08 +0200 Subject: [PATCH 5/9] feat: add xdg paths to tera context --- docs/templates.md | 4 ++++ src/tera.rs | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/templates.md b/docs/templates.md index e0a1cecfc..025444faf 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -14,6 +14,10 @@ The following context objects are available inside templates: `.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: diff --git a/src/tera.rs b/src/tera.rs index bae79a300..ae612c921 100644 --- a/src/tera.rs +++ b/src/tera.rs @@ -400,14 +400,14 @@ mod tests { fn test_xdg_data_home() { reset(); let s = render("{{xdg_data_home}}"); - assert!(s.ends_with("/.local")); // test dir is not deterministic + 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")); // test dir is not deterministic + assert!(s.ends_with("/.local/state")); // test dir is not deterministic } #[test] From 4ec4304fae92ff75ca7f304d8dd2d525686e5703 Mon Sep 17 00:00:00 2001 From: Erick Guan Date: Sun, 15 Sep 2024 18:16:03 +0200 Subject: [PATCH 6/9] chore: fix markdown trailing space --- docs/templates.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/templates.md b/docs/templates.md index 025444faf..69e143f9a 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -27,11 +27,11 @@ As well as these functions: - `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 +- `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 +- `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 From 6f78d558dcfabe3d8804ded8c0bf32ca8dd3e6bc Mon Sep 17 00:00:00 2001 From: Erick Guan Date: Sun, 15 Sep 2024 20:17:05 +0200 Subject: [PATCH 7/9] fixup! feat: add datetime, random, error in tera.rs --- src/tera.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tera.rs b/src/tera.rs index ae612c921..8dacf4ac0 100644 --- a/src/tera.rs +++ b/src/tera.rs @@ -103,7 +103,7 @@ pub fn get_tera(dir: Option<&Path>) -> Tera { (0..n).map(|_| alphabet.choose(&mut rng).unwrap()).collect(); Ok(Value::String(result)) } - _ => Err("choice alphtbet must be an string".into()), + _ => Err("choice alphabet must be an string".into()), } } _ => Err("choice n must be an integer".into()), From 95f8abfe3ce3c3b499d8185f6858b8d03032897d Mon Sep 17 00:00:00 2001 From: Erick Guan Date: Sun, 15 Sep 2024 21:03:28 +0200 Subject: [PATCH 8/9] fixup! feat: add datetime, random, error in tera.rs Removed because `tera` supports `now`. --- src/tera.rs | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/src/tera.rs b/src/tera.rs index 8dacf4ac0..464fab458 100644 --- a/src/tera.rs +++ b/src/tera.rs @@ -1,7 +1,6 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; -use chrono::{Local, Utc}; use heck::{ ToKebabCase, ToLowerCamelCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase, @@ -110,28 +109,6 @@ pub fn get_tera(dir: Option<&Path>) -> Tera { } }, ); - tera.register_function( - "datetime", - move |args: &HashMap| -> tera::Result { - let format = match args.get("format") { - Some(Value::String(format)) => format, - _ => "%+", - }; - let result = Local::now().format(format).to_string(); - Ok(Value::String(result)) - }, - ); - tera.register_function( - "datetime_utc", - move |args: &HashMap| -> tera::Result { - let format = match args.get("format") { - Some(Value::String(format)) => format, - _ => "%+", - }; - let result = Utc::now().format(format).to_string(); - Ok(Value::String(result)) - }, - ); tera.register_filter( "hash_file", move |input: &Value, args: &HashMap| match input { @@ -346,7 +323,6 @@ pub fn get_tera(dir: Option<&Path>) -> Tera { mod tests { use super::*; use crate::test::reset; - use chrono::Datelike; use insta::assert_snapshot; #[test] @@ -466,20 +442,6 @@ mod tests { assert_eq!(result.trim().len(), 8); } - #[test] - fn test_datetime() { - reset(); - let result = render("{{datetime(format=\"%Y\")}}"); - assert_eq!(result, Local::now().year().to_string()); - } - - #[test] - fn test_datetime_utc() { - reset(); - let result = render("{{datetime_utc(format=\"%m\")}}"); - assert_eq!(result, format!("{:0>2}", Utc::now().month().to_string())); - } - #[test] fn test_quote() { reset(); From a24b12d58cda13948866618ef86251cc8baf82a4 Mon Sep 17 00:00:00 2001 From: Erick Guan Date: Sun, 15 Sep 2024 21:10:05 +0200 Subject: [PATCH 9/9] fixup! feat: add datetime, random, error in tera.rs Remove in favor of tera built-in functions --- src/tera.rs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/tera.rs b/src/tera.rs index 464fab458..aaf7df6f8 100644 --- a/src/tera.rs +++ b/src/tera.rs @@ -79,15 +79,6 @@ pub fn get_tera(dir: Option<&Path>) -> Tera { Ok(Value::String(env::consts::FAMILY.to_string())) }, ); - tera.register_function( - "error", - move |args: &HashMap| -> tera::Result { - match args.get("message") { - Some(Value::String(message)) => Err(message.clone().into()), - _ => Err("error message must be a string".into()), - } - }, - ); tera.register_function( "choice", move |args: &HashMap| -> tera::Result { @@ -428,13 +419,6 @@ mod tests { } } - #[test] - #[should_panic] - fn test_error() { - reset(); - render("{{error(\"message\")}}"); - } - #[test] fn test_choice() { reset();