an archetype carries its own notification-router flows for its agents
ArchetypeMeta gets flows, checked when the controller stores the record: every todo must go to the new members recipient, which the router expands to the agents of the archetype. The operator's flows document refuses members. The router reads the archetype records and each roster agent's archetype key, and its NATS grant gains read on both buckets.
This commit is contained in:
parent
ca339b7ba3
commit
1efbb72fdd
1 changed files with 883 additions and 29 deletions
|
|
@ -271,6 +271,11 @@ pub enum Recipients {
|
|||
Field(String),
|
||||
/// This agent, whatever the event.
|
||||
Agent(String),
|
||||
/// Every agent created from the archetype the flow came from. Only an
|
||||
/// archetype's own flows take it: the operator's document has no
|
||||
/// archetype, so [`Flow::validate`] refuses it there, and
|
||||
/// [`Flow::validate_archetype`] refuses every other recipient.
|
||||
Members,
|
||||
}
|
||||
|
||||
/// Why [`Document::decode`] could not read a document.
|
||||
|
|
@ -665,12 +670,29 @@ fn jinja_of_legacy(template: &str) -> String {
|
|||
|
||||
impl Flow {
|
||||
/// Check the flow is a tree within the caps, and that every node reads
|
||||
/// only what exists.
|
||||
/// only what exists. This is the check for the operator's document, which
|
||||
/// has no archetype, so a [`Recipients::Members`] fails it.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Says why not.
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
self.validate_in(Scope::Document)
|
||||
}
|
||||
|
||||
/// The check for a flow an archetype carries: [`Flow::validate`], except
|
||||
/// that every todo goes to [`Recipients::Members`] and no node asks for a
|
||||
/// review. A flow that names an agent, or the agents an event field holds,
|
||||
/// or asks for a review, belongs in the operator's document.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Says why not.
|
||||
pub fn validate_archetype(&self) -> Result<(), String> {
|
||||
self.validate_in(Scope::Archetype)
|
||||
}
|
||||
|
||||
fn validate_in(&self, scope: Scope) -> Result<(), String> {
|
||||
if !is_id(&self.id) {
|
||||
return Err(format!(
|
||||
"id {:?} is not 1 to 64 letters, digits, - or _",
|
||||
|
|
@ -695,7 +717,7 @@ impl Flow {
|
|||
return Err(format!("node id {} is used twice", node.id));
|
||||
}
|
||||
node.kind
|
||||
.validate()
|
||||
.validate(scope)
|
||||
.map_err(|why| format!("node {}: {why}", node.id))?;
|
||||
}
|
||||
for edge in &self.edges {
|
||||
|
|
@ -766,8 +788,17 @@ impl Flow {
|
|||
}
|
||||
}
|
||||
|
||||
/// Where a flow lives, which decides who its todos may go to.
|
||||
#[derive(Clone, Copy)]
|
||||
enum Scope {
|
||||
/// The operator's document, or a built-in.
|
||||
Document,
|
||||
/// An archetype's `meta.flows`.
|
||||
Archetype,
|
||||
}
|
||||
|
||||
impl NodeKind {
|
||||
fn validate(&self) -> Result<(), String> {
|
||||
fn validate(&self, scope: Scope) -> Result<(), String> {
|
||||
match self {
|
||||
Self::Source { events } => {
|
||||
if events.is_empty() {
|
||||
|
|
@ -792,7 +823,12 @@ impl NodeKind {
|
|||
));
|
||||
}
|
||||
}
|
||||
Self::Todo(sink) => sink.validate()?,
|
||||
Self::Todo(sink) => sink.validate(scope)?,
|
||||
Self::RequestReviewer { .. } if matches!(scope, Scope::Archetype) => {
|
||||
return Err("an archetype's flow cannot request a review: that is the \
|
||||
operator's flows document's job"
|
||||
.to_owned());
|
||||
}
|
||||
Self::RequestReviewer { reviewer } => {
|
||||
if !is_login(reviewer) {
|
||||
return Err(format!("reviewer {reviewer:?} is not a forge login"));
|
||||
|
|
@ -804,12 +840,26 @@ impl NodeKind {
|
|||
}
|
||||
|
||||
impl TodoSink {
|
||||
fn validate(&self) -> Result<(), String> {
|
||||
match &self.to {
|
||||
Recipients::Field(f) if !is_field(f) => {
|
||||
fn validate(&self, scope: Scope) -> Result<(), String> {
|
||||
match (&self.to, scope) {
|
||||
(Recipients::Members, Scope::Archetype) => {}
|
||||
(Recipients::Members, Scope::Document) => {
|
||||
return Err("recipients members is for an archetype's flows: the \
|
||||
document has no archetype"
|
||||
.to_owned());
|
||||
}
|
||||
(_, Scope::Archetype) => {
|
||||
return Err(
|
||||
"an archetype's flow sends only to its members: a flow to a \
|
||||
named agent or to an event field's agents is the operator's flows \
|
||||
document's job"
|
||||
.to_owned(),
|
||||
);
|
||||
}
|
||||
(Recipients::Field(f), Scope::Document) if !is_field(f) => {
|
||||
return Err(format!("recipients from {f:?}, which is no event field"));
|
||||
}
|
||||
Recipients::Agent(a) if !is_id(a) => {
|
||||
(Recipients::Agent(a), Scope::Document) if !is_id(a) => {
|
||||
return Err(format!("recipient {a:?} is not an agent name"));
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -1681,4 +1731,75 @@ mod tests {
|
|||
})
|
||||
);
|
||||
}
|
||||
|
||||
/// A copy of the `custom` flow whose todo goes to `to`.
|
||||
fn sending_to(id: &str, to: Recipients) -> Flow {
|
||||
let mut f = custom(id);
|
||||
let NodeKind::Todo(sink) = &mut f.nodes[2].kind else {
|
||||
panic!("a todo")
|
||||
};
|
||||
sink.to = to;
|
||||
f
|
||||
}
|
||||
|
||||
/// `members` is an archetype flow's recipient: the operator's document and
|
||||
/// the document check refuse it, an archetype flow takes nothing else.
|
||||
#[test]
|
||||
fn members_is_for_an_archetypes_flows_only() {
|
||||
let members = sending_to("m", Recipients::Members);
|
||||
// Presence: the same flow passes where `members` belongs.
|
||||
assert_eq!(members.validate_archetype(), Ok(()));
|
||||
let why = members.validate().expect_err("no archetype in a document");
|
||||
assert!(why.contains("no archetype"), "{why}");
|
||||
let doc = Document {
|
||||
flows: vec![members],
|
||||
..Document::default()
|
||||
};
|
||||
assert!(doc.check().is_err());
|
||||
assert!(doc.active().0.iter().all(|f| f.id != "m"), "left out");
|
||||
|
||||
for other in [
|
||||
Recipients::Agent("triage".to_owned()),
|
||||
Recipients::Field("author".to_owned()),
|
||||
] {
|
||||
let f = sending_to("o", other);
|
||||
assert_eq!(f.validate(), Ok(()), "control: a document takes it");
|
||||
let why = f.validate_archetype().expect_err("not for an archetype");
|
||||
assert!(why.contains("only to its members"), "{why}");
|
||||
}
|
||||
let mut review = custom("r");
|
||||
review.nodes[2].kind = NodeKind::RequestReviewer {
|
||||
reviewer: "argus".to_owned(),
|
||||
};
|
||||
assert_eq!(review.validate(), Ok(()), "control: a document takes it");
|
||||
assert!(review.validate_archetype().is_err());
|
||||
for built in builtins() {
|
||||
assert!(built.validate_archetype().is_err(), "{}", built.id);
|
||||
}
|
||||
}
|
||||
|
||||
/// An archetype's flow reads its key and text as the same Jinja templates a
|
||||
/// document's flow does: an unknown variable or broken syntax is refused.
|
||||
#[test]
|
||||
fn an_archetypes_templates_are_checked_like_the_documents() {
|
||||
let with = |key: &str, text: &str| {
|
||||
let mut f = sending_to("t", Recipients::Members);
|
||||
let NodeKind::Todo(sink) = &mut f.nodes[2].kind else {
|
||||
panic!("a todo")
|
||||
};
|
||||
sink.key = key.to_owned();
|
||||
sink.text = text.to_owned();
|
||||
f
|
||||
};
|
||||
let ok = with("forge:{{ repo }}#{{ number }}:x", "{{ title }} {{ url }}");
|
||||
assert_eq!(ok.validate_archetype(), Ok(()), "control");
|
||||
for (key, text) in [
|
||||
("forge:{{ repo }}#{{ numbr }}:x", "t"),
|
||||
("k", "{{ nope }}"),
|
||||
("k", "{{ title "),
|
||||
] {
|
||||
let f = with(key, text);
|
||||
assert!(f.validate_archetype().is_err(), "{key} {text}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue