Watch
0
0
Fork
You've already forked hyperhive
0

notify flows: refuse unknown placeholders and require the revision read on every save

This commit is contained in:
atlas 2026-10-24 04:38:17 +02:00 • committed by mara
commit ada364a482
7 changed files with 203 additions and 57 deletions

View file

@ -291,6 +291,21 @@ fn is_field(name: &str) -> bool {
FIELDS.contains(&name)
}
/// The `{name}` placeholders in `template`, in order. A `{` with no `}` after
/// it is plain text.
#[must_use]
pub fn placeholders(template: &str) -> Vec<&str> {
let mut names = Vec::new();
let mut rest = template;
while let Some(open) = rest.find('{') {
let after = &rest[open + 1..];
let Some(close) = after.find('}') else { break };
names.push(&after[..close]);
rest = &after[close + 1..];
}
names
}
impl Flow {
/// Check the flow is a tree within the caps, and that every node reads
/// only what exists.
@ -435,6 +450,11 @@ impl TodoSink {
if self.text.len() > MAX_TEXT {
return Err(format!("text is longer than {MAX_TEXT} bytes"));
}
for (what, template) in [("key", &self.key), ("text", &self.text)] {
if let Some(bad) = placeholders(template).into_iter().find(|n| !is_field(n)) {
return Err(format!("{what} has {{{bad}}}, which is no event field"));
}
}
match self.op {
Op::Upsert if self.text.trim().is_empty() => Err("an upsert with no text".to_owned()),
Op::Upsert if self.prefix => Err("prefix is for a clear".to_owned()),
@ -676,6 +696,35 @@ mod tests {
assert_eq!(custom("c").validate(), Ok(()));
}
#[test]
fn a_placeholder_must_name_an_event_field() {
let with = |key: &str, text: &str| {
let mut f = custom("c");
f.nodes[2].kind = NodeKind::Todo(TodoSink {
to: Recipients::Agent("triage".to_owned()),
op: Op::Upsert,
key: key.to_owned(),
prefix: false,
text: text.to_owned(),
});
f.validate()
};
assert_eq!(
with("forge:{repo}#{number}:triage", "{title} {url}"),
Ok(())
);
assert!(
with("forge:{repo}#{numbr}:triage", "t")
.unwrap_err()
.contains("numbr")
);
assert!(with("k", "see {ulr}").unwrap_err().contains("ulr"));
assert!(with("k{}", "t").is_err(), "an empty name");
// An unclosed brace is text.
assert_eq!(with("k", "set {x"), Ok(()));
assert_eq!(placeholders("a{b}c{d} {e"), ["b", "d"]);
}
#[test]
fn a_sink_must_say_what_its_op_needs() {
let sink = |op, prefix, text: &str| {