//! The notification router's sinks that act on the forge, which it asks //! swarm-controller to run. //! //! The router has no forge token. For a `request_reviewer` node //! ([`crate::notify_flows::NodeKind::RequestReviewer`]) it sends a request on //! [`REQUEST_REVIEWER_SUBJECT`] and swarm-controller answers on the request's //! reply subject with an [`Outcome`], having acted under its own token. //! //! Three crates name these: the router sends, the controller answers, and the //! auth-callout responder grants each its side. The controller is the //! subject's one subscriber and the router its one publisher. use std::time::Duration; use serde::{Deserialize, Serialize}; /// The subject a request for a review goes to. pub const REQUEST_REVIEWER_SUBJECT: &str = "$SWARM.router.sink.request-reviewer"; /// How long the router waits for the answer. The controller's reply grant /// lasts this long, so its own work stops sooner ([`WORK_DEADLINE`]). pub const REPLY_DEADLINE: Duration = Duration::from_secs(30); /// How long the controller works on one request before it answers /// [`Outcome::Error`]. pub const WORK_DEADLINE: Duration = Duration::from_secs(20); /// Ask for `reviewer`'s review of pull request `number` of `repo`. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct RequestReviewer { /// `owner/name`. pub repo: String, pub number: u64, /// The reviewer's forge login. pub reviewer: String, } /// What the controller did with a [`RequestReviewer`], on the wire as one /// line: `requested`, `skipped:` or `error:`. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Outcome { /// The forge accepted the request. Requested, /// Nothing was sent, and why. Skipped(String), /// The request failed, and why. Error(String), } impl Outcome { /// The wire line. #[must_use] pub fn encode(&self) -> String { match self { Self::Requested => "requested".to_owned(), Self::Skipped(why) => format!("skipped:{why}"), Self::Error(why) => format!("error:{why}"), } } /// The outcome a reply line says; a line that is none of the three is an /// [`Outcome::Error`] saying so. #[must_use] pub fn decode(line: &str) -> Self { if line == "requested" { Self::Requested } else if let Some(why) = line.strip_prefix("skipped:") { Self::Skipped(why.to_owned()) } else if let Some(why) = line.strip_prefix("error:") { Self::Error(why.to_owned()) } else { Self::Error(format!("unreadable reply {line:?}")) } } } #[cfg(test)] mod tests { use super::*; #[test] fn an_outcome_round_trips_its_line() { for outcome in [ Outcome::Requested, Outcome::Skipped("a review stands".to_owned()), Outcome::Error("the forge said: no: why".to_owned()), ] { assert_eq!(Outcome::decode(&outcome.encode()), outcome); } assert_eq!(Outcome::Requested.encode(), "requested"); assert_eq!(Outcome::Skipped("x".to_owned()).encode(), "skipped:x"); assert_eq!(Outcome::Error("x".to_owned()).encode(), "error:x"); } #[test] fn a_line_that_is_no_outcome_reads_as_an_error() { assert!(matches!(Outcome::decode("ok"), Outcome::Error(e) if e.contains("ok"))); } /// The reply grant covers the work and the router's wait covers the grant. #[test] fn the_controller_stops_working_before_the_router_stops_waiting() { assert!(WORK_DEADLINE < REPLY_DEADLINE); } }