From ffbcb78d9d2049619a630e438658b23ee455b57b Mon Sep 17 00:00:00 2001 From: Alexander Gil Date: Fri, 5 Jul 2024 08:01:51 +0200 Subject: [PATCH] refactor(tera): Change Jinja2 engine for minijinja Signed-off-by: Alexander Gil --- Cargo.lock | 169 +++++++++----------- examples/builtins.rh | 2 +- examples/copy.rh | 4 +- examples/pacman.rh | 2 +- examples/recursivity.rh | 6 +- examples/register.rh | 2 +- examples/task.rh | 4 +- rash_core/Cargo.toml | 7 +- rash_core/src/bin/rash.rs | 35 ++--- rash_core/src/docopt/mod.rs | 146 ++++++++---------- rash_core/src/docopt/options.rs | 32 ++-- rash_core/src/error.rs | 16 +- rash_core/src/lib.rs | 2 +- rash_core/src/modules/assert.rs | 6 +- rash_core/src/modules/debug.rs | 6 +- rash_core/src/modules/set_vars.rs | 18 +-- rash_core/src/modules/template.rs | 24 ++- rash_core/src/task/mod.rs | 249 +++++++++++++++++++----------- rash_core/src/task/valid.rs | 7 +- rash_core/src/utils/jinja2.rs | 139 +++++++++++++++++ rash_core/src/utils/mod.rs | 2 +- rash_core/src/utils/tera.rs | 111 ------------- rash_core/src/vars/env.rs | 30 ++-- rash_core/src/vars/mod.rs | 25 +-- rash_core/tests/mocks/pacman.rh | 24 ++- rash_core/tests/modules/pacman.rs | 5 +- 26 files changed, 546 insertions(+), 527 deletions(-) create mode 100644 rash_core/src/utils/jinja2.rs delete mode 100644 rash_core/src/utils/tera.rs diff --git a/Cargo.lock b/Cargo.lock index 4739d063..e44aa834 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -226,11 +226,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" +checksum = "fca2be1d5c43812bae364ee3f30b3afcb7877cf59f4aeb94c66f313a41d2fac9" [[package]] name = "cargo-husky" @@ -246,9 +252,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" +checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" [[package]] name = "cfg-if" @@ -329,9 +335,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.11" +version = "4.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6ae69fbb0833c6fcd5a8d4b8609f108c7ad95fc11e248d853ff2c42a90df26a" +checksum = "a8670053e87c316345e384ca1f3eba3006fc6355ed8b8a1140d104e109e3df34" dependencies = [ "clap", ] @@ -749,17 +755,6 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "globwalk" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" -dependencies = [ - "bitflags", - "ignore", - "walkdir", -] - [[package]] name = "half" version = "2.4.1" @@ -881,9 +876,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" dependencies = [ "equivalent", "hashbrown 0.14.5", @@ -903,6 +898,7 @@ dependencies = [ "libc", "mio", "rand", + "sc", "serde", "tempfile", "uuid", @@ -1063,6 +1059,23 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memo-map" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374c335b2df19e62d4cb323103473cbc6510980253119180de862d89184f6a83" + +[[package]] +name = "minijinja" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf369fce3289017a63e514dfca10a0a6f1a02216b21b588b79f6a1081eb999f5" +dependencies = [ + "memo-map", + "self_cell", + "serde", +] + [[package]] name = "mio" version = "1.0.1" @@ -1223,9 +1236,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "dee4364d9f3b902ef14fab8a1ddffb783a1cb6b4bba3bfc1fa3922732c7de97f" +dependencies = [ + "zerocopy", +] [[package]] name = "prettytable-rs" @@ -1380,6 +1396,7 @@ dependencies = [ "ipc-channel", "itertools 0.13.0", "log", + "minijinja", "nix", "rash_derive", "regex", @@ -1394,7 +1411,6 @@ dependencies = [ "strum", "strum_macros", "tempfile", - "tera", "term_size", ] @@ -1556,6 +1572,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "sc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "010e18bd3bfd1d45a7e666b236c78720df0d9a7698ebaa9c1c559961eb60a38b" + [[package]] name = "schemars" version = "0.8.21" @@ -1586,6 +1608,12 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "self_cell" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" + [[package]] name = "semver" version = "1.0.23" @@ -1654,7 +1682,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.2.6", + "indexmap 2.3.0", "serde", "serde_derive", "serde_json", @@ -1680,7 +1708,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.3.0", "itoa", "ryu", "serde", @@ -1794,22 +1822,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "tera" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee" -dependencies = [ - "globwalk", - "lazy_static", - "pest", - "pest_derive", - "regex", - "serde", - "serde_json", - "unic-segment", -] - [[package]] name = "term" version = "0.7.0" @@ -1929,9 +1941,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fb9f64314842840f1d940ac544da178732128f1c78c21772e876579e0da1db" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" @@ -1939,7 +1951,7 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.3.0", "toml_datetime", "winnow", ] @@ -1962,56 +1974,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-segment" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" -dependencies = [ - "unic-ucd-segment", -] - -[[package]] -name = "unic-ucd-segment" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] - [[package]] name = "unicase" version = "2.7.0" @@ -2372,3 +2334,24 @@ checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" dependencies = [ "tap", ] + +[[package]] +name = "zerocopy" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] diff --git a/examples/builtins.rh b/examples/builtins.rh index 8db0bbe2..ef4945c5 100755 --- a/examples/builtins.rh +++ b/examples/builtins.rh @@ -6,5 +6,5 @@ uid: {{ rash.user.uid }} gid: {{ rash.user.gid }} dest: "{% if rash.user.uid != 0 %}/tmp{% endif %}/user_info" - mode: "{{ env.FILE_MODE | default(value='400') }}" + mode: "{{ env.FILE_MODE | default('400') }}" diff --git a/examples/copy.rh b/examples/copy.rh index 69aa35f3..c308f7ad 100755 --- a/examples/copy.rh +++ b/examples/copy.rh @@ -12,6 +12,6 @@ - copy: src: "{{ item }}" - dest: "{{ dest }}/{{ item | split(pat='/') | last }}" + dest: "{{ dest }}/{{ item | split('/') | last }}" mode: "{{ options.mode }}" - loop: "{{ source | default (value=[]) }}" + loop: "{{ source | default([]) }}" diff --git a/examples/pacman.rh b/examples/pacman.rh index 0b78c4a4..596f6e3a 100755 --- a/examples/pacman.rh +++ b/examples/pacman.rh @@ -12,4 +12,4 @@ register: packages - debug: - msg: "{{ packages.extra | json_encode }}" + msg: "{{ packages.extra }}" diff --git a/examples/recursivity.rh b/examples/recursivity.rh index 45d71f8f..279dda26 100755 --- a/examples/recursivity.rh +++ b/examples/recursivity.rh @@ -8,14 +8,14 @@ - name: user "{{ env.USER }}" loves debug: msg: | - "My favorite browser is {{ env.BROWSER | split(pat="/") | last | capitalize }}" - when: env | get(key="BROWSER") + "My favorite browser is {{ env.BROWSER | split("/") | last | capitalize }}" + when: "'BROWSER' in env" - command: cmd: "pwd" chdir: "{{ env.HOME }}" transfer_pid: true - when: env | get(key="MY_PASSWORD") + when: "'MY_PASSWORD' in env" - name: last command must send with transfer_pid to let it as PID 1 command: diff --git a/examples/register.rh b/examples/register.rh index 07dce02b..1c401817 100755 --- a/examples/register.rh +++ b/examples/register.rh @@ -6,5 +6,5 @@ - name: files in directory debug: - var: item | replace(from=rash.dir, to='.') + var: item | replace(rash.dir, '.') loop: "{{ find_result.extra }}" diff --git a/examples/task.rh b/examples/task.rh index 234244a0..1ea2afff 100755 --- a/examples/task.rh +++ b/examples/task.rh @@ -20,7 +20,7 @@ dest: "/tmp/MY_PASSWORD_FILE_{{ file_name }}" mode: "400" vars: - file_name: "{{ item | split(pat='/') | last }}" + file_name: "{{ item | split('/') | last }}" loop: "{{ find_result.extra }}" - when: env | get(key="MY_PASSWORD") + when: "'MY_PASSWORD' in env" register: save_passwords_result diff --git a/rash_core/Cargo.toml b/rash_core/Cargo.toml index 0be469c0..eda60fd2 100644 --- a/rash_core/Cargo.toml +++ b/rash_core/Cargo.toml @@ -29,13 +29,13 @@ serde_json.workspace = true serde_yaml.workspace = true byte-unit = "5.1.4" console = "0.15.8" +minijinja = { version = "2.1.2", features = ["loader"]} clap = { workspace = true, features = ["std", "color", "derive", "cargo"]} exec = "0.3.1" fern = { version = "0.6.2", features = ["colored"] } ignore = "0.4.22" -# memfd could be added but brakes compatibility with kernels < 3.17 -# ipc-channel = { version = "0.17", features = ["memfd"] } -ipc-channel = "0.18" +# memfd brakes compatibility with kernels < 3.17 +ipc-channel = { version = "0.18", features = ["memfd"] } itertools = "0.13.0" nix = { version = "0.29", features = ["process", "user"] } serde = { version = "1.0.200", features = ["derive"] } @@ -46,7 +46,6 @@ similar = { version = "2.5", features = ["inline"] } strum = "0.26.2" strum_macros = "0.26.2" tempfile = "3.10.1" -tera = { version = "1.19.1", default-features = false } term_size = "1.0.0-beta.2" [dev-dependencies] diff --git a/rash_core/src/bin/rash.rs b/rash_core/src/bin/rash.rs index 3adeb716..4f137f24 100644 --- a/rash_core/src/bin/rash.rs +++ b/rash_core/src/bin/rash.rs @@ -12,6 +12,7 @@ use std::path::Path; use std::process::exit; use clap::{crate_authors, crate_description, crate_version, ArgAction, Parser}; +use minijinja::context; #[macro_use] extern crate log; @@ -140,24 +141,22 @@ fn main() { }; match parse_file(&main_file, &global_params) { - Ok(tasks) => match env::load(args.environment) { - Ok(env_vars) => { - new_vars.extend(env_vars); - match Builtins::new(script_args, script_path) { - Ok(builtins) => new_vars.insert("rash", &builtins), - Err(e) => crash_error(e), - }; - trace!("Vars: {}", &new_vars.clone().into_json().to_string()); - match Context::exec(Context::new(tasks, new_vars)) { - Ok(_) => (), - Err(context_error) => match context_error.kind() { - ErrorKind::EmptyTaskStack => (), - _ => crash_error(context_error), - }, - }; - } - Err(e) => crash_error(e), - }, + Ok(tasks) => { + let env_vars = env::load(args.environment); + new_vars = context! {..new_vars, ..env_vars}; + match Builtins::new(script_args, script_path) { + Ok(builtins) => new_vars = context! {rash => &builtins, ..new_vars}, + Err(e) => crash_error(e), + }; + trace!("Vars: {}", &new_vars.clone().to_string()); + match Context::exec(Context::new(tasks, new_vars)) { + Ok(_) => (), + Err(context_error) => match context_error.kind() { + ErrorKind::EmptyTaskStack => (), + _ => crash_error(context_error), + }, + }; + } Err(e) => crash_error(e), } } diff --git a/rash_core/src/docopt/mod.rs b/rash_core/src/docopt/mod.rs index 989762c3..e9041ee7 100644 --- a/rash_core/src/docopt/mod.rs +++ b/rash_core/src/docopt/mod.rs @@ -9,17 +9,16 @@ use crate::vars::Vars; use std::collections::HashSet; +use minijinja::context; use regex::{Regex, RegexSet}; -use tera::Context; /// Parse file doc and args to return docopts variables. /// Supports help subcommand to print help and exit. pub fn parse(file: &str, args: &[&str]) -> Result { let help_msg = parse_help(file); - let mut vars = Context::new(); let usages = match parse_usage(&help_msg) { Some(usages) => usages, - None => return Ok(vars), + None => return Ok(context! {}), }; let options = options::Options::parse_doc(&help_msg, &usages)?; @@ -106,8 +105,17 @@ pub fn parse(file: &str, args: &[&str]) -> Result { }) .collect(); + let mut vars = options.initial_vars(); + // TODO: change vars.extend to a more functional approach with context! {vars, ..context!{}} // init vars - vars.extend(options.initial_vars()); + + // create lambda for extend vars + let extend_vars = |vars: Vars, x: Vars| { + context! { + ..vars, + ..x} + }; + args_defs_expand_repeatable .clone() .iter() @@ -117,24 +125,21 @@ pub fn parse(file: &str, args: &[&str]) -> Result { .iter() .enumerate() .filter_map(|(idx, arg_def)| match args_kinds[usage_idx].get(idx) { - Some(0) => Some( - Context::from_value( - match args_defs_expand_repeatable.iter().any(|args_def| { - args_def.iter().filter(|&x| x == arg_def).count() > 1 - }) { - true => json!({arg_def: 0}), - false => json!({arg_def: false}), - }, - ) - // safe unwrap: all args_kinds were previously checked - .unwrap(), - ), + Some(0) => Some(Vars::from_serialize( + match args_defs_expand_repeatable + .iter() + .any(|args_def| args_def.iter().filter(|&x| x == arg_def).count() > 1) + { + true => json!({arg_def: 0}), + false => json!({arg_def: false}), + }, + )), Some(1) | Some(2) => None, _ => unreachable!(), }) .collect::>() }) - .for_each(|x| vars.extend(x)); + .for_each(|x| vars = extend_vars(vars.clone(), x)); let vars_vec = args_defs_expand_repeatable .iter() @@ -170,19 +175,18 @@ pub fn parse(file: &str, args: &[&str]) -> Result { Error::new(ErrorKind::InvalidData, help_msg.clone()) })?; - let mut new_vars_json = vars.into_json(); + let mut new_vars_json = json! {vars.clone()}; vars_vec .into_iter() - .map(|x| x.into_json()) + .map(|x| json! {x}) .for_each(|x| merge_json(&mut new_vars_json, x)); - // safe unwrap: new_vars_json is a json object - let new_vars = Context::from_value(new_vars_json).unwrap(); - - match new_vars.get("help") { - Some(json!(true)) => Err(Error::new(ErrorKind::GracefulExit, help_msg)), - _ => match new_vars.get("options") { - Some(options) => match options.get("help") { - Some(json!(true)) => Err(Error::new(ErrorKind::GracefulExit, help_msg)), + let new_vars = Vars::from_serialize(new_vars_json); + + match new_vars.get_attr("help") { + Ok(y) if y.is_true() => Err(Error::new(ErrorKind::GracefulExit, help_msg)), + _ => match new_vars.get_attr("options") { + Ok(options) => match options.get_attr("help") { + Ok(z) if z.is_true() => Err(Error::new(ErrorKind::GracefulExit, help_msg)), _ => Ok(new_vars), }, _ => Ok(new_vars), @@ -413,7 +417,7 @@ fn parse_required(arg: &str, def: &str, defs: &[String]) -> Option { } else { json!({arg: true}) }; - Some(Context::from_value(value).unwrap()) + Some(Vars::from_serialize(value)) } else { None } @@ -427,7 +431,7 @@ fn parse_positional(arg: &str, def: &str) -> Vars { } else { json!({ key: arg }) }; - Context::from_value(value).unwrap() + Vars::from_serialize(value) } #[cfg(test)] @@ -449,14 +453,13 @@ mod tests { assert_eq!( result, - Context::from_value(json!( + Vars::from_serialize(json!( { "help": false, "install": true, "update": false, "package_filters": vec!["foo"], })) - .unwrap() ); let args = vec!["install", "foo", "boo"]; @@ -464,14 +467,13 @@ mod tests { assert_eq!( result, - Context::from_value(json!( + Vars::from_serialize(json!( { "help": false, "install": true, "update": false, "package_filters": vec!["foo", "boo"], })) - .unwrap() ) } @@ -490,11 +492,10 @@ mod tests { let result = parse(file, &args).unwrap(); assert_eq!( result, - Context::from_value(json!({ + Vars::from_serialize(json!({ "source": ["foo", "boo"], "dest": "/tmp", })) - .unwrap() ) } @@ -512,11 +513,10 @@ mod tests { let result = parse(file, &args).unwrap(); assert_eq!( result, - Context::from_value(json!({ + Vars::from_serialize(json!({ "a": ["a", "c"], "b": ["b", "d"], })) - .unwrap() ) } @@ -571,12 +571,11 @@ mod tests { let result = parse(file, &args).unwrap(); assert_eq!( result, - Context::from_value(json!({ + Vars::from_serialize(json!({ "options": { "d": true, } })) - .unwrap() ) } @@ -595,14 +594,13 @@ mod tests { assert_eq!( result, - Context::from_value(json!( + Vars::from_serialize(json!( { "help": false, "install": true, "update": false, "package_filters": vec!["foo", "boo"], })) - .unwrap() ); let args = vec!["install"]; @@ -626,12 +624,11 @@ mod tests { assert_eq!( result, - Context::from_value(json!( + Vars::from_serialize(json!( { "a": 2, "b": 0, })) - .unwrap() ) } @@ -649,13 +646,12 @@ mod tests { assert_eq!( result, - Context::from_value(json!( + Vars::from_serialize(json!( { "options": { "d": 0, }, })) - .unwrap() ); let args = vec!["-d"]; @@ -663,13 +659,12 @@ mod tests { assert_eq!( result, - Context::from_value(json!( + Vars::from_serialize(json!( { "options": { "d": 1, }, })) - .unwrap() ); let args = vec!["-dd"]; @@ -677,13 +672,12 @@ mod tests { assert_eq!( result, - Context::from_value(json!( + Vars::from_serialize(json!( { "options": { "d": 2, }, })) - .unwrap() ); let args = vec!["-d", "-d"]; @@ -691,13 +685,12 @@ mod tests { assert_eq!( result, - Context::from_value(json!( + Vars::from_serialize(json!( { "options": { "d": 2, }, })) - .unwrap() ); } @@ -712,18 +705,17 @@ mod tests { let args = vec![]; let result = parse(file, &args).unwrap(); - assert_eq!(result, Context::from_value(json!({})).unwrap()); + assert_eq!(result, Vars::from_serialize(json!({}))); let args = vec!["x"]; let result = parse(file, &args).unwrap(); assert_eq!( result, - Context::from_value(json!( + Vars::from_serialize(json!( { "d": "x", })) - .unwrap() ); } @@ -739,18 +731,17 @@ mod tests { let args = vec![]; let result = parse(file, &args).unwrap(); - assert_eq!(result, Context::from_value(json!({})).unwrap()); + assert_eq!(result, Vars::from_serialize(json!({}))); let args = vec!["x"]; let result = parse(file, &args).unwrap(); assert_eq!( result, - Context::from_value(json!( + Vars::from_serialize(json!( { "d": vec!["x"], })) - .unwrap() ); let args = vec!["x", "y"]; @@ -758,11 +749,10 @@ mod tests { assert_eq!( result, - Context::from_value(json!( + Vars::from_serialize(json!( { "d": vec!["x", "y"], })) - .unwrap() ); let file = r#" @@ -775,18 +765,17 @@ mod tests { let args = vec![]; let result = parse(file, &args).unwrap(); - assert_eq!(result, Context::from_value(json!({})).unwrap()); + assert_eq!(result, Vars::from_serialize(json!({}))); let args = vec!["x"]; let result = parse(file, &args).unwrap(); assert_eq!( result, - Context::from_value(json!( + Vars::from_serialize(json!( { "d": vec!["x"], })) - .unwrap() ); let args = vec!["x", "y"]; @@ -794,11 +783,10 @@ mod tests { assert_eq!( result, - Context::from_value(json!( + Vars::from_serialize(json!( { "d": vec!["x", "y"], })) - .unwrap() ); } @@ -833,7 +821,7 @@ mod tests { assert_eq!( result, - Context::from_value(json!( + Vars::from_serialize(json!( { "options": { "drifting": true, @@ -852,7 +840,6 @@ mod tests { "ship": false, "shoot": false })) - .unwrap() ); let args = vec!["mine", "set", "10", "50", "--speed=50"]; @@ -865,7 +852,7 @@ mod tests { assert_eq!( result, - Context::from_value(json!( + Vars::from_serialize(json!( { "options": { "drifting": false, @@ -887,7 +874,6 @@ mod tests { "ship": true, "shoot": false })) - .unwrap() ); let args = vec!["ship", "foo", "move", "2", "3", "-s20"]; @@ -895,7 +881,7 @@ mod tests { assert_eq!( result, - Context::from_value(json!( + Vars::from_serialize(json!( { "options": { "drifting": false, @@ -917,7 +903,6 @@ mod tests { "ship": true, "shoot": false })) - .unwrap() ); let args = vec!["ship", "foo", "move", "2", "3", "-s=20"]; @@ -925,7 +910,7 @@ mod tests { assert_eq!( result, - Context::from_value(json!( + Vars::from_serialize(json!( { "options": { "drifting": false, @@ -947,7 +932,6 @@ mod tests { "ship": true, "shoot": false })) - .unwrap() ); let args = vec!["ship", "foo", "move", "2", "3", "-s20", "-x"]; @@ -976,7 +960,7 @@ mod tests { assert_eq!( result, - Context::from_value(json!( + Vars::from_serialize(json!( { "options": { "dry_run": false, @@ -987,7 +971,6 @@ mod tests { "o": "yea", }, })) - .unwrap() ) } @@ -1010,7 +993,7 @@ mod tests { assert_eq!( result, - Context::from_value(json!( + Vars::from_serialize(json!( { "options": { "dry_run": false, @@ -1018,7 +1001,6 @@ mod tests { "help": false, }, })) - .unwrap() ) } @@ -1043,7 +1025,7 @@ mod tests { assert_eq!( result, - Context::from_value(json!( + Vars::from_serialize(json!( { "options": { "apply": false, @@ -1055,7 +1037,6 @@ mod tests { }, "port": "443" })) - .unwrap() ); } @@ -1437,10 +1418,9 @@ Foo: let result = parse_required(arg, arg_def, &[]).unwrap(); assert_eq!( result, - Context::from_value(json!({ + Vars::from_serialize(json!({ "foo": true, })) - .unwrap() ) } @@ -1461,10 +1441,9 @@ Foo: let result = parse_positional(arg, arg_def); assert_eq!( result, - Context::from_value(json!({ + Vars::from_serialize(json!({ "foo": "boo", })) - .unwrap() ) } @@ -1476,10 +1455,9 @@ Foo: let result = parse_positional(arg, arg_def); assert_eq!( result, - Context::from_value(json!({ + Vars::from_serialize(json!({ "foo": vec!["boo"], })) - .unwrap() ) } } diff --git a/rash_core/src/docopt/options.rs b/rash_core/src/docopt/options.rs index 9dd8b650..f13b313e 100644 --- a/rash_core/src/docopt/options.rs +++ b/rash_core/src/docopt/options.rs @@ -7,8 +7,8 @@ use std::collections::HashSet; use std::sync::LazyLock; use itertools::Itertools; +use minijinja::Value; use regex::Regex; -use tera::Context; const OPTIONS_MARK: &str = "[options]"; @@ -341,17 +341,16 @@ impl Options { OptionArg::Repeatable { .. } => json!(1), OptionArg::Simple { .. } => json!(true), }; - Some( - Context::from_value(json!( - { "options": - { option_arg.get_key_representation(): value - } - })) - .unwrap(), - ) + Some(Value::from_serialize(json!( + { "options": + { option_arg.get_key_representation(): value + } + }))) } pub fn initial_vars(&self) -> Vars { + // TODO: refactor to more functional way and remove JSON. Look for a better structure to + // store state let mut new_vars_json = json!({}); self.hash_set @@ -380,8 +379,7 @@ impl Options { }) }) .for_each(|x| merge_json(&mut new_vars_json, x)); - // safe unwrap: new_vars_json is a json object - Context::from_value(new_vars_json).unwrap() + Value::from_serialize(new_vars_json) } /// Replace options in args for standard docopt usage arguments @@ -721,12 +719,11 @@ Usage: {usage} assert_eq!( result, - Context::from_value(json!({ + Value::from_serialize(json!({ "options": { "help": true, } })) - .unwrap() ) } @@ -762,12 +759,11 @@ Usage: {usage} assert_eq!( result, - Context::from_value(json!({ + Value::from_serialize(json!({ "options": { "sorted": true, } })) - .unwrap() ); let arg_def = r"{--help#--sorted#-o=<-o>#--quiet|--verbose}"; @@ -776,12 +772,11 @@ Usage: {usage} assert_eq!( result, - Context::from_value(json!({ + Value::from_serialize(json!({ "options": { "o": "Fgwe=sad", } })) - .unwrap() ); let arg_def = r"{--help#--sorted#-o=<-o>#--quiet|--verbose}"; @@ -790,12 +785,11 @@ Usage: {usage} assert_eq!( result, - Context::from_value(json!({ + Value::from_serialize(json!({ "options": { "o": "Fgwe=sad", } })) - .unwrap() ); let arg_def = r"{--help#--sorted#-o=<-o>#--quiet|--verbose}"; diff --git a/rash_core/src/error.rs b/rash_core/src/error.rs index 836a478c..a491a763 100644 --- a/rash_core/src/error.rs +++ b/rash_core/src/error.rs @@ -4,9 +4,9 @@ use std::fmt; use std::io; use std::result; +use minijinja::Error as JinjaError; use nix::Error as NixError; use serde_yaml::Error as YamlError; -use tera::Error as TeraError; /// A specialized type `rash` operations. pub type Result = result::Result; @@ -48,7 +48,7 @@ pub enum ErrorKind { GracefulExit, /// An entity was not found, often a module. NotFound, - /// Data is invalid, often fail to render Tera. + /// Data is invalid. InvalidData, /// I/O error propagation IOError, @@ -58,8 +58,8 @@ pub enum ErrorKind { SubprocessFail, /// Task stack is empty EmptyTaskStack, - /// Tera failed to render template - TeraRenderError, + /// Jinja2 failed to render template + JinjaRenderError, /// Any `rash` error not part of this list. Other, } @@ -74,7 +74,7 @@ impl ErrorKind { ErrorKind::OmitParam => "omit param", ErrorKind::SubprocessFail => "subprocess fail", ErrorKind::EmptyTaskStack => "task stack is empty", - ErrorKind::TeraRenderError => "Tera failed to render template", + ErrorKind::JinjaRenderError => "Jinja2 failed to render template", ErrorKind::Other => "other os error", } } @@ -167,7 +167,7 @@ impl From for Error { } } -impl From for Error { +impl From for Error { /// Converts an tera::Error into an [`Error`]. /// /// This conversion allocates a new error with a custom representation of Tera error. @@ -175,10 +175,10 @@ impl From for Error { /// /// [`Error`]: ../error/struct.Error.html #[inline] - fn from(error: TeraError) -> Error { + fn from(error: JinjaError) -> Error { Error { repr: Repr::Custom(Box::new(Custom { - kind: ErrorKind::TeraRenderError, + kind: ErrorKind::JinjaRenderError, error: Box::new(error), })), } diff --git a/rash_core/src/lib.rs b/rash_core/src/lib.rs index a40290e8..1b315a94 100644 --- a/rash_core/src/lib.rs +++ b/rash_core/src/lib.rs @@ -36,7 +36,7 @@ mod tests { let context = Context::new( parse_file(file, &task::GlobalParams::default()).unwrap(), - env::load(vec![]).unwrap(), + env::load(vec![]), ); let context_error = Context::exec(context).unwrap_err(); diff --git a/rash_core/src/modules/assert.rs b/rash_core/src/modules/assert.rs index 84c9e16a..1434b2d4 100644 --- a/rash_core/src/modules/assert.rs +++ b/rash_core/src/modules/assert.rs @@ -23,7 +23,7 @@ /// ANCHOR_END: examples use crate::error::{Error, ErrorKind, Result}; use crate::modules::{parse_params, Module, ModuleResult}; -use crate::utils::tera::is_render_string; +use crate::utils::jinja2::is_render_string; use crate::vars::Vars; #[cfg(feature = "docs")] @@ -136,7 +136,7 @@ mod tests { Params { that: vec!["1 == 1".to_owned()], }, - &Vars::new(), + &Vars::from_serialize(json!({})), ) .unwrap(); } @@ -147,7 +147,7 @@ mod tests { Params { that: vec!["1 != 1".to_owned()], }, - &Vars::new(), + &Vars::from_serialize(json!({})), ) .unwrap_err(); } diff --git a/rash_core/src/modules/debug.rs b/rash_core/src/modules/debug.rs index 07df0e49..ef964695 100644 --- a/rash_core/src/modules/debug.rs +++ b/rash_core/src/modules/debug.rs @@ -27,7 +27,7 @@ /// ANCHOR_END: examples use crate::error::Result; use crate::modules::{parse_params, Module, ModuleResult}; -use crate::utils::tera::render_string; +use crate::utils::jinja2::render_string; use crate::vars::Vars; #[cfg(feature = "docs")] @@ -134,7 +134,7 @@ mod tests { #[test] fn test_debug_msg() { - let vars = Vars::new(); + let vars = Vars::UNDEFINED; let output = debug( Params { required: Required::Msg("foo boo".to_owned()), @@ -155,7 +155,7 @@ mod tests { #[test] fn test_debug_vars() { - let vars = Vars::from_value(json!({"yea": "foo"})).unwrap(); + let vars = Vars::from_serialize(json!({"yea": "foo"})); let output = debug( Params { required: Required::Var("yea".to_owned()), diff --git a/rash_core/src/modules/set_vars.rs b/rash_core/src/modules/set_vars.rs index 5183245c..787c5d74 100644 --- a/rash_core/src/modules/set_vars.rs +++ b/rash_core/src/modules/set_vars.rs @@ -41,6 +41,7 @@ use crate::error::{Error, ErrorKind, Result}; use crate::modules::{Module, ModuleResult}; use crate::vars::Vars; +use minijinja::context; #[cfg(feature = "docs")] use schemars::schema::RootSchema; use serde_yaml::Value; @@ -60,15 +61,14 @@ impl Module for SetVars { Value::Mapping(map) => { map.iter() .map(|hash_map| { - new_vars.insert( - hash_map.0.as_str().ok_or_else(|| { - Error::new( - ErrorKind::InvalidData, - format!("{:?} is not a valid string", &hash_map.0), - ) - })?, - hash_map.1, - ); + let key = hash_map.0.as_str().ok_or_else(|| { + Error::new( + ErrorKind::InvalidData, + format!("{:?} is not a valid string", &hash_map.0), + ) + })?; + let element = json!({key: hash_map.1}); + new_vars = context! {..new_vars.clone(), ..Vars::from_serialize(element)}; Ok(()) }) .collect::>>()?; diff --git a/rash_core/src/modules/template.rs b/rash_core/src/modules/template.rs index 61a161e4..8aaf44df 100644 --- a/rash_core/src/modules/template.rs +++ b/rash_core/src/modules/template.rs @@ -20,18 +20,18 @@ /// mode: "0400" /// ``` /// ANCHOR_END: examples -use crate::error::{Error, ErrorKind, Result}; +use crate::error::Result; use crate::modules::copy::copy_file; use crate::modules::copy::{Input, Params as CopyParams}; use crate::modules::{parse_params, Module, ModuleResult}; +use crate::utils::jinja2::render_string; use crate::vars::Vars; #[cfg(feature = "docs")] use rash_derive::DocJsonSchema; -use std::fs::metadata; +use std::fs::{metadata, read_to_string}; use std::os::unix::fs::PermissionsExt; -use std::path::Path; #[cfg(feature = "docs")] use schemars::schema::RootSchema; @@ -39,13 +39,12 @@ use schemars::schema::RootSchema; use schemars::JsonSchema; use serde::Deserialize; use serde_yaml::Value; -use tera::Tera; #[derive(Debug, PartialEq, Deserialize)] #[cfg_attr(feature = "docs", derive(JsonSchema, DocJsonSchema))] #[serde(deny_unknown_fields)] pub struct Params { - /// Path of Tera formatted template. + /// Path of Jinja formatted template. /// This can be a relative or an absolute path. src: String, /// Absolute path where the file should be rendered to. @@ -57,9 +56,6 @@ pub struct Params { } fn render_content(params: Params, vars: Vars) -> Result { - let mut tera = Tera::default(); - tera.add_template_file(Path::new(¶ms.src), None) - .map_err(|e| Error::new(ErrorKind::InvalidData, e))?; let mode = match params.mode.as_deref() { Some("preserve") => { let src_metadata = metadata(¶ms.src)?; @@ -71,10 +67,7 @@ fn render_content(params: Params, vars: Vars) -> Result { }; Ok(CopyParams { - input: Input::Content( - tera.render(¶ms.src, &vars) - .map_err(|e| Error::new(ErrorKind::InvalidData, e))?, - ), + input: Input::Content(render_string(&read_to_string(params.src)?, &vars)?), dest: params.dest.clone(), mode, }) @@ -113,11 +106,12 @@ impl Module for Template { mod tests { use super::*; - use crate::vars; + use crate::error::ErrorKind; use std::fs::{set_permissions, File}; use std::io::Write; + use minijinja::context; use tempfile::tempdir; #[test] @@ -205,7 +199,7 @@ mod tests { #[allow(clippy::write_literal)] writeln!(file, "{}", "{{ boo }}").unwrap(); - let vars = vars::from_iter(vec![("boo", "test")].into_iter()); + let vars = context! { boo => "test" }; let copy_params = render_content( Params { @@ -240,7 +234,7 @@ mod tests { permissions.set_mode(0o604); set_permissions(&file_path, permissions).unwrap(); - let vars = vars::from_iter(vec![("boo", "test")].into_iter()); + let vars = Vars::from_serialize(context! { boo => "test" }); let copy_params = render_content( Params { diff --git a/rash_core/src/task/mod.rs b/rash_core/src/task/mod.rs index 6c7177a0..84cefc20 100644 --- a/rash_core/src/task/mod.rs +++ b/rash_core/src/task/mod.rs @@ -4,7 +4,7 @@ mod valid; use crate::error::{Error, ErrorKind, Result}; use crate::modules::{Module, ModuleResult}; use crate::task::new::TaskNew; -use crate::utils::tera::{is_render_string, render, render_as_json, render_string}; +use crate::utils::jinja2::{is_render_string, render, render_as_json, render_string}; use crate::vars::Vars; use rash_derive::FieldNames; @@ -14,6 +14,7 @@ use std::result::Result as StdResult; use ipc_channel::ipc::IpcReceiver; use ipc_channel::ipc::{self, IpcSender}; +use minijinja::context; use nix::sys::wait::{waitpid, WaitStatus}; use nix::unistd::{fork, setgid, setuid, ForkResult, Uid, User}; use serde_error::Error as SerdeError; @@ -108,15 +109,15 @@ impl Task { } #[inline(always)] - fn extend_vars(&self, vars: Vars) -> Result { + fn extend_vars(&self, additional_vars: Vars) -> Result { match self.vars.clone() { Some(v) => { - let mut e_vars = vars.clone(); trace!("extend vars: {:?}", &v); - e_vars.extend(Vars::from_serialize(render(v, &vars)?)?); - Ok(e_vars) + Ok( + context! { ..Vars::from_serialize(render(v.clone(), &additional_vars)?), ..Vars::from_serialize(additional_vars)}, + ) } - None => Ok(vars), + None => Ok(additional_vars), } } @@ -241,11 +242,12 @@ impl Task { || "".to_owned() ) ); - let mut new_vars = result_vars; + let mut new_vars = context! {..result_vars}; if self.register.is_some() { let register = self.register.as_ref().unwrap(); trace!("register {:?} in {:?}", &result, register); - new_vars.insert(register, &result); + new_vars = + context! { ..Vars::from_serialize(json!({register: &result})), ..new_vars}; } Ok(new_vars) } @@ -314,7 +316,7 @@ impl Task { trace!("send result: {:?}", result); tx.send( result - .map(|x| x.into_json().to_string()) + .map(|x| x.to_string()) .map_err(|e| SerdeError::new(&e)), ) .unwrap_or_else(|e| { @@ -344,13 +346,7 @@ impl Task { format!("{e:?}"), ))) }) - .map(|x| { - // safe unwrap: this value comes from vars.into_json() - tera::Context::from_value(serde_json::from_str(&x).unwrap()) - // safe unwrap: json is object because comes from - // child tera::Context - .unwrap() - }) + .map(|x| Vars::from_serialize(&x)) .map_err(|e| Error::new(ErrorKind::Other, e)) } Err(e) => Err(Error::new(ErrorKind::Other, e)), @@ -376,15 +372,15 @@ impl Task { debug!("Params: {:?}", self.params); if self.r#loop.is_some() { - let mut new_vars = vars.clone(); + // TODO: remove unnecessary movements + let mut ctx = vars.clone(); for item in self.render_iterator(vars)?.into_iter() { - new_vars.insert("item", &item); - new_vars = self.exec_module(new_vars.clone())?; + let new_ctx = context! {item => &item, ..ctx.clone()}; + ctx = self.exec_module(new_ctx)?; } - Ok(new_vars) + Ok(ctx) } else { - let new_vars = self.exec_module(vars)?; - Ok(new_vars) + Ok(self.exec_module(vars)?) } } @@ -437,11 +433,9 @@ pub fn parse_file(tasks_file: &str, global_params: &GlobalParams) -> Result "test"}); let yaml: Value = serde_yaml::from_str(&s).unwrap(); let task = Task::from(yaml); assert!(task.is_exec(&vars).unwrap()); @@ -502,7 +496,7 @@ mod tests { command: 'example' "# .to_owned(); - let vars = vars::from_iter(vec![("boo", "false")].into_iter()); + let vars = Vars::from_serialize(vec![("boo", "false")]); let yaml: Value = serde_yaml::from_str(&s).unwrap(); let task = Task::from(yaml); assert!(!task.is_exec(&vars).unwrap()); @@ -515,7 +509,7 @@ mod tests { command: 'example' "# .to_owned(); - let vars = vars::from_iter(vec![("boo", "test")].into_iter()); + let vars = Vars::from_serialize(context! { boo => "test"}); let yaml: Value = serde_yaml::from_str(&s).unwrap(); let task = Task::from(yaml); assert!(!task.is_exec(&vars).unwrap()); @@ -528,7 +522,7 @@ mod tests { command: 'example' "# .to_owned(); - let vars = vars::from_iter(vec![].into_iter()); + let vars = context! {}; let yaml: Value = serde_yaml::from_str(&s).unwrap(); let task = Task::from(yaml); assert!(!task.is_exec(&vars).unwrap()); @@ -543,7 +537,7 @@ mod tests { command: 'example' "# .to_owned(); - let vars = vars::from_iter(vec![("boo", "test")].into_iter()); + let vars = Vars::from_serialize(context! { boo => "test"}); let yaml: Value = serde_yaml::from_str(&s).unwrap(); let task = Task::from(yaml); assert!(task.is_exec(&vars).unwrap()); @@ -558,27 +552,65 @@ mod tests { command: 'example' "# .to_owned(); - let vars = vars::from_iter(vec![("boo", "test")].into_iter()); + let vars = Vars::from_serialize(context! { boo => "test"}); + let yaml: Value = serde_yaml::from_str(&s).unwrap(); + let task = Task::from(yaml); + assert!(!task.is_exec(&vars).unwrap()); + + let s: String = r#" + command: 'example' + when: + - true + - false + - true + "# + .to_owned(); + let vars = Vars::from_serialize(context! {}); + let yaml: Value = serde_yaml::from_str(&s).unwrap(); + let task = Task::from(yaml); + assert!(!task.is_exec(&vars).unwrap()); + + let s: String = r#" + command: 'example' + when: + - true + - true + - true + "# + .to_owned(); + let vars = Vars::from_serialize(context! {}); + let yaml: Value = serde_yaml::from_str(&s).unwrap(); + let task = Task::from(yaml); + assert!(task.is_exec(&vars).unwrap()); + + let s: String = r#" + command: 'example' + when: + - true or true or true + - false + - true + "# + .to_owned(); + let vars = Vars::from_serialize(context! {}); let yaml: Value = serde_yaml::from_str(&s).unwrap(); let task = Task::from(yaml); assert!(!task.is_exec(&vars).unwrap()); } - // tera v2 will fix this - // #[test] - // fn test_is_exec_array_with_or_operator() { - // let s: String = r#" - // when: - // - true - // - "boo == 'test'" or false - // command: 'example' - // "# - // .to_owned(); - // let vars = vars::from_iter(vec![("boo", "test")].into_iter()); - // let yaml: Value = serde_yaml::from_str(&s).unwrap(); - // let task = Task::from(yaml); - // assert!(task.is_exec(&vars).unwrap()); - // } + #[test] + fn test_is_exec_array_with_or_operator() { + let s: String = r#" + command: 'example' + when: + - true + - boo == 'test' or false + "# + .to_owned(); + let vars = Vars::from_serialize(context! { boo => "test"}); + let yaml: Value = serde_yaml::from_str(&s).unwrap(); + let task = Task::from(yaml); + assert!(task.is_exec(&vars).unwrap()); + } #[test] fn test_render_iterator() { @@ -590,7 +622,7 @@ mod tests { - 3 "# .to_owned(); - let vars = vars::from_iter(vec![].into_iter()); + let vars = context! {}; let yaml: Value = serde_yaml::from_str(&s).unwrap(); let task = Task::from(yaml); assert_eq!( @@ -606,7 +638,7 @@ mod tests { command: 'example' "# .to_owned(); - let vars = vars::from_iter(vec![("boo", "test")].into_iter()); + let vars = Vars::from_serialize(context! { boo => "test" }); let yaml: Value = serde_yaml::from_str(&s).unwrap(); let task = Task::from(yaml); assert!(task @@ -621,7 +653,7 @@ mod tests { command: 'example' "# .to_owned(); - let vars = vars::from_iter(vec![("boo", "test")].into_iter()); + let vars = Vars::from_serialize(context! { boo => "test"}); let yaml: Value = serde_yaml::from_str(&s).unwrap(); let task = Task::from(yaml); assert!(task @@ -636,7 +668,7 @@ mod tests { command: 'example' "# .to_owned(); - let vars = vars::from_iter(vec![("boo", "test")].into_iter()); + let vars = Vars::from_serialize(context! { boo => "test"}); let yaml: Value = serde_yaml::from_str(&s).unwrap(); let task = Task::from(yaml); assert!(!task @@ -651,7 +683,7 @@ mod tests { command: 'example' "# .to_owned(); - let vars = vars::from_iter(vec![("boo", "test")].into_iter()); + let vars = Vars::from_serialize(context! { boo => "test"}); let yaml: Value = serde_yaml::from_str(&s).unwrap(); let task = Task::from(yaml); assert!(!task @@ -666,7 +698,7 @@ mod tests { command: 'example' "# .to_owned(); - let vars = vars::from_iter(vec![("boo", "test")].into_iter()); + let vars = Vars::from_serialize(context! { boo => "test"}); let yaml: Value = serde_yaml::from_str(&s).unwrap(); let task = Task::from(yaml); assert!(!task @@ -683,7 +715,7 @@ mod tests { command: 'example' "# .to_owned(); - let vars = vars::from_iter(vec![("boo", "test")].into_iter()); + let vars = Vars::from_serialize(context! { boo => "test"}); let yaml: Value = serde_yaml::from_str(&s).unwrap(); let task = Task::from(yaml); assert!(task @@ -700,7 +732,7 @@ mod tests { command: 'example' "# .to_owned(); - let vars = vars::from_iter(vec![("boo", "test")].into_iter()); + let vars = Vars::from_serialize(context! { boo => "test"}); let yaml: Value = serde_yaml::from_str(&s).unwrap(); let task = Task::from(yaml); assert!(!task @@ -719,12 +751,13 @@ mod tests { when: item == 1 "# .to_owned(); - let vars = vars::from_iter(vec![].into_iter()); + let vars = context! {}; let yaml: Value = serde_yaml::from_str(&s).unwrap(); let task = Task::from(yaml); let result = task.exec(vars).unwrap(); - let mut expected = Context::new(); - expected.insert("item", &json!(3)); + let expected = context! { + item => 3, + }; assert_eq!(result, expected); } @@ -732,10 +765,10 @@ mod tests { fn test_render_iterator_var() { let s: String = r#" command: 'example' - loop: "{{ range(end=3) }}" + loop: "{{ range(3) }}" "# .to_owned(); - let vars = vars::from_iter(vec![("boo", "test")].into_iter()); + let vars = Vars::from_serialize(context! { boo => "test"}); let yaml: Value = serde_yaml::from_str(&s).unwrap(); let task = Task::from(yaml); assert_eq!( @@ -753,7 +786,7 @@ mod tests { - 2 "# .to_owned(); - let vars = vars::from_iter(vec![("boo", "test")].into_iter()); + let vars = Vars::from_serialize(context! { boo => "test"}); let yaml: Value = serde_yaml::from_str(&s).unwrap(); let task = Task::from(yaml); assert_eq!( @@ -772,11 +805,58 @@ mod tests { let yaml: Value = serde_yaml::from_str(&s0).unwrap(); let task = Task::from(yaml); - let vars = vars::from_iter(vec![].into_iter()); + let vars = context! {}; let result = task.exec(vars.clone()).unwrap(); assert_eq!(result, vars); } + #[test] + fn test_task_execute_keep_vars() { + let s0 = r#" + name: task 1 + command: echo foo + "# + .to_owned(); + let yaml: Value = serde_yaml::from_str(&s0).unwrap(); + let task = Task::from(yaml); + + let vars = context! {buu => "boo"}; + let result = task.exec(vars.clone()).unwrap(); + assert_eq!(result, vars); + + let s0 = r#" + name: task 1 + debug: + msg: "foo" + "# + .to_owned(); + let yaml: Value = serde_yaml::from_str(&s0).unwrap(); + let task = Task::from(yaml); + + let vars = context! {buu => "boo"}; + let result = task.exec(vars.clone()).unwrap(); + assert_eq!(result, vars); + } + + #[test] + fn test_task_execute_register() { + let s0 = r#" + name: task 1 + command: echo foo + register: yea + "# + .to_owned(); + let yaml: Value = serde_yaml::from_str(&s0).unwrap(); + let task = Task::from(yaml); + + let vars = context! {}; + let result = task.exec(vars.clone()).unwrap(); + assert!(result.get_attr("yea").map(|x| !x.is_undefined()).unwrap()); + } + + // TODO: check item is removed from vars after task loop execution + // check behaviour in Ansible + #[test] fn test_read_tasks() { let file = r#" @@ -833,8 +913,7 @@ mod tests { .cloned() .map(|(k, v)| (k.to_owned(), v.to_owned())) .collect::>(), - ) - .unwrap(); + ); let rendered_params = task.render_params(vars).unwrap(); assert_eq!(rendered_params["cmd"].as_str().unwrap(), "ls boo"); @@ -852,7 +931,7 @@ mod tests { .to_owned(); let yaml: Value = serde_yaml::from_str(&s0).unwrap(); let task = Task::from(yaml); - let vars = Vars::from_value(json!({})).unwrap(); + let vars = context! {}; let rendered_params = task.render_params(vars).unwrap(); assert_eq!(rendered_params["cmd"].as_str().unwrap(), "ls boo"); @@ -870,14 +949,10 @@ mod tests { .to_owned(); let yaml: Value = serde_yaml::from_str(&s0).unwrap(); let task = Task::from(yaml); - let vars = Vars::from_serialize( - [("directory", "boo"), ("xuu", "zoo")] - .iter() - .cloned() - .map(|(k, v)| (k.to_owned(), v.to_owned())) - .collect::>(), - ) - .unwrap(); + let vars = context! { + directory => "boo", + xuu => "zoo", + }; let rendered_params = task.render_params(vars).unwrap(); assert_eq!(rendered_params["cmd"].as_str().unwrap(), "ls boo"); @@ -896,17 +971,13 @@ mod tests { .to_owned(); let yaml: Value = serde_yaml::from_str(&s0).unwrap(); let task = Task::from(yaml); - let vars = Vars::from_serialize( - [("directory", "boo"), ("xuu", "zoo")] - .iter() - .cloned() - .map(|(k, v)| (k.to_owned(), v.to_owned())) - .collect::>(), - ) - .unwrap(); + let vars = context! { + directory => "boo", + xuu => "zoo", + }; let rendered_params_err = task.render_params(vars).unwrap_err(); - assert_eq!(rendered_params_err.kind(), ErrorKind::TeraRenderError); + assert_eq!(rendered_params_err.kind(), ErrorKind::JinjaRenderError); } #[test] @@ -921,10 +992,10 @@ mod tests { .to_owned(); let yaml: Value = serde_yaml::from_str(&s0).unwrap(); let task = Task::from(yaml); - let vars = Vars::from_value(json!({})).unwrap(); + let vars = context! {}; let rendered_params_err = task.render_params(vars).unwrap_err(); - assert_eq!(rendered_params_err.kind(), ErrorKind::TeraRenderError); + assert_eq!(rendered_params_err.kind(), ErrorKind::JinjaRenderError); } #[test] @@ -936,14 +1007,10 @@ mod tests { .to_owned(); let yaml: Value = serde_yaml::from_str(&s0).unwrap(); let task = Task::from(yaml); - let vars = Vars::from_serialize( - [("directory", "boo"), ("xuu", "zoo")] - .iter() - .cloned() - .map(|(k, v)| (k.to_owned(), v.to_owned())) - .collect::>(), - ) - .unwrap(); + let vars = context! { + directory => "boo", + xuu => "zoo", + }; let rendered_params = task.render_params(vars).unwrap(); assert_eq!(rendered_params.as_str().unwrap(), "ls boo"); diff --git a/rash_core/src/task/valid.rs b/rash_core/src/task/valid.rs index f0524c54..4b43ae31 100644 --- a/rash_core/src/task/valid.rs +++ b/rash_core/src/task/valid.rs @@ -62,10 +62,9 @@ impl TaskValid { v.iter() .map(|x| self.parse_bool_or_string(x)) .collect::>>()? - // tera v2 will fix this allowing ({}) - // .iter() - // .map(|s| format!("({})", s)) - // .collect::>() + .iter() + .map(|s| format!("({})", s)) + .collect::>() .join(" and "), ), None => self.parse_bool_or_string(attr), diff --git a/rash_core/src/utils/jinja2.rs b/rash_core/src/utils/jinja2.rs new file mode 100644 index 00000000..0e6e394a --- /dev/null +++ b/rash_core/src/utils/jinja2.rs @@ -0,0 +1,139 @@ +use crate::error; +use crate::error::{Error, ErrorKind, Result}; +use crate::vars::Vars; + +use std::result::Result as StdResult; +use std::sync::LazyLock; + +use minijinja::{ + Environment, Error as MinijinjaError, ErrorKind as MinijinjaErrorKind, UndefinedBehavior, +}; +use serde_yaml::value::Value; + +const OMIT_MESSAGE: &str = "Param is omitted"; + +fn omit() -> StdResult { + Err(MinijinjaError::new( + MinijinjaErrorKind::InvalidOperation, + OMIT_MESSAGE, + )) +} + +fn init_env() -> Environment<'static> { + let mut env = Environment::new(); + env.set_keep_trailing_newline(true); + env.set_undefined_behavior(UndefinedBehavior::Strict); + env.add_function("omit", omit); + env +} + +static MINIJINJA_ENV: LazyLock> = LazyLock::new(init_env); + +#[inline(always)] +pub fn render(value: Value, vars: &Vars) -> Result { + match value { + Value::String(s) => Ok(Value::String(render_string(&s, vars)?)), + Value::Number(_) => Ok(value), + Value::Bool(_) => Ok(value), + Value::Sequence(v) => Ok(Value::Sequence( + v.iter() + .map(|x| render(x.clone(), vars)) + .collect::>>()?, + )), + Value::Mapping(x) => Ok(Value::Mapping( + x.iter() + .map(|t| render((*t.1).clone(), vars).map(|value| ((*t.0).clone(), value))) + .collect::>()?, + )), + _ => Err(Error::new( + ErrorKind::InvalidData, + format!("{value:?} is not a valid render value"), + )), + } +} + +// TODO: remove borrowing if possible +#[inline(always)] +pub fn render_string(s: &str, vars: &Vars) -> Result { + let mut env = MINIJINJA_ENV.clone(); + trace!("rendering {:?}", &s); + env.add_template("t", s)?; + let tmpl = env.get_template("t").map_err(map_minijinja_error)?; + tmpl.render(vars).map_err(map_minijinja_error) +} + +// TODO: check this +#[inline(always)] +pub fn render_as_json(s: &str, vars: &Vars) -> Result { + render_string(s, vars) +} + +// TODO: use minijinja compile_expression +#[inline(always)] +pub fn is_render_string(s: &str, vars: &Vars) -> Result { + match render_string( + &format!("{{% if {s} %}}true{{% else %}}false{{% endif %}}"), + vars, + )? + .as_str() + { + "false" => Ok(false), + _ => Ok(true), + } +} + +fn map_minijinja_error(e: MinijinjaError) -> Error { + let f = |e: &MinijinjaError| -> Option { Some(e.detail()? == OMIT_MESSAGE) }; + match f(&e) { + Some(true) => error::Error::new(error::ErrorKind::OmitParam, OMIT_MESSAGE), + _ => error::Error::from(e), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use minijinja::context; + + #[test] + fn test_render() { + let r_yaml = render(Value::from(1), &context! {}).unwrap(); + assert_eq!(r_yaml, Value::from(1)); + + let r_yaml = render(Value::from("yea"), &context! {}).unwrap(); + assert_eq!(r_yaml, Value::from("yea")); + } + + #[test] + fn test_render_string() { + let r_yaml = render_string("{{ yea }}", &context! {yea => 1}).unwrap(); + assert_eq!(r_yaml, "1"); + + let r_yaml = render_string("{{ yea }} ", &context! {yea => 1}).unwrap(); + assert_eq!(r_yaml, "1 "); + + let r_yaml = render_string(" {{ yea }}", &context! {yea => 1}).unwrap(); + assert_eq!(r_yaml, " 1"); + + let r_yaml = render_string("{{ yea }}\n", &context! {yea => 1}).unwrap(); + assert_eq!(r_yaml, "1\n"); + } + + #[test] + fn test_is_render_string() { + let r_true = is_render_string("true", &context! {}).unwrap(); + assert!(r_true); + let r_false = is_render_string("false", &context! {}).unwrap(); + assert!(!r_false); + let r_true = is_render_string("boo == 'test'", &context! {boo => "test"}).unwrap(); + assert!(r_true); + } + + #[test] + fn test_render_string_omit() { + let string = "{{ package_filters | default(value=omit()) }}"; + let e = render_string(string, &context! {}).unwrap_err(); + assert_eq!(e.kind(), error::ErrorKind::OmitParam) + } +} diff --git a/rash_core/src/utils/mod.rs b/rash_core/src/utils/mod.rs index a1be4d65..fcb1da14 100644 --- a/rash_core/src/utils/mod.rs +++ b/rash_core/src/utils/mod.rs @@ -1,4 +1,4 @@ -pub mod tera; +pub mod jinja2; use crate::error::{Error, ErrorKind, Result}; diff --git a/rash_core/src/utils/tera.rs b/rash_core/src/utils/tera.rs deleted file mode 100644 index bcbb2497..00000000 --- a/rash_core/src/utils/tera.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::error; -use crate::error::{Error, ErrorKind, Result}; -use crate::vars::Vars; - -use std::collections::HashMap; -use std::error::Error as StdError; -use std::sync::LazyLock; - -use serde_yaml::value::Value; -use tera::Tera; - -fn omit(_: &HashMap) -> tera::Result { - Err(tera::Error::call_filter( - "omit", - tera::Error::msg("Not defined"), - )) -} - -fn init_tera() -> Tera { - let mut tera = Tera::default(); - tera.register_function("omit", omit); - tera -} - -static TERA: LazyLock = LazyLock::new(init_tera); - -#[inline(always)] -pub fn render(value: Value, vars: &Vars) -> Result { - match value { - Value::String(s) => Ok(Value::String(render_string(&s, vars)?)), - Value::Number(_) => Ok(value), - Value::Bool(_) => Ok(value), - Value::Sequence(v) => Ok(Value::Sequence( - v.iter() - .map(|x| render(x.clone(), vars)) - .collect::>>()?, - )), - Value::Mapping(x) => Ok(Value::Mapping( - x.iter() - .map(|t| render((*t.1).clone(), vars).map(|value| ((*t.0).clone(), value))) - .collect::>()?, - )), - _ => Err(Error::new( - ErrorKind::InvalidData, - format!("{value:?} is not a valid render value"), - )), - } -} - -#[inline(always)] -pub fn render_string(s: &str, vars: &Vars) -> Result { - let mut tera = TERA.clone(); - trace!("rendering {:?}", &s); - tera.render_str(s, vars).map_err(|e| { - let f = |e: &dyn StdError| -> Option { - Some(e.source()?.source()?.source()?.to_string() == "Not defined") - }; - match f(&e) { - Some(true) => error::Error::new(error::ErrorKind::OmitParam, "Param is omitted"), - _ => error::Error::from(e), - } - }) -} - -#[inline(always)] -pub fn render_as_json(s: &str, vars: &Vars) -> Result { - render_string(&s.replace("}}", "| json_encode() | safe }}"), vars) -} - -#[inline(always)] -pub fn is_render_string(s: &str, vars: &Vars) -> Result { - match render_string( - // tera v2 will fix this allowing ({}) - &format!("{{% if {s} | safe %}}true{{% else %}}false{{% endif %}}"), - vars, - )? - .as_str() - { - "false" => Ok(false), - _ => Ok(true), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_render() { - let r_yaml = render(Value::from(1), &Vars::new()).unwrap(); - assert_eq!(r_yaml, Value::from(1)); - - let r_yaml = render(Value::from("yea"), &Vars::new()).unwrap(); - assert_eq!(r_yaml, Value::from("yea")); - } - - #[test] - fn test_is_render_string() { - let r_true = is_render_string("true", &Vars::new()).unwrap(); - assert!(r_true); - let r_false = is_render_string("false", &Vars::new()).unwrap(); - assert!(!r_false); - } - - #[test] - fn test_render_string_omit() { - let string = "{{ package_filters | default(value=omit()) }}"; - let e = render_string(string, &Vars::new()).unwrap_err(); - assert_eq!(e.kind(), error::ErrorKind::OmitParam) - } -} diff --git a/rash_core/src/vars/env.rs b/rash_core/src/vars/env.rs index 5796b92f..ea2aa729 100644 --- a/rash_core/src/vars/env.rs +++ b/rash_core/src/vars/env.rs @@ -1,11 +1,9 @@ -use crate::error::{Error, ErrorKind, Result}; use crate::vars::Vars; use std::collections::HashMap; use std::env; use serde::Serialize; -use tera::Context; #[derive(Serialize)] struct Env { @@ -31,13 +29,12 @@ impl From for Env { /// /// use std::env; /// -/// let vars = load(vec![("foo".to_owned(), "boo".to_owned())]).unwrap(); +/// let vars = load(vec![("foo".to_owned(), "boo".to_owned())]); /// ``` -pub fn load(envars: Vec<(String, String)>) -> Result { +pub fn load(envars: Vec<(String, String)>) -> Vars { trace!("{:?}", envars); envars.into_iter().for_each(|(k, v)| env::set_var(k, v)); - Context::from_serialize(Env::from(env::vars())) - .map_err(|e| Error::new(ErrorKind::InvalidData, e)) + Vars::from_serialize(Env::from(env::vars())) } #[cfg(test)] @@ -55,23 +52,24 @@ mod tests { #[test] fn test_inventory_from_envars() { run_test_with_envar(("KEY", "VALUE"), || { - let json = load(vec![]).unwrap().into_json(); - let result = json.get("env").unwrap().get("KEY").unwrap(); + let vars = load(vec![]); + let result = vars.get_attr("env").unwrap().get_attr("KEY").unwrap(); - assert_eq!(result, "VALUE"); + assert_eq!(result.to_string(), "VALUE"); }); } #[test] fn test_inventory_from_envars_none() { run_test_with_envar(("KEY_NOT_FOUND", "VALUE"), || { - let vars = load(vec![]).unwrap(); - assert!(vars - .into_json() - .get("env") - .unwrap() - .get("key_not_found") - .is_none()); + let vars = load(vec![]); + assert_eq!( + vars.get_attr("env") + .unwrap() + .get_attr("key_not_found") + .unwrap(), + Vars::UNDEFINED + ); }); } } diff --git a/rash_core/src/vars/mod.rs b/rash_core/src/vars/mod.rs index c4c6c0a8..76d9ab9c 100644 --- a/rash_core/src/vars/mod.rs +++ b/rash_core/src/vars/mod.rs @@ -1,25 +1,10 @@ +// TODO: remove this file pub mod builtin; pub mod env; -use tera::Context; +use minijinja::Value; -/// Variables stored and accessible during execution, based on [`tera::Context`] +/// Variables stored and accessible during execution, based on [`minijinja::Value`] /// -/// [`tera::Context`]: ../../tera/struct.Context.html -pub type Vars = Context; - -#[cfg(test)] -use std::collections::HashMap; - -#[cfg(test)] -pub fn from_iter<'a, I>(iterable: I) -> Vars -where - I: Iterator, -{ - Context::from_serialize( - iterable - .map(|(k, v)| (k.to_owned(), v.to_owned())) - .collect::>(), - ) - .unwrap() -} +/// [`minijinja::Value`]: ../../minijinja/macro.context.html +pub type Vars = Value; diff --git a/rash_core/tests/mocks/pacman.rh b/rash_core/tests/mocks/pacman.rh index 220c639e..d5139d37 100755 --- a/rash_core/tests/mocks/pacman.rh +++ b/rash_core/tests/mocks/pacman.rh @@ -33,6 +33,7 @@ # -w, --downloadonly download packages but do not install/upgrade anything # -y, --refresh download fresh package databases from the server # (-yy to force a refresh even if up to date) +# --deps list packages installed as dependencies [filter] # --arch set an alternate architecture # --asdeps install packages as non-explicitly installed # --asexplicit install packages as explicitly installed @@ -71,7 +72,7 @@ resolving dependencies... looking for conflicting packages... - Packages (1) {{ targets | join(sep=' ') }} + Packages (1) {{ targets | join(' ') }} Total Installed Size: 21.73 MiB Net Upgrade Size: 0.00 MiB @@ -100,7 +101,7 @@ resolving dependencies... looking for conflicting packages... - Packages (1) {{ targets | join(sep=' ') }} + Packages (1) {{ targets | join(' ') }} Total Installed Size: 21.73 MiB Net Upgrade Size: 0.00 MiB @@ -127,11 +128,9 @@ - name: Install debug: msg: "{{ item }}" - loop: "{{ targets | default (value=[]) }}" - vars: - a: "{{ options.upgrade or options.sync }}" + loop: "{{ targets | default([]) }}" when: - - a == "true" + - options.upgrade or options.sync - options.noconfirm - options.print_format == "%n" @@ -166,7 +165,7 @@ msg: |- checking dependencies... - Packages (1) {{ targets | join(sep=' ') }} + Packages (1) {{ targets | join(' ') }} Total Removed Size: 21.73 MiB @@ -230,7 +229,7 @@ - name: Packages to set reason debug: msg: "{{ item }}: install reason has been set to 'dependency installed'" - loop: "{{ targets | default (value=[]) }}" + loop: "{{ targets | default([]) }}" when: - options.database - options.asdeps @@ -238,7 +237,7 @@ - name: Packages to set reason debug: msg: "{{ item }}: install reason has been set to 'explicity installed'" - loop: "{{ targets | default (value=[]) }}" + loop: "{{ targets | default([]) }}" when: - options.database - options.asexplicit @@ -250,12 +249,9 @@ - -c - 'echo "error: no targets specified (use -h for help)" && exit 1' transfer_pid: true - # workaround until tera v2 - vars: - a: "{{ options.upgrade or options.sync or options.remove or options.database }}" - b: "{{ targets is undefined or targets | length == 0 }}" when: - - a == "true" and b == "true" + - options.upgrade or options.sync or options.remove or options.database + - targets | default([]) | length == 0 - not options.sysupgrade and not options.list - name: List packages explicitly installed diff --git a/rash_core/tests/modules/pacman.rs b/rash_core/tests/modules/pacman.rs index 66d73f00..1eeb8799 100644 --- a/rash_core/tests/modules/pacman.rs +++ b/rash_core/tests/modules/pacman.rs @@ -180,17 +180,16 @@ fn test_pacman_result_extra() { state: sync register: packages - debug: - msg: "{{{{ packages.extra | json_encode }}}}" + msg: "{{{{ packages.extra }}}}" "#, mocks_dir.to_str().unwrap() ); - let args = ["--output", "raw"]; let (stdout, stderr) = run_test(&script_text, &args); assert!(stderr.is_empty()); assert_eq!( - stdout.lines().last().unwrap(), + stdout.lines().last().unwrap().replace(' ', ""), serde_json::to_string(&json!({ "installed_packages": ["rash"], "removed_packages": ["linux61-zfs"],