Skip to main content

cuenv_events/renderers/
json.rs

1//! JSON renderer for cuenv events.
2//!
3//! Renders events as JSON lines for machine consumption.
4//! This module is allowed to use println! as it's the output layer.
5
6#![allow(clippy::print_stdout)]
7
8use crate::bus::EventReceiver;
9use crate::event::{CuenvEvent, EventCategory, SystemEvent};
10
11/// JSON renderer that outputs events as JSON lines.
12#[derive(Debug, Default)]
13pub struct JsonRenderer {
14    /// Whether to pretty-print JSON.
15    pretty: bool,
16}
17
18impl JsonRenderer {
19    /// Create a new JSON renderer with compact output.
20    #[must_use]
21    pub const fn new() -> Self {
22        Self { pretty: false }
23    }
24
25    /// Create a new JSON renderer with pretty-printed output.
26    #[must_use]
27    pub const fn pretty() -> Self {
28        Self { pretty: true }
29    }
30
31    /// Run the renderer, consuming events from the receiver.
32    ///
33    /// The renderer will exit gracefully when it receives a `SystemEvent::Shutdown` event,
34    /// ensuring all pending events are processed before termination.
35    pub async fn run(self, mut receiver: EventReceiver) {
36        while let Some(event) = receiver.recv().await {
37            self.render(&event);
38            // Exit after rendering shutdown event
39            if matches!(event.category, EventCategory::System(SystemEvent::Shutdown)) {
40                break;
41            }
42        }
43    }
44
45    /// Render a single event as JSON.
46    pub fn render(&self, event: &CuenvEvent) {
47        let json = if self.pretty {
48            serde_json::to_string_pretty(event)
49        } else {
50            serde_json::to_string(event)
51        };
52
53        if let Ok(json) = json {
54            println!("{json}");
55        }
56    }
57
58    /// Render a single event to a string (for testing).
59    #[must_use]
60    pub fn render_to_string(&self, event: &CuenvEvent) -> Option<String> {
61        if self.pretty {
62            serde_json::to_string_pretty(event).ok()
63        } else {
64            serde_json::to_string(event).ok()
65        }
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use crate::event::{EventCategory, EventSource, OutputEvent};
73    use uuid::Uuid;
74
75    fn create_test_event() -> CuenvEvent {
76        CuenvEvent::new(
77            Uuid::nil(),
78            EventSource::new("test::target"),
79            EventCategory::Output(OutputEvent::Stdout {
80                content: "Test message".to_string(),
81            }),
82        )
83    }
84
85    #[test]
86    fn test_json_renderer_new_is_compact() {
87        let renderer = JsonRenderer::new();
88        assert!(!renderer.pretty);
89    }
90
91    #[test]
92    fn test_json_renderer_pretty() {
93        let renderer = JsonRenderer::pretty();
94        assert!(renderer.pretty);
95    }
96
97    #[test]
98    fn test_json_renderer_default_is_compact() {
99        let renderer = JsonRenderer::default();
100        assert!(!renderer.pretty);
101    }
102
103    #[test]
104    fn test_json_renderer_debug() {
105        let renderer = JsonRenderer::new();
106        let debug = format!("{renderer:?}");
107        assert!(debug.contains("JsonRenderer"));
108        assert!(debug.contains("pretty"));
109    }
110
111    #[test]
112    fn test_render_to_string_compact() {
113        let renderer = JsonRenderer::new();
114        let event = create_test_event();
115
116        let json = renderer.render_to_string(&event);
117        assert!(json.is_some());
118
119        let json_str = json.unwrap();
120        // Compact output should not have newlines in the middle
121        assert!(!json_str.contains("\n  "));
122        // Should contain expected fields
123        assert!(json_str.contains("\"id\""));
124        assert!(json_str.contains("\"correlation_id\""));
125        assert!(json_str.contains("test::target"));
126        assert!(json_str.contains("Test message"));
127    }
128
129    #[test]
130    fn test_render_to_string_pretty() {
131        let renderer = JsonRenderer::pretty();
132        let event = create_test_event();
133
134        let json = renderer.render_to_string(&event);
135        assert!(json.is_some());
136
137        let json_str = json.unwrap();
138        // Pretty output should have newlines and indentation
139        assert!(json_str.contains('\n'));
140        assert!(json_str.contains("  "));
141        // Should still contain expected fields
142        assert!(json_str.contains("\"id\""));
143        assert!(json_str.contains("test::target"));
144    }
145
146    #[test]
147    fn test_render_to_string_is_valid_json() {
148        let renderer = JsonRenderer::new();
149        let event = create_test_event();
150
151        let json_str = renderer.render_to_string(&event).unwrap();
152
153        // Should be valid JSON that can be parsed back
154        let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
155        assert!(parsed.is_object());
156        assert!(parsed.get("id").is_some());
157        assert!(parsed.get("timestamp").is_some());
158        assert!(parsed.get("source").is_some());
159        assert!(parsed.get("category").is_some());
160    }
161
162    #[test]
163    fn test_render_to_string_pretty_is_valid_json() {
164        let renderer = JsonRenderer::pretty();
165        let event = create_test_event();
166
167        let json_str = renderer.render_to_string(&event).unwrap();
168
169        // Pretty-printed JSON should also be valid
170        let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
171        assert!(parsed.is_object());
172    }
173}