Skip to main content

shape_runtime/
output_adapter.rs

1//! Output adapter trait for handling print() results
2//!
3//! This allows different execution modes (script vs REPL) to control
4//! how print() output is handled without heuristics.
5
6use shape_value::{PrintResult, ValueWord};
7use std::sync::{Arc, Mutex};
8
9/// Trait for handling print() output
10///
11/// Different execution modes can provide different adapters:
12/// - Scripts: StdoutAdapter (print and discard spans)
13/// - REPL: ReplAdapter (print and preserve spans for reformatting)
14/// - Tests: MockAdapter (capture output)
15pub trait OutputAdapter: Send + Sync {
16    /// Handle print() output
17    ///
18    /// # Arguments
19    /// * `result` - The PrintResult with rendered string and spans
20    ///
21    /// # Returns
22    /// The value to return from print() (Unit for scripts, PrintResult for REPL)
23    fn print(&mut self, result: PrintResult) -> ValueWord;
24
25    /// Handle Content HTML from printing a Content value.
26    /// Default implementation does nothing (terminal adapters don't need HTML).
27    fn print_content_html(&mut self, _html: String) {}
28
29    /// Clone the adapter (for trait object cloning)
30    fn clone_box(&self) -> Box<dyn OutputAdapter>;
31}
32
33// Implement Clone for Box<dyn OutputAdapter>
34impl Clone for Box<dyn OutputAdapter> {
35    fn clone(&self) -> Self {
36        self.clone_box()
37    }
38}
39
40/// Standard output adapter - prints to stdout and discards spans
41///
42/// Used for script execution where spans aren't needed.
43#[derive(Debug, Clone)]
44pub struct StdoutAdapter;
45
46impl OutputAdapter for StdoutAdapter {
47    fn print(&mut self, result: PrintResult) -> ValueWord {
48        // Print the rendered output
49        println!("{}", result.rendered);
50
51        // Return None (traditional print() behavior)
52        ValueWord::none()
53    }
54
55    fn clone_box(&self) -> Box<dyn OutputAdapter> {
56        Box::new(self.clone())
57    }
58}
59
60/// REPL output adapter - prints to stdout and preserves spans
61///
62/// Used in REPL mode to enable post-execution reformatting with :reformat
63#[derive(Debug, Clone)]
64pub struct ReplAdapter;
65
66impl OutputAdapter for ReplAdapter {
67    fn print(&mut self, result: PrintResult) -> ValueWord {
68        // Do NOT print to stdout in REPL mode (let the REPL UI handle display)
69        // Return PrintResult with spans for REPL inspection
70        ValueWord::from_print_result(result)
71    }
72
73    fn clone_box(&self) -> Box<dyn OutputAdapter> {
74        Box::new(self.clone())
75    }
76}
77
78/// Mock adapter for testing - captures output without printing
79#[derive(Debug, Clone, Default)]
80pub struct MockAdapter {
81    /// Captured print outputs
82    pub captured: Vec<String>,
83}
84
85impl MockAdapter {
86    pub fn new() -> Self {
87        MockAdapter {
88            captured: Vec::new(),
89        }
90    }
91
92    /// Get all captured output
93    pub fn output(&self) -> Vec<String> {
94        self.captured.clone()
95    }
96
97    /// Clear captured output
98    pub fn clear(&mut self) {
99        self.captured.clear();
100    }
101}
102
103impl OutputAdapter for MockAdapter {
104    fn print(&mut self, result: PrintResult) -> ValueWord {
105        // Capture instead of printing
106        self.captured.push(result.rendered.clone());
107
108        // Return None (traditional behavior)
109        ValueWord::none()
110    }
111
112    fn clone_box(&self) -> Box<dyn OutputAdapter> {
113        Box::new(self.clone())
114    }
115}
116
117/// Shared capture adapter for host integrations (server/notebook)
118///
119/// Captures rendered print output into shared state so the host can
120/// surface it in API responses without scraping stdout.
121/// Also captures Content HTML when Content values are printed.
122#[derive(Debug, Clone, Default)]
123pub struct SharedCaptureAdapter {
124    captured: Arc<Mutex<Vec<String>>>,
125    content_html: Arc<Mutex<Vec<String>>>,
126}
127
128impl SharedCaptureAdapter {
129    pub fn new() -> Self {
130        Self::default()
131    }
132
133    /// Get all captured output lines.
134    pub fn output(&self) -> Vec<String> {
135        self.captured
136            .lock()
137            .map(|v| v.clone())
138            .unwrap_or_else(|_| Vec::new())
139    }
140
141    /// Clear captured output lines.
142    pub fn clear(&self) {
143        if let Ok(mut v) = self.captured.lock() {
144            v.clear();
145        }
146    }
147
148    /// Push Content HTML captured from print(content_value).
149    pub fn push_content_html(&self, html: String) {
150        if let Ok(mut v) = self.content_html.lock() {
151            v.push(html);
152        }
153    }
154
155    /// Get all captured Content HTML fragments.
156    pub fn content_html(&self) -> Vec<String> {
157        self.content_html
158            .lock()
159            .map(|v| v.clone())
160            .unwrap_or_default()
161    }
162}
163
164impl OutputAdapter for SharedCaptureAdapter {
165    fn print(&mut self, result: PrintResult) -> ValueWord {
166        if let Ok(mut v) = self.captured.lock() {
167            v.push(result.rendered.clone());
168        }
169        ValueWord::none()
170    }
171
172    fn print_content_html(&mut self, html: String) {
173        self.push_content_html(html);
174    }
175
176    fn clone_box(&self) -> Box<dyn OutputAdapter> {
177        Box::new(self.clone())
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use shape_value::PrintSpan;
185    use shape_value::heap_value::HeapValue;
186
187    fn make_test_result() -> PrintResult {
188        PrintResult {
189            rendered: "Test output".to_string(),
190            spans: vec![PrintSpan::Literal {
191                text: "Test output".to_string(),
192                start: 0,
193                end: 11,
194                span_id: "span_1".to_string(),
195            }],
196        }
197    }
198
199    #[test]
200    fn test_stdout_adapter_returns_none() {
201        let mut adapter = StdoutAdapter;
202        let result = make_test_result();
203        let returned = adapter.print(result);
204
205        assert!(returned.is_none());
206    }
207
208    #[test]
209    fn test_repl_adapter_preserves_spans() {
210        let mut adapter = ReplAdapter;
211        let result = make_test_result();
212        let returned = adapter.print(result);
213
214        match returned.as_heap_ref().expect("Expected heap value") {
215            HeapValue::PrintResult(pr) => {
216                assert_eq!(pr.rendered, "Test output");
217                assert_eq!(pr.spans.len(), 1);
218            }
219            other => panic!("Expected PrintResult, got {:?}", other),
220        }
221    }
222
223    #[test]
224    fn test_mock_adapter_captures() {
225        let mut adapter = MockAdapter::new();
226
227        adapter.print(PrintResult {
228            rendered: "Output 1".to_string(),
229            spans: vec![],
230        });
231        adapter.print(PrintResult {
232            rendered: "Output 2".to_string(),
233            spans: vec![],
234        });
235
236        assert_eq!(adapter.output(), vec!["Output 1", "Output 2"]);
237
238        adapter.clear();
239        assert_eq!(adapter.output().len(), 0);
240    }
241
242    #[test]
243    fn test_shared_capture_adapter_captures() {
244        let mut adapter = SharedCaptureAdapter::new();
245
246        adapter.print(PrintResult {
247            rendered: "Output A".to_string(),
248            spans: vec![],
249        });
250        adapter.print(PrintResult {
251            rendered: "Output B".to_string(),
252            spans: vec![],
253        });
254
255        assert_eq!(adapter.output(), vec!["Output A", "Output B"]);
256
257        adapter.clear();
258        assert!(adapter.output().is_empty());
259    }
260}