razeri-plugin-api 0.17.0

Extension traits and registries for Razeri plugins
Documentation
// Copyright (C) 2026 Gabriel Shea
// Licensed under either of the Apache License, Version 2.0 or the MIT
// license, at your option.

//! Stdout event sink — writes events as JSON to stderr.
//!
//! Uses stderr rather than stdout so it does not interfere with
//! structured program output on stdout.
//!
//! **Registered only when asked for**: a program that names no sink writes no
//! event to its error output. A host that wants it — a container whose
//! platform keeps stderr, say — builds it with
//! [`create_sink`](crate::sinks::create_sink) from `{ type: stdout }`, or
//! registers [`StdoutSink`] on its [`EventRegistry`](crate::EventRegistry).

use async_trait::async_trait;

use crate::error::EventSinkError;
use crate::event::{EventSink, RazeriEvent};

/// Writes each event as a single-line JSON object to stderr.
pub struct StdoutSink;

impl StdoutSink {
    /// Create a new `StdoutSink`.
    #[must_use]
    pub fn new() -> Self {
        Self
    }
}

impl Default for StdoutSink {
    fn default() -> Self {
        Self::new()
    }
}

#[async_trait]
impl EventSink for StdoutSink {
    fn name(&self) -> &'static str {
        "stdout"
    }

    async fn on_event(&self, event: &RazeriEvent) -> Result<(), EventSinkError> {
        let json = serde_json::to_string(event)
            .map_err(|e| EventSinkError::SinkError(format!("serialization failed: {e}")))?;
        eprintln!("{json}");
        Ok(())
    }

    async fn flush(&self) -> Result<(), EventSinkError> {
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// **The console sink is registered only when it is asked for**
    /// (`fr-1019-90`, ruled 2026-10-21): a program that names no sink writes
    /// no event to its error output; one that wants it builds it with
    /// `create_sink` from `{ type: stdout }`, or registers `StdoutSink`.
    #[test]
    fn the_console_sink_is_registered_only_when_asked() {
        let linked = crate::PluginRegistryBuilder::new().with_auto_discovered().build().events;
        let names = linked.sink_names();
        assert!(!names.iter().any(|name| name == "stdout"), "{names:?}");
    }
    use crate::event::{RazeriEvent, WorkflowContext};
    use chrono::Utc;

    fn test_workflow() -> WorkflowContext {
        WorkflowContext::new("test-wf").unwrap()
    }

    #[tokio::test]
    async fn stdout_sink_accepts_events() {
        let sink = StdoutSink::new();
        assert_eq!(sink.name(), "stdout");

        let event = RazeriEvent::design_validated(
            "test-stdout-1",
            Utc::now(),
            test_workflow(),
            "test.yaml",
            0,
            0,
        )
        .unwrap();
        let result = sink.on_event(&event).await;
        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn stdout_sink_flush_is_noop() {
        let sink = StdoutSink::new();
        assert!(sink.flush().await.is_ok());
    }
}