templates: one Jinja language (hive-template) for the notify router's flows, flows document version 2
A todo node's key and text were hand-expanded `{field}` strings. They are now Jinja templates rendered through the new hive-template crate (minijinja, an unknown variable is an error), and the controller's save check reads the variables the template uses. The flows document is version 2; a version 1 document reads as version 2 with each `{field}` converted to `{{ field }}`, and the next save writes it.
This commit is contained in:
parent
11393bc538
commit
8ef3138863
3 changed files with 306 additions and 91 deletions
18
hive-template/Cargo.toml
Normal file
18
hive-template/Cargo.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "hive-template"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
readme = "README.md"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
minijinja = { version = "2", default-features = false, features = [
|
||||
"builtins",
|
||||
"serde",
|
||||
] }
|
||||
serde.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json.workspace = true
|
||||
14
hive-template/README.md
Normal file
14
hive-template/README.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# hive-template
|
||||
|
||||
The one template language the hyperhive workspace renders with: Jinja, through
|
||||
[`minijinja`](https://crates.io/crates/minijinja). A value is `{{ name }}`, a
|
||||
block is `{% if cond %}...{% endif %}`, and a lone `{` or `}` stays plain text.
|
||||
|
||||
- **`render(template, &context)`**: the text, or a message when the template
|
||||
does not parse or names a variable the context lacks. An unknown variable is
|
||||
an error, never an empty string.
|
||||
- **`variables(template)`**: the variables the template reads, so a caller can
|
||||
refuse an unknown field when the template is saved instead of when it renders.
|
||||
|
||||
Used by the notification router's flow templates (`swarm-notify-router`,
|
||||
`swarm-wire`).
|
||||
89
hive-template/src/lib.rs
Normal file
89
hive-template/src/lib.rs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
//! The one template language the workspace renders with: Jinja, through
|
||||
//! `minijinja`, with an unknown variable an error rather than an empty string.
|
||||
//!
|
||||
//! A template is `{{ name }}` for a value and `{% if cond %}...{% endif %}` for
|
||||
//! a block; a lone `{` or `}` is plain text. Callers pass their context as any
|
||||
//! `Serialize` value and get back the text or a message naming what is wrong.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use minijinja::{Environment, UndefinedBehavior};
|
||||
use serde::Serialize;
|
||||
|
||||
/// An environment that refuses a name the context does not hold, and keeps the
|
||||
/// template's trailing newline.
|
||||
fn environment() -> Environment<'static> {
|
||||
let mut env = Environment::new();
|
||||
env.set_undefined_behavior(UndefinedBehavior::Strict);
|
||||
env.set_keep_trailing_newline(true);
|
||||
env
|
||||
}
|
||||
|
||||
/// `template` rendered against `context`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The template does not parse, or it names a variable `context` does not hold.
|
||||
pub fn render(template: &str, context: &impl Serialize) -> Result<String, String> {
|
||||
environment()
|
||||
.render_str(template, context)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// The variables `template` reads that it does not define itself, such as `repo`
|
||||
/// for `{{ repo }}`. Lets a caller refuse a template that names an unknown field
|
||||
/// before any event renders it.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The template does not parse.
|
||||
pub fn variables(template: &str) -> Result<BTreeSet<String>, String> {
|
||||
let env = environment();
|
||||
let parsed = env.template_from_str(template).map_err(|e| e.to_string())?;
|
||||
Ok(parsed.undeclared_variables(false).into_iter().collect())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_value_and_a_block_render() {
|
||||
let ctx = json!({ "repo": "o/r", "role": "agent" });
|
||||
let out = render(
|
||||
"{{ repo }}{% if role == \"agent\" %} (agent){% endif %}\n",
|
||||
&ctx,
|
||||
);
|
||||
assert_eq!(out.as_deref(), Ok("o/r (agent)\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_braces_are_plain_text() {
|
||||
assert_eq!(
|
||||
render("{not a name} and { open", &json!({})).as_deref(),
|
||||
Ok("{not a name} and { open")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unknown_variable_is_an_error() {
|
||||
assert!(render("{{ numbr }}", &json!({ "number": "7" })).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variables_lists_what_the_template_reads_not_what_it_defines() {
|
||||
let found = variables("{{ repo }}#{{ number }}{% set x = 1 %}{{ x }}").expect("parses");
|
||||
assert_eq!(
|
||||
found.into_iter().collect::<Vec<_>>(),
|
||||
["number", "repo"].map(String::from)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_template_that_does_not_parse_is_refused() {
|
||||
assert!(variables("{{ repo ").is_err());
|
||||
assert!(render("{% if %}", &json!({})).is_err());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue