Watch
0
0
Fork
You've already forked hyperhive
0

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:
damocles 2026-11-11 02:13:54 +01:00 • committed by mara
commit 8ef3138863
2 changed files with 306 additions and 91 deletions

View file

@ -175,28 +175,20 @@ fn holds(condition: &Condition, fields: &Fields) -> bool {
}
}
/// `template` with each `{field}` replaced by that field's value, a list
/// joined with `, `. A name that is no field is an error naming it.
/// `template`, a Jinja template, rendered over the event's fields; a list field
/// reads as its items joined with `, `. A name that is no field is an error.
pub fn expand(template: &str, fields: &Fields) -> Result<String, String> {
let mut out = String::with_capacity(template.len());
let mut rest = template;
while let Some(open) = rest.find('{') {
out.push_str(&rest[..open]);
let after = &rest[open + 1..];
let Some(close) = after.find('}') else {
out.push_str(&rest[open..]);
return Ok(out);
};
let name = &after[..close];
match fields.get(name) {
Some(Value::Text(v)) => out.push_str(v),
Some(Value::List(items)) => out.push_str(&items.join(", ")),
None => return Err(format!("{{{name}}} is no event field")),
}
rest = &after[close + 1..];
}
out.push_str(rest);
Ok(out)
let context: BTreeMap<&str, String> = fields
.iter()
.map(|(name, value)| {
let text = match value {
Value::Text(v) => v.clone(),
Value::List(items) => items.join(", "),
};
(*name, text)
})
.collect();
hive_template::render(template, &context)
}
#[cfg(test)]
@ -387,9 +379,9 @@ mod tests {
f.nodes[2].kind = NodeKind::Todo(TodoSink {
to: Recipients::Agent("triage".to_owned()),
op: Op::Upsert,
key: "forge:{repo}#{number}:triage".to_owned(),
key: "forge:{{ repo }}#{{ number }}:triage".to_owned(),
prefix: false,
text: "triage {repo}#{number}".to_owned(),
text: "triage {{ repo }}#{{ number }}".to_owned(),
});
let got = evaluate(
&f,
@ -502,13 +494,10 @@ mod tests {
fn a_placeholder_expands_to_its_field_and_an_unknown_one_is_an_error() {
let fields = event("assigned", &["a", "b"], &[], &[]);
assert_eq!(
expand("{repo}#{number} {assignees_added} {", &fields),
expand("{{ repo }}#{{ number }} {{ assignees_added }} {", &fields),
Ok("hyperhive/hyperhive#7 a, b {".to_owned())
);
assert_eq!(
expand("x {nope}", &fields),
Err("{nope} is no event field".to_owned())
);
assert!(expand("x {{ nope }}", &fields).is_err());
}
/// A sink naming a placeholder the event lacks sends nothing, rather
@ -519,7 +508,7 @@ mod tests {
f.nodes[2].kind = NodeKind::Todo(TodoSink {
to: Recipients::Agent("triage".to_owned()),
op: Op::Upsert,
key: "forge:{repo}#{numbr}:triage".to_owned(),
key: "forge:{{ repo }}#{{ numbr }}:triage".to_owned(),
prefix: false,
text: "t".to_owned(),
});
@ -527,7 +516,7 @@ mod tests {
assert!(evaluate(&f, "issue_assign", &fields).is_empty());
// Control: spelled right, the same flow emits.
if let NodeKind::Todo(s) = &mut f.nodes[2].kind {
s.key = "forge:{repo}#{number}:triage".to_owned();
s.key = "forge:{{ repo }}#{{ number }}:triage".to_owned();
}
assert_eq!(evaluate(&f, "issue_assign", &fields).len(), 1);
}

View file

@ -236,7 +236,7 @@ mod tests {
);
for bad in [
junk(2, b"not json"),
junk(3, br#"{"version":2,"flows":[]}"#),
junk(3, br#"{"version":3,"flows":[]}"#),
junk(4, br#"{"flows":[{"id":"x"}]}"#),
Read::Failed("timeout".to_owned()),
] {