diff --git a/Cargo.lock b/Cargo.lock index a23ac3791..8680b5cfe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1978,6 +1978,7 @@ dependencies = [ "openssl", "path-absolutize", "petgraph", + "platform-info", "predicates", "pretty_assertions", "rand", @@ -2547,6 +2548,16 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "platform-info" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5ff316b9c4642feda973c18f0decd6c8b0919d4722566f6e4337cce0dd88217" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "portable-atomic" version = "1.7.0" diff --git a/Cargo.toml b/Cargo.toml index 73cb02486..d4d062548 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,11 +74,12 @@ indicatif = { version = "0.17.8", features = ["default", "improved_unicode"] } indoc = "2.0.5" itertools = "0.13" log = "0.4.21" -num_cpus = "1.16.0" +num_cpus = "1.16.0" # gets cross-platform the number of CPU once_cell = "1.19.0" openssl = { version = "0.10.66", optional = true } path-absolutize = "3.1.1" petgraph = "0.6.4" +platform-info = "2.0.3" # cross-platform platform information rand = "0.8.5" rayon = "1.10.0" regex = "1.10.4" diff --git a/src/tera.rs b/src/tera.rs index 09029eb1e..72972f5f4 100644 --- a/src/tera.rs +++ b/src/tera.rs @@ -1,7 +1,12 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; +use heck::{ + ToKebabCase, ToLowerCamelCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase, + ToUpperCamelCase, +}; use once_cell::sync::Lazy; +use platform_info::{PlatformInfo, PlatformInfoAPI, UNameAPI}; use tera::{Context, Tera, Value}; use crate::cmd::cmd; @@ -36,6 +41,47 @@ pub fn get_tera(dir: Option<&Path>) -> Tera { } }, ); + tera.register_function( + "arch", + move |_args: &HashMap| -> tera::Result { + let info = PlatformInfo::new().expect("unable to determine platform info"); + let result = String::from(info.machine().to_string_lossy()); // ignore potential UTF-8 convension error + Ok(Value::String(result)) + }, + ); + tera.register_function( + "num_cpus", + move |_args: &HashMap| -> tera::Result { + let num = num_cpus::get(); + Ok(Value::String(num.to_string())) + }, + ); + tera.register_function( + "os", + move |_args: &HashMap| -> tera::Result { + let info = PlatformInfo::new().expect("unable to determine platform info"); + let result = String::from(info.osname().to_string_lossy()); // ignore potential UTF-8 convension error + Ok(Value::String(result)) + }, + ); + tera.register_function( + "os_family", + move |_args: &HashMap| -> tera::Result { + let info = PlatformInfo::new().expect("unable to determine platform info"); + let result = String::from(info.sysname().to_string_lossy()); // ignore potential UTF-8 convension error + Ok(Value::String(result)) + }, + ); + tera.register_function( + "invocation_directory", + move |_args: &HashMap| -> tera::Result { + let path = env::current_dir().unwrap_or_default(); + + let result = String::from(path.to_string_lossy()); + + Ok(Value::String(result)) + }, + ); tera.register_filter( "hash", move |input: &Value, _args: &HashMap| match input { @@ -78,6 +124,59 @@ pub fn get_tera(dir: Option<&Path>) -> Tera { _ => Err("join_path input must be an array of strings".into()), }, ); + tera.register_filter( + "quote", + move |input: &Value, _args: &HashMap| match input { + Value::String(s) => { + let result = format!("'{}'", s.replace("'", "\\'")); + + Ok(Value::String(result)) + } + _ => Err("quote input must be a string".into()), + }, + ); + tera.register_filter( + "kebabcase", + move |input: &Value, _args: &HashMap| match input { + Value::String(s) => Ok(Value::String(s.to_kebab_case())), + _ => Err("kebabcase input must be a string".into()), + }, + ); + tera.register_filter( + "lowercamelcase", + move |input: &Value, _args: &HashMap| match input { + Value::String(s) => Ok(Value::String(s.to_lower_camel_case())), + _ => Err("lowercamelcase input must be a string".into()), + }, + ); + tera.register_filter( + "shoutykebabcase", + move |input: &Value, _args: &HashMap| match input { + Value::String(s) => Ok(Value::String(s.to_shouty_kebab_case())), + _ => Err("shoutykebabcase input must be a string".into()), + }, + ); + tera.register_filter( + "shoutysnakecase", + move |input: &Value, _args: &HashMap| match input { + Value::String(s) => Ok(Value::String(s.to_shouty_snake_case())), + _ => Err("shoutysnakecase input must be a string".into()), + }, + ); + tera.register_filter( + "snakecase", + move |input: &Value, _args: &HashMap| match input { + Value::String(s) => Ok(Value::String(s.to_snake_case())), + _ => Err("snakecase input must be a string".into()), + }, + ); + tera.register_filter( + "uppercamelcase", + move |input: &Value, _args: &HashMap| match input { + Value::String(s) => Ok(Value::String(s.to_upper_camel_case())), + _ => Err("uppercamelcase input must be a string".into()), + }, + ); tera.register_tester( "file_exists", move |input: Option<&Value>, _args: &[Value]| match input { @@ -85,5 +184,197 @@ pub fn get_tera(dir: Option<&Path>) -> Tera { _ => Err("file_exists input must be a string".into()), }, ); + tera } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[cfg(target_arch = "x86_64")] + fn test_render_with_custom_function_arch_x86_64() { + let mut tera = get_tera(Option::default()); + + let result = tera + .render_str("{{ arch() }}", &Context::default()) + .unwrap(); + + assert_eq!("x86_64", result); + } + + #[test] + #[cfg(target_arch = "aarch64")] + fn test_render_with_custom_function_arch_arm64() { + let mut tera = get_tera(Option::default()); + + let result = tera + .render_str("{{ arch() }}", &Context::default()) + .unwrap(); + + assert_eq!("aarch64", result); + } + + #[test] + fn test_render_with_custom_function_num_cpus() { + let mut tera = get_tera(Option::default()); + + let result = tera + .render_str("{{ num_cpus() }}", &Context::default()) + .unwrap(); + + let num = result.parse::().unwrap(); + assert!(num > 0); + } + + #[test] + #[cfg(target_os = "linux")] + fn test_render_with_custom_function_os_linux() { + let mut tera = get_tera(Option::default()); + + let result = tera.render_str("{{ os() }}", &Context::default()).unwrap(); + + assert_eq!("GNU/Linux", result); + } + + #[test] + #[cfg(target_os = "windows")] + fn test_render_with_custom_function_os_windows() { + let mut tera = get_tera(Option::default()); + + let result = tera.render_str("{{ os() }}", &Context::default()).unwrap(); + + assert_eq!("Windows", result); + } + + #[test] + #[cfg(target_family = "unix")] + fn test_render_with_custom_function_os_family_unix() { + let mut tera = get_tera(Option::default()); + + let result = tera + .render_str("{{ os_family() }}", &Context::default()) + .unwrap(); + + assert_eq!("Linux", result); + } + + #[test] + #[cfg(target_family = "windows")] + fn test_render_with_custom_function_os_windows() { + let mut tera = get_tera(Option::default()); + + let result = tera + .render_str("{{ os_family() }}", &Context::default()) + .unwrap(); + + assert_eq!("Windows", result); + } + + #[test] + #[cfg(target_family = "unix")] + fn test_render_with_custom_function_invocation_directory() { + let a = env::set_current_dir("/tmp").is_ok(); + let mut tera = get_tera(Option::default()); + assert!(a); + println!("{:?}", env::current_dir().unwrap()); + + let result = tera + .render_str("{{ invocation_directory() }}", &Context::default()) + .unwrap(); + + assert_eq!("/tmp", result); + } + + #[test] + #[cfg(target_family = "windows")] + fn test_render_with_custom_function_invocation_directory() { + let a = env::set_current_dir("C:\\").is_ok(); + let mut tera = get_tera(Option::default()); + assert!(a); + + let result = tera + .render_str("{{ invocation_directory() }}", &Context::default()) + .unwrap(); + + assert_eq!("C:\\", result); + } + + #[test] + fn test_render_with_custom_filter_quote() { + let mut tera = get_tera(Option::default()); + + let result = tera + .render_str("{{ \"quoted'str\" | quote }}", &Context::default()) + .unwrap(); + + assert_eq!("'quoted\\'str'", result); + } + + #[test] + fn test_render_with_custom_filter_kebabcase() { + let mut tera = get_tera(Option::default()); + + let result = tera + .render_str("{{ \"thisFilter\" | kebabcase }}", &Context::default()) + .unwrap(); + + assert_eq!("this-filter", result); + } + + #[test] + fn test_render_with_custom_filter_lowercamelcase() { + let mut tera = get_tera(Option::default()); + + let result = tera + .render_str("{{ \"Camel-case\" | lowercamelcase }}", &Context::default()) + .unwrap(); + + assert_eq!("camelCase", result); + } + + #[test] + fn test_render_with_custom_filter_shoutykebabcase() { + let mut tera = get_tera(Option::default()); + + let result = tera + .render_str("{{ \"kebabCase\" | shoutykebabcase }}", &Context::default()) + .unwrap(); + + assert_eq!("KEBAB-CASE", result); + } + + #[test] + fn test_render_with_custom_filter_shoutysnakecase() { + let mut tera = get_tera(Option::default()); + + let result = tera + .render_str("{{ \"snakeCase\" | shoutysnakecase }}", &Context::default()) + .unwrap(); + + assert_eq!("SNAKE_CASE", result); + } + + #[test] + fn test_render_with_custom_filter_snakecase() { + let mut tera = get_tera(Option::default()); + + let result = tera + .render_str("{{ \"snakeCase\" | snakecase }}", &Context::default()) + .unwrap(); + + assert_eq!("snake_case", result); + } + + #[test] + fn test_render_with_custom_filter_uppercamelcase() { + let mut tera = get_tera(Option::default()); + + let result = tera + .render_str("{{ \"CamelCase\" | uppercamelcase }}", &Context::default()) + .unwrap(); + + assert_eq!("CamelCase", result); + } +}