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    /// Clone the adapter (for trait object cloning)
26    fn clone_box(&self) -> Box<dyn OutputAdapter>;
27}
28
29// Implement Clone for Box<dyn OutputAdapter>
30impl Clone for Box<dyn OutputAdapter> {
31    fn clone(&self) -> Self {
32        self.clone_box()
33    }
34}
35
36/// Standard output adapter - prints to stdout and discards spans
37///
38/// Used for script execution where spans aren't needed.
39#[derive(Debug, Clone)]
40pub struct StdoutAdapter;
41
42impl OutputAdapter for StdoutAdapter {
43    fn print(&mut self, result: PrintResult) -> ValueWord {
44        // Print the rendered output
45        println!("{}", result.rendered);
46
47        // Return None (traditional print() behavior)
48        ValueWord::none()
49    }
50
51    fn clone_box(&self) -> Box<dyn OutputAdapter> {
52        Box::new(self.clone())
53    }
54}
55
56/// REPL output adapter - prints to stdout and preserves spans
57///
58/// Used in REPL mode to enable post-execution reformatting with :reformat
59#[derive(Debug, Clone)]
60pub struct ReplAdapter;
61
62impl OutputAdapter for ReplAdapter {
63    fn print(&mut self, result: PrintResult) -> ValueWord {
64        // Do NOT print to stdout in REPL mode (let the REPL UI handle display)
65        // Return PrintResult with spans for REPL inspection
66        ValueWord::from_print_result(result)
67    }
68
69    fn clone_box(&self) -> Box<dyn OutputAdapter> {
70        Box::new(self.clone())
71    }
72}
73
74/// Mock adapter for testing - captures output without printing
75#[derive(Debug, Clone, Default)]
76pub struct MockAdapter {
77    /// Captured print outputs
78    pub captured: Vec<String>,
79}
80
81impl MockAdapter {
82    pub fn new() -> Self {
83        MockAdapter {
84            captured: Vec::new(),
85        }
86    }
87
88    /// Get all captured output
89    pub fn output(&self) -> Vec<String> {
90        self.captured.clone()
91    }
92
93    /// Clear captured output
94    pub fn clear(&mut self) {
95        self.captured.clear();
96    }
97}
98
99impl OutputAdapter for MockAdapter {
100    fn print(&mut self, result: PrintResult) -> ValueWord {
101        // Capture instead of printing
102        self.captured.push(result.rendered.clone());
103
104        // Return None (traditional behavior)
105        ValueWord::none()
106    }
107
108    fn clone_box(&self) -> Box<dyn OutputAdapter> {
109        Box::new(self.clone())
110    }
111}
112
113/// Shared capture adapter for host integrations (server/notebook)
114///
115/// Captures rendered print output into shared state so the host can
116/// surface it in API responses without scraping stdout.
117#[derive(Debug, Clone, Default)]
118pub struct SharedCaptureAdapter {
119    captured: Arc<Mutex<Vec<String>>>,
120}
121
122impl SharedCaptureAdapter {
123    pub fn new() -> Self {
124        Self::default()
125    }
126
127    /// Get all captured output lines.
128    pub fn output(&self) -> Vec<String> {
129        self.captured
130            .lock()
131            .map(|v| v.clone())
132            .unwrap_or_else(|_| Vec::new())
133    }
134
135    /// Clear captured output lines.
136    pub fn clear(&self) {
137        if let Ok(mut v) = self.captured.lock() {
138            v.clear();
139        }
140    }
141}
142
143impl OutputAdapter for SharedCaptureAdapter {
144    fn print(&mut self, result: PrintResult) -> ValueWord {
145        if let Ok(mut v) = self.captured.lock() {
146            v.push(result.rendered.clone());
147        }
148        ValueWord::none()
149    }
150
151    fn clone_box(&self) -> Box<dyn OutputAdapter> {
152        Box::new(self.clone())
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use shape_value::PrintSpan;
160    use shape_value::heap_value::HeapValue;
161
162    fn make_test_result() -> PrintResult {
163        PrintResult {
164            rendered: "Test output".to_string(),
165            spans: vec![PrintSpan::Literal {
166                text: "Test output".to_string(),
167                start: 0,
168                end: 11,
169                span_id: "span_1".to_string(),
170            }],
171        }
172    }
173
174    #[test]
175    fn test_stdout_adapter_returns_none() {
176        let mut adapter = StdoutAdapter;
177        let result = make_test_result();
178        let returned = adapter.print(result);
179
180        assert!(returned.is_none());
181    }
182
183    #[test]
184    fn test_repl_adapter_preserves_spans() {
185        let mut adapter = ReplAdapter;
186        let result = make_test_result();
187        let returned = adapter.print(result);
188
189        match returned.as_heap_ref().expect("Expected heap value") {
190            HeapValue::PrintResult(pr) => {
191                assert_eq!(pr.rendered, "Test output");
192                assert_eq!(pr.spans.len(), 1);
193            }
194            other => panic!("Expected PrintResult, got {:?}", other),
195        }
196    }
197
198    #[test]
199    fn test_mock_adapter_captures() {
200        let mut adapter = MockAdapter::new();
201
202        adapter.print(PrintResult {
203            rendered: "Output 1".to_string(),
204            spans: vec![],
205        });
206        adapter.print(PrintResult {
207            rendered: "Output 2".to_string(),
208            spans: vec![],
209        });
210
211        assert_eq!(adapter.output(), vec!["Output 1", "Output 2"]);
212
213        adapter.clear();
214        assert_eq!(adapter.output().len(), 0);
215    }
216
217    #[test]
218    fn test_shared_capture_adapter_captures() {
219        let mut adapter = SharedCaptureAdapter::new();
220
221        adapter.print(PrintResult {
222            rendered: "Output A".to_string(),
223            spans: vec![],
224        });
225        adapter.print(PrintResult {
226            rendered: "Output B".to_string(),
227            spans: vec![],
228        });
229
230        assert_eq!(adapter.output(), vec!["Output A", "Output B"]);
231
232        adapter.clear();
233        assert!(adapter.output().is_empty());
234    }
235}