From e1bbaeb224780e71c81d5a0e5feb8a2f6eb84507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Einar=20S=2E=20Ids=C3=B8?= <2356425+einarsi@users.noreply.github.com> Date: Sun, 25 Feb 2024 16:26:47 +0100 Subject: [PATCH] feat: add filter to unpack values from maps (#216) --- docs/Howto_SCG.md | 77 +++++++++++++++++----- src/renderer.rs | 160 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 219 insertions(+), 18 deletions(-) diff --git a/docs/Howto_SCG.md b/docs/Howto_SCG.md index d6d130c6..d3dd4c70 100644 --- a/docs/Howto_SCG.md +++ b/docs/Howto_SCG.md @@ -23,7 +23,14 @@ using 1.0 to 2.x, expect having to change a few lines in your templates and YAML - [CSV source](#csv-source) - [Command-line options](#command-line-options) - [The template engine](#the-template-engine) - - [Custom keywords, filters and functions](#custom-keywords-filters-and-functions) + - [Custom keywords, filters and functions](#custom-keywords-filters-and-functions) + - [`values`](#values) + - [`unpack`](#unpack) + - [`bitmask`](#bitmask) + - [`gitcommit`](#gitcommit) + - [`gitcommitlong`](#gitcommitlong) + - [`now()`](#now) + - [`scgversion`](#scgversion) - [scg checklogs](#scg-checklogs) - [scg update](#scg-update) - [Howto/tutorial](#howtotutorial) @@ -335,17 +342,18 @@ For further information, please take a look at the - [Filter functions](https://docs.rs/minijinja/latest/minijinja/filters/index.html) - [Test functions](https://docs.rs/minijinja/latest/minijinja/tests/index.html) -#### Custom keywords, filters and functions +### Custom keywords, filters and functions In addition to the built-in [filter functions](https://docs.rs/minijinja/latest/minijinja/filters/index.html#built-in-filters) and [global functions](https://docs.rs/minijinja/latest/minijinja/functions/index.html#functions) in MiniJinja, some custom functionality has been added. -##### `values` +#### `values` -Filter that extracts the values of a hashmap. Similar to the built-in -[items](https://docs.rs/minijinja/latest/minijinja/filters/fn.items.html) which extracts key-value pairs. +Similar to the built-in [items](https://docs.rs/minijinja/latest/minijinja/filters/fn.items.html) which extracts +key-value pairs, this filter extracts just the values of a map into an array. This can be handy when paired with e.g. +the [selectattr](https://docs.rs/minijinja/latest/minijinja/filters/fn.selectattr.html) filter. Example: @@ -360,14 +368,12 @@ This results in a hashmap that is available in all templates (since 2.8) and loo ```json { - "D01": - {"well": "D01", "flowline": "FL1", "Pdc": "13-1111-33"}, - "D02": - {"well": "D02", "flowline": "FL2", "Pdc": "13-2222-33"}, + "D01": { "well": "D01", "flowline": "FL1", "Pdc": "13-1111-33" }, + "D02": { "well": "D02", "flowline": "FL2", "Pdc": "13-2222-33" } } ``` -The values can now be extracted with +The source rows can now be extracted with `{{ wells | values }}` -> `[{"well": "D01", "Flowline": "FL1", "Pdc": "13-1111-33"}, {"well": "D02", "Flowline": "FL2", "Pdc": "13-2222-33"}]` @@ -382,7 +388,46 @@ Printing the Pdc value for all wells that are connected to flowline FL1: -> `13-1111-33` -##### `bitmask` +#### `unpack` + +Filter that unpacks values for the provided keys. Can be applied directly to a source, a source row or an array of +source rows. This prevents manually extracting each value with the Minjinja [set](https://docs.rs/minijinja/latest/minijinja/syntax/index.html#-set-) statement. + +Examples: + +Printing selected values for all rows in a source file: +```jinja +{% for well, flowline in main | unpack("well", "flowline") %} +{{ well }}: {{ flowline }} +{%- endfor %} +``` + +-> + +``` +D01: FL1 +D02: FL2 +``` + +Printing selected values for a single row in a source file: +```jinja +{% with (flowline, pdc) = main["D02"] | unpack("flowline", "pdc") %} +{{ flowline }}, {{ pdc }} +{%- endwith %} +``` + +-> `FL2, 13-2222-33` + +Printing selected values for all wells that are connected to flowline 2: +```jinja +{% for (well, pdc) in main | values | selectattr("flowline", "endingwith", 2) | unpack("well", "pdc") %} +{{ well }}: {{ pdc }} +{%- endfor %} +``` + +-> `D02: 13-2222-33` + +#### `bitmask` Filter that converts a non-negative integer or a sequence of non-negative integers into a bitmask. Each integer will be translated into a 1 in the bitmask that is otherwise 0. Takes an optional argument that is the length of the bitmask @@ -393,21 +438,21 @@ Examples: `{{ [1, 3, 31] | bitmask }}` -> `1000000000000000000000000000101` `{{ [1, 3] | bitmask(5) }}` -> `00101` -##### `gitcommit` +#### `gitcommit` Global variable that inserts the Git commit hash on short form. Example: `{{ gitcommit }}` -> 714e102 -##### `gitcommitlong` +#### `gitcommitlong` Global variable that inserts the Git commit hash on long form. Example: `{{ gitcommitlong }}` -> 714e10261b59baf4a0257700f57c5e36a6e8c6c3 -##### `now()` +#### `now()` Function that inserts a datestamp. The default format is `%Y-%m-%d %H:%M:%S"`. The format can be customized by providing an [strftime string](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) as function argument. @@ -416,12 +461,12 @@ Examples: `{{ now() }}` -> 2023-02-23 14:18:12 `{{ now("%a %d %b %Y %H:%M:%S") }}` -> Thu 23 feb 2023 14:18:12 -##### `scgversion` +#### `scgversion` Global variable that inserts the SCG version used to create the output file. Example: -`{{ scgversion }}` -> 2.2.1 +`{{ scgversion }}` -> 2.2.1 Try for example to add the following line at the top of the first template file: `// Generated with SCG v{{ scgversion }} on {{ now() }} from git commit {{ gitcommit }}` diff --git a/src/renderer.rs b/src/renderer.rs index 2d6db41b..1618bae4 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,6 +1,6 @@ use crate::config::Counter as CounterConfig; use chrono::Local; -use minijinja::value::{Value, ValueKind}; +use minijinja::value::{from_args, Kwargs, Rest, Value, ValueKind}; use minijinja::{Environment, Error, ErrorKind}; use serde::Serialize; use std::collections::HashMap; @@ -50,6 +50,66 @@ impl CounterMap { } } +fn filt_unpack(v: Value, unpack_keys: Rest) -> Result, Error> { + let (item_keys, _): (&[Value], Kwargs) = from_args(&unpack_keys)?; + match v.kind() { + ValueKind::Map => { + let items_are_maps = v + .try_iter() + .unwrap() + .all(|key| v.get_item(&key).unwrap_or(Value::UNDEFINED).kind() == ValueKind::Map); + if items_are_maps { + let rv = v + .try_iter() + .unwrap() + .map(|key| { + let value = v.get_item(&key).unwrap_or(Value::UNDEFINED); + item_keys + .iter() + .map(|key| value.get_item(key).unwrap_or(Value::UNDEFINED)) + .collect() + }) + .collect(); + Ok(rv) + } else { + let rv = item_keys + .iter() + .map(|key| v.get_item(key).unwrap_or(Value::UNDEFINED)) + .collect(); + Ok(rv) + } + } + ValueKind::Seq => { + let items_are_maps = v + .try_iter() + .unwrap() + .all(|val| val.kind() == ValueKind::Map); + if items_are_maps { + let rv: Vec = v + .try_iter() + .unwrap() + .map(|value| { + item_keys + .iter() + .map(|key| value.get_item(key).unwrap_or(Value::UNDEFINED)) + .collect() + }) + .collect(); + Ok(rv) + } else { + Err(Error::new( + ErrorKind::InvalidOperation, + "input is not a map of maps (source), map (source row) or list of maps (source rows)", + )) + } + } + _ => Err(Error::new( + ErrorKind::InvalidOperation, + "input is not a map of maps (source), map (source row) or list of maps (source rows)", + )), + } +} + fn filt_values(v: Value) -> Result { if v.kind() == ValueKind::Map { let mut rv = Vec::with_capacity(v.len().unwrap_or(0)); @@ -211,6 +271,7 @@ impl<'a> MiniJinja<'a> { renderer.env.add_function("now", func_timestamp); renderer.env.add_filter("bitmask", filt_bitmask); renderer.env.add_filter("values", filt_values); + renderer.env.add_filter("unpack", filt_unpack); renderer.env.set_formatter(erroring_formatter); let local_template_path = template_path.to_path_buf(); @@ -262,6 +323,101 @@ mod tests { use minijinja::{context, render}; use regex::Regex; + #[test] + fn filt_unpack_source() { + let mut env = Environment::new(); + env.add_filter("unpack", filt_unpack); + let result = render! {in env, "{{ A | unpack('b') }}", + A => context!( + AA => context!(a => "aa", b => "bb"), + CC => context!(a => "cc", b => "dd"), + ) + }; + assert_eq!(result, "[[\"bb\"], [\"dd\"]]") + } + + #[test] + fn filt_unpack_source_invalid_key() { + let mut env = Environment::new(); + env.add_filter("unpack", filt_unpack); + let result = render! {in env, "{{ A | unpack('c') }}", + A => context!( + AA => context!(a => "aa", b => "bb"), + CC => context!(a => "cc", b => "dd"), + ) + }; + assert_eq!(result, "[[undefined], [undefined]]") + } + + #[test] + fn filt_unpack_source_row() { + let mut env = Environment::new(); + env.add_filter("unpack", filt_unpack); + let result = render! {in env, "{{ AA | unpack('a', 'b') }}", + AA => context!(a => "aa", b => "bb") + }; + assert_eq!(result, "[\"aa\", \"bb\"]") + } + + #[test] + fn filt_unpack_source_row_invalid_key() { + let mut env = Environment::new(); + env.add_filter("unpack", filt_unpack); + let result = render! {in env, "{{ AA | unpack('a', 'c') }}", + AA => context!(a => "aa", b => "bb") + }; + assert_eq!(result, "[\"aa\", undefined]") + } + + #[test] + fn filt_unpack_source_rows() { + let mut env = Environment::new(); + env.add_filter("unpack", filt_unpack); + let result = render! {in env, "{{ A | unpack('c') }}", + A => vec!( + context!(a => "aa", b => "bb"), + context!(a => "cc", b => "dd"), + ) + }; + assert_eq!(result, "[[undefined], [undefined]]") + } + + #[test] + #[should_panic(expected = "input is not a map")] + fn filt_unpack_invalid_type() { + let mut env = Environment::new(); + env.add_filter("unpack", filt_unpack); + render! {in env, "{{ A | unpack('c') }}", + A => "Some string" + }; + } + + #[test] + #[should_panic(expected = "input is not a map")] + fn filt_unpack_invalid_seq_item() { + let mut env = Environment::new(); + env.add_filter("unpack", filt_unpack); + render! {in env, "{{ A | unpack('c') }}", + A => vec!( + context!(a => "aa", b => "bb"), + Value::from("Some string"), + ) + }; + } + + #[test] + fn filt_unpack_source_rows_invalid_key() { + let mut env = Environment::new(); + env.add_filter("unpack", filt_unpack); + let result = render! {in env, "{{ A | unpack('b') }}", + A => vec!( + context!(a => "aa", b => "bb"), + context!(a => "cc", b => "dd"), + ) + }; + assert_eq!(result, "[[\"bb\"], [\"dd\"]]") + } + #[test] fn filt_values_simple() { let mut env = Environment::new(); @@ -270,7 +426,7 @@ mod tests { A => context!( AA => context!(a => "aa", b => "bb"), CC => context!(a => "cc", b => "dd"), - ) + ) }; assert_eq!( result,