93 lines
3.3 KiB
Rust
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:?}");
|
|
}
|
|
}
|
|
}
|