Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/swarm-wire/src/forge_events.rs

93 lines
3.3 KiB
Rust

//! The forge events stream: every verified `issues` and `pull_request`
//! delivery swarm-controller receives, kept for the notification router.
//!
//! The controller checks a delivery's HMAC and publishes its body unchanged
//! on [`subject`], one subject per Forgejo event type (the
//! `X-Forgejo-Event-Type` header: `issues`, `issue_assign`, `pull_request`,
//! `pull_request_sync`, …). The router reads the stream with one durable
//! consumer, so it holds no webhook secret and registers no hook.
//!
//! Three crates name these: the controller publishes, the router consumes,
//! and the auth-callout responder grants each its side.
use std::time::Duration;
/// The stream the deliveries are kept in.
pub const STREAM: &str = "SWARM_EVENTS";
/// Every subject [`STREAM`] captures sits below this.
pub const SUBJECT_PREFIX: &str = "$SWARM.events.forge";
/// How long the stream keeps a delivery.
pub const MAX_AGE: Duration = Duration::from_hours(7 * 24);
/// The router's durable consumer on [`STREAM`].
pub const ROUTER_CONSUMER: &str = "notify-router";
/// The event type swarm-controller publishes, on [`subject`], for a pull request
/// it has checked against its target branch after a push there. Forgejo sends
/// no such delivery. The body is a `pull_request` delivery whose `action` is
/// `conflicting` or `mergeable`.
pub const MERGEABILITY_EVENT: &str = "pull_request_mergeability";
/// The subject a delivery of Forgejo event type `event_type` is kept under.
#[must_use]
pub fn subject(event_type: &str) -> String {
format!("{SUBJECT_PREFIX}.{event_type}")
}
/// Every [`subject`] at once: the stream's subjects, the router's consumer
/// filter and the controller's publish grant.
#[must_use]
pub fn subject_wildcard() -> String {
subject("*")
}
/// The event type a [`subject`] names, or `None` for a subject not below
/// [`SUBJECT_PREFIX`].
#[must_use]
pub fn event_type(subject: &str) -> Option<&str> {
subject
.strip_prefix(SUBJECT_PREFIX)?
.strip_prefix('.')
.filter(|t| !t.is_empty() && !t.contains('.'))
}
/// Whether `event_type` can be one token of a subject. The value comes from
/// a request header, and a `.`, a wildcard or whitespace would publish
/// somewhere other than one leaf below [`SUBJECT_PREFIX`].
#[must_use]
pub fn is_subject_token(event_type: &str) -> bool {
!event_type.is_empty()
&& event_type
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_subject_round_trips_to_its_event_type() {
assert_eq!(subject("issue_assign"), "$SWARM.events.forge.issue_assign");
assert_eq!(subject_wildcard(), "$SWARM.events.forge.*");
assert_eq!(
event_type("$SWARM.events.forge.issue_assign"),
Some("issue_assign")
);
assert_eq!(event_type("$SWARM.events.forge."), None);
assert_eq!(event_type("$SWARM.events.forge.a.b"), None);
assert_eq!(event_type("$SWARM.term.atlas"), None);
}
#[test]
fn only_a_plain_event_type_is_a_subject_token() {
for good in ["issues", "issue_assign", "pull_request_review_approved"] {
assert!(is_subject_token(good), "{good}");
}
for bad in ["", "a.b", "*", ">", "a b", "Issues"] {
assert!(!is_subject_token(bad), "{bad:?}");
}
}
}