Skip to content

Commit

Permalink
feat: add filter to unpack values from maps (#216)
Browse files Browse the repository at this point in the history
  • Loading branch information
einarsi authored Feb 25, 2024
1 parent b599c5a commit e1bbaeb
Show file tree
Hide file tree
Showing 2 changed files with 219 additions and 18 deletions.
77 changes: 61 additions & 16 deletions docs/Howto_SCG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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` <!-- omit in toc -->
#### `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:

Expand All @@ -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"}]`
Expand All @@ -382,7 +388,46 @@ Printing the Pdc value for all wells that are connected to flowline FL1:

-> `13-1111-33`

##### `bitmask` <!-- omit in toc -->
#### `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
Expand All @@ -393,21 +438,21 @@ Examples:
`{{ [1, 3, 31] | bitmask }}` -> `1000000000000000000000000000101`
`{{ [1, 3] | bitmask(5) }}` -> `00101`

##### `gitcommit` <!-- omit in toc -->
#### `gitcommit`

Global variable that inserts the Git commit hash on short form.

Example:
`{{ gitcommit }}` -> 714e102

##### `gitcommitlong` <!-- omit in toc -->
#### `gitcommitlong`

Global variable that inserts the Git commit hash on long form.

Example:
`{{ gitcommitlong }}` -> 714e10261b59baf4a0257700f57c5e36a6e8c6c3

##### `now()` <!-- omit in toc -->
#### `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.
Expand All @@ -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` <!-- omit in toc -->
#### `scgversion`

Global variable that inserts the SCG version used to create the output file.

Example:
`{{ scgversion }}` -> 2.2.1 <!-- omit in toc -->
`{{ 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 }}`
Expand Down
160 changes: 158 additions & 2 deletions src/renderer.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -50,6 +50,66 @@ impl CounterMap {
}
}

fn filt_unpack(v: Value, unpack_keys: Rest<Value>) -> Result<Vec<Value>, 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<Value> = 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<Value, Error> {
if v.kind() == ValueKind::Map {
let mut rv = Vec::with_capacity(v.len().unwrap_or(0));
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand All @@ -270,7 +426,7 @@ mod tests {
A => context!(
AA => context!(a => "aa", b => "bb"),
CC => context!(a => "cc", b => "dd"),
)
)
};
assert_eq!(
result,
Expand Down

0 comments on commit e1bbaeb

Please sign in to comment.