Skip to main content

razeri_plugin_api/sinks/
stdout.rs

1// Copyright (C) 2026 Gabriel Shea
2// Licensed under either of the Apache License, Version 2.0 or the MIT
3// license, at your option.
4
5//! Stdout event sink — writes events as JSON to stderr.
6//!
7//! Uses stderr rather than stdout so it does not interfere with
8//! structured program output on stdout.
9//!
10//! **Registered only when asked for**: a program that names no sink writes no
11//! event to its error output. A host that wants it — a container whose
12//! platform keeps stderr, say — builds it with
13//! [`create_sink`](crate::sinks::create_sink) from `{ type: stdout }`, or
14//! registers [`StdoutSink`] on its [`EventRegistry`](crate::EventRegistry).
15
16use async_trait::async_trait;
17
18use crate::error::EventSinkError;
19use crate::event::{EventSink, RazeriEvent};
20
21/// Writes each event as a single-line JSON object to stderr.
22pub struct StdoutSink;
23
24impl StdoutSink {
25    /// Create a new `StdoutSink`.
26    #[must_use]
27    pub fn new() -> Self {
28        Self
29    }
30}
31
32impl Default for StdoutSink {
33    fn default() -> Self {
34        Self::new()
35    }
36}
37
38#[async_trait]
39impl EventSink for StdoutSink {
40    fn name(&self) -> &'static str {
41        "stdout"
42    }
43
44    async fn on_event(&self, event: &RazeriEvent) -> Result<(), EventSinkError> {
45        let json = serde_json::to_string(event)
46            .map_err(|e| EventSinkError::SinkError(format!("serialization failed: {e}")))?;
47        eprintln!("{json}");
48        Ok(())
49    }
50
51    async fn flush(&self) -> Result<(), EventSinkError> {
52        Ok(())
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    /// **The console sink is registered only when it is asked for**
61    /// (`fr-1019-90`, ruled 2026-10-21): a program that names no sink writes
62    /// no event to its error output; one that wants it builds it with
63    /// `create_sink` from `{ type: stdout }`, or registers `StdoutSink`.
64    #[test]
65    fn the_console_sink_is_registered_only_when_asked() {
66        let linked = crate::PluginRegistryBuilder::new().with_auto_discovered().build().events;
67        let names = linked.sink_names();
68        assert!(!names.iter().any(|name| name == "stdout"), "{names:?}");
69    }
70    use crate::event::{RazeriEvent, WorkflowContext};
71    use chrono::Utc;
72
73    fn test_workflow() -> WorkflowContext {
74        WorkflowContext::new("test-wf").unwrap()
75    }
76
77    #[tokio::test]
78    async fn stdout_sink_accepts_events() {
79        let sink = StdoutSink::new();
80        assert_eq!(sink.name(), "stdout");
81
82        let event = RazeriEvent::design_validated(
83            "test-stdout-1",
84            Utc::now(),
85            test_workflow(),
86            "test.yaml",
87            0,
88            0,
89        )
90        .unwrap();
91        let result = sink.on_event(&event).await;
92        assert!(result.is_ok());
93    }
94
95    #[tokio::test]
96    async fn stdout_sink_flush_is_noop() {
97        let sink = StdoutSink::new();
98        assert!(sink.flush().await.is_ok());
99    }
100}