2026-04-24 18:01:34 +00:00
|
|
|
//! Notifications service — pipeline-event fan-out to chat transports.
|
|
|
|
|
//!
|
|
|
|
|
//! Subscribes to [`WatcherEvent`] broadcasts and posts human-readable messages
|
|
|
|
|
//! to all configured chat rooms whenever a work item moves through the pipeline.
|
|
|
|
|
//!
|
|
|
|
|
//! Follows service-module conventions:
|
|
|
|
|
//! - `mod.rs` (this file) — public API, typed [`Error`] type, orchestration
|
2026-04-28 15:38:38 +00:00
|
|
|
//! - `io/` — the ONLY place that performs side effects (DB reads, config
|
2026-04-24 18:01:34 +00:00
|
|
|
//! loads, `tokio::spawn`)
|
|
|
|
|
//! - `format.rs` — pure: message formatting functions
|
|
|
|
|
//! - `filter.rs` — pure: debounce constants and suppression predicates
|
|
|
|
|
//! - `events.rs` — pure: WatcherEvent classification / event mapping
|
|
|
|
|
//! - `route.rs` — pure: room-routing decisions
|
|
|
|
|
|
|
|
|
|
pub(super) mod events;
|
|
|
|
|
pub(super) mod filter;
|
|
|
|
|
pub(super) mod format;
|
|
|
|
|
pub(super) mod io;
|
|
|
|
|
pub(super) mod route;
|
|
|
|
|
|
|
|
|
|
pub use format::{
|
2026-07-17 19:18:37 +00:00
|
|
|
format_blocked_notification, format_disk_recovery_notification,
|
|
|
|
|
format_disk_warning_notification, format_error_notification, format_stage_notification,
|
2026-04-24 18:01:34 +00:00
|
|
|
};
|
2026-07-18 10:29:40 +00:00
|
|
|
pub use io::resolve_project_display_name;
|
2026-04-24 18:01:34 +00:00
|
|
|
pub use io::spawn_notification_listener;
|
2026-05-14 07:51:16 +00:00
|
|
|
pub use io::spawn_stage_notification_subscriber;
|
2026-04-24 18:01:34 +00:00
|
|
|
|
|
|
|
|
// ── Error type ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// Typed errors returned by `service::notifications` operations.
|
|
|
|
|
///
|
|
|
|
|
/// HTTP handlers and bot commands may map these to user-facing messages.
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
|
pub enum Error {
|
|
|
|
|
/// The incoming event type is not recognised or not supported.
|
|
|
|
|
UnknownEvent(String),
|
|
|
|
|
/// A message could not be formatted for delivery (e.g. malformed input).
|
|
|
|
|
RenderFailure(String),
|
|
|
|
|
/// The underlying chat transport rejected the send operation.
|
|
|
|
|
TransportSendFailure(String),
|
|
|
|
|
/// Required configuration (room IDs, credentials) is absent.
|
|
|
|
|
ConfigMissing(String),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl std::fmt::Display for Error {
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
match self {
|
|
|
|
|
Self::UnknownEvent(msg) => write!(f, "Unknown event: {msg}"),
|
|
|
|
|
Self::RenderFailure(msg) => write!(f, "Render failure: {msg}"),
|
|
|
|
|
Self::TransportSendFailure(msg) => write!(f, "Transport send failure: {msg}"),
|
|
|
|
|
Self::ConfigMissing(msg) => write!(f, "Config missing: {msg}"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
// ── Error Display ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn error_unknown_event_display() {
|
|
|
|
|
let e = Error::UnknownEvent("bad_event_type".to_string());
|
|
|
|
|
assert!(e.to_string().contains("Unknown event"));
|
|
|
|
|
assert!(e.to_string().contains("bad_event_type"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn error_render_failure_display() {
|
|
|
|
|
let e = Error::RenderFailure("malformed input".to_string());
|
|
|
|
|
assert!(e.to_string().contains("Render failure"));
|
|
|
|
|
assert!(e.to_string().contains("malformed input"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn error_transport_send_failure_display() {
|
|
|
|
|
let e = Error::TransportSendFailure("connection refused".to_string());
|
|
|
|
|
assert!(e.to_string().contains("Transport send failure"));
|
|
|
|
|
assert!(e.to_string().contains("connection refused"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn error_config_missing_display() {
|
|
|
|
|
let e = Error::ConfigMissing("room_id not set".to_string());
|
|
|
|
|
assert!(e.to_string().contains("Config missing"));
|
|
|
|
|
assert!(e.to_string().contains("room_id not set"));
|
|
|
|
|
}
|
|
|
|
|
}
|