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    captured_full: Arc<Mutex<Vec<PrintResult>>>,
126    content_html: Arc<Mutex<Vec<String>>>,
127}
128
129impl SharedCaptureAdapter {
130    pub fn new() -> Self {
131        Self::default()
132    }
133
134    /// Get all captured output lines.
135    pub fn output(&self) -> Vec<String> {
136        self.captured
137            .lock()
138            .map(|v| v.clone())
139            .unwrap_or_else(|_| Vec::new())
140    }
141
142    /// Clear captured output lines.
143    pub fn clear(&self) {
144        if let Ok(mut v) = self.captured.lock() {
145            v.clear();
146        }
147    }
148
149    /// Push Content HTML captured from print(content_value).
150    pub fn push_content_html(&self, html: String) {
151        if let Ok(mut v) = self.content_html.lock() {
152            v.push(html);
153        }
154    }
155
156    /// Get all captured Content HTML fragments.
157    pub fn content_html(&self) -> Vec<String> {
158        self.content_html
159            .lock()
160            .map(|v| v.clone())
161            .unwrap_or_default()
162    }
163
164    /// Get all captured full PrintResults (with spans).
165    pub fn print_results(&self) -> Vec<PrintResult> {
166        self.captured_full
167            .lock()
168            .map(|v| v.clone())
169            .unwrap_or_default()
170    }
171}
172
173impl OutputAdapter for SharedCaptureAdapter {
174    fn print(&mut self, result: PrintResult) -> ValueWord {
175        if let Ok(mut v) = self.captured.lock() {
176            v.push(result.rendered.clone());
177        }
178        if let Ok(mut v) = self.captured_full.lock() {
179            v.push(result);
180        }
181        ValueWord::none()
182    }
183
184    fn print_content_html(&mut self, html: String) {
185        self.push_content_html(html);
186    }
187
188    fn clone_box(&self) -> Box<dyn OutputAdapter> {
189        Box::new(self.clone())
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use shape_value::PrintSpan;
197    use shape_value::heap_value::HeapValue;
198
199    fn make_test_result() -> PrintResult {
200        PrintResult {
201            rendered: "Test output".to_string(),
202            spans: vec![PrintSpan::Literal {
203                text: "Test output".to_string(),
204                start: 0,
205                end: 11,
206                span_id: "span_1".to_string(),
207            }],
208        }
209    }
210
211    #[test]
212    fn test_stdout_adapter_returns_none() {
213        let mut adapter = StdoutAdapter;
214        let result = make_test_result();
215        let returned = adapter.print(result);
216
217        assert!(returned.is_none());
218    }
219
220    #[test]
221    fn test_repl_adapter_preserves_spans() {
222        let mut adapter = ReplAdapter;
223        let result = make_test_result();
224        let returned = adapter.print(result);
225
226        match returned.as_heap_ref().expect("Expected heap value") {
227            HeapValue::PrintResult(pr) => {
228                assert_eq!(pr.rendered, "Test output");
229                assert_eq!(pr.spans.len(), 1);
230            }
231            other => panic!("Expected PrintResult, got {:?}", other),
232        }
233    }
234
235    #[test]
236    fn test_mock_adapter_captures() {
237        let mut adapter = MockAdapter::new();
238
239        adapter.print(PrintResult {
240            rendered: "Output 1".to_string(),
241            spans: vec![],
242        });
243        adapter.print(PrintResult {
244            rendered: "Output 2".to_string(),
245            spans: vec![],
246        });
247
248        assert_eq!(adapter.output(), vec!["Output 1", "Output 2"]);
249
250        adapter.clear();
251        assert_eq!(adapter.output().len(), 0);
252    }
253
254    #[test]
255    fn test_shared_capture_adapter_captures() {
256        let mut adapter = SharedCaptureAdapter::new();
257
258        adapter.print(PrintResult {
259            rendered: "Output A".to_string(),
260            spans: vec![],
261        });
262        adapter.print(PrintResult {
263            rendered: "Output B".to_string(),
264            spans: vec![],
265        });
266
267        assert_eq!(adapter.output(), vec!["Output A", "Output B"]);
268
269        adapter.clear();
270        assert!(adapter.output().is_empty());
271    }
272}