Skip to main content

opssignal_core/providers/
console.rs

1use crate::delivery::Provider;
2use crate::signal::{Signal, SignalError};
3use crate::template::{self, Templates};
4
5/// Always-available fallback. Per the architecture review: unmatched
6/// signals must go SOMEWHERE — a notification SDK that silently drops
7/// signals is worse than having no SDK at all. Console is the
8/// guaranteed-to-work last resort.
9#[derive(Default)]
10pub struct ConsoleProvider {
11    templates: Option<Templates>,
12}
13
14impl ConsoleProvider {
15    pub fn new() -> Self {
16        Self::default()
17    }
18
19    fn line_for(&self, signal: &Signal) -> String {
20        template::render(&self.templates, "text", signal, &[]).unwrap_or_else(|| {
21            format!(
22                "[signal:{}] {} {} — {}",
23                signal.source, signal.severity, signal.event_type, signal.title
24            )
25        })
26    }
27
28    /// Uses `templates` for the parts they set; see [`crate::template`].
29    pub fn with_templates(mut self, templates: Option<Templates>) -> Self {
30        self.templates = templates;
31        self
32    }
33}
34
35#[async_trait::async_trait]
36impl Provider for ConsoleProvider {
37    fn name(&self) -> &'static str {
38        "console"
39    }
40
41    fn preview(&self, signal: &Signal) -> Option<serde_json::Value> {
42        Some(serde_json::json!({ "text": self.line_for(signal) }))
43    }
44
45    async fn send(&self, signal: &Signal) -> Result<(), SignalError> {
46        println!("{}", self.line_for(signal));
47        Ok(())
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54    use crate::signal::SignalInput;
55
56    #[tokio::test]
57    async fn console_always_succeeds() {
58        let signal = Signal::from_input(SignalInput {
59            source: "x".into(),
60            event_type: "y".into(),
61            title: "z".into(),
62            ..Default::default()
63        });
64        assert_eq!(ConsoleProvider::new().name(), "console");
65        ConsoleProvider::new()
66            .send(&signal)
67            .await
68            .expect("console never fails");
69    }
70
71    #[tokio::test]
72    async fn text_template_is_accepted() {
73        let provider = ConsoleProvider::new()
74            .with_templates(template::for_test("console", &[("text", "{{ title }}")]));
75        provider
76            .send(&Signal::from_input(SignalInput {
77                source: "t".into(),
78                event_type: "e".into(),
79                title: "hello".into(),
80                ..Default::default()
81            }))
82            .await
83            .expect("console never fails");
84    }
85}