1use crate::{
7 WireValue, render_any_error_ansi, render_any_error_html, render_any_error_plain,
8 render_any_error_terminal,
9};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct TerminalRenderCaps {
14 pub ansi: bool,
15}
16
17impl TerminalRenderCaps {
18 pub fn detect() -> Self {
19 Self {
20 ansi: terminal_supports_ansi(),
21 }
22 }
23}
24
25pub trait WireRenderAdapter: Send + Sync {
30 fn render_terminal(&self, value: &WireValue, caps: TerminalRenderCaps) -> Option<String>;
31
32 fn render_html(&self, value: &WireValue) -> Option<String> {
33 let _ = value;
34 None
35 }
36}
37
38#[derive(Debug, Default, Clone, Copy)]
40pub struct AnyErrorWireRenderAdapter;
41
42impl WireRenderAdapter for AnyErrorWireRenderAdapter {
43 fn render_terminal(&self, value: &WireValue, caps: TerminalRenderCaps) -> Option<String> {
44 if caps.ansi {
45 render_any_error_ansi(value)
46 } else {
47 render_any_error_plain(value)
48 }
49 }
50
51 fn render_html(&self, value: &WireValue) -> Option<String> {
52 render_any_error_html(value)
53 }
54}
55
56#[derive(Default)]
58pub struct WireRenderer {
59 adapters: Vec<Box<dyn WireRenderAdapter>>,
60}
61
62impl WireRenderer {
63 pub fn with_default_adapters() -> Self {
64 let mut renderer = Self::default();
65 renderer.adapters.push(Box::new(AnyErrorWireRenderAdapter));
66 renderer
67 }
68
69 pub fn register_adapter<A: WireRenderAdapter + 'static>(&mut self, adapter: A) {
70 self.adapters.push(Box::new(adapter));
71 }
72
73 pub fn render_terminal(&self, value: &WireValue) -> Option<String> {
74 let caps = TerminalRenderCaps::detect();
75 for adapter in &self.adapters {
76 if let Some(rendered) = adapter.render_terminal(value, caps) {
77 return Some(rendered);
78 }
79 }
80 None
81 }
82
83 pub fn render_html(&self, value: &WireValue) -> Option<String> {
84 for adapter in &self.adapters {
85 if let Some(rendered) = adapter.render_html(value) {
86 return Some(rendered);
87 }
88 }
89 None
90 }
91}
92
93pub fn render_wire_terminal(value: &WireValue) -> Option<String> {
95 if let Some(rendered) = render_any_error_terminal(value) {
98 return Some(rendered);
99 }
100 WireRenderer::with_default_adapters().render_terminal(value)
101}
102
103pub fn render_wire_html(value: &WireValue) -> Option<String> {
105 WireRenderer::with_default_adapters().render_html(value)
106}
107
108fn terminal_supports_ansi() -> bool {
109 if std::env::var_os("NO_COLOR").is_some() {
110 return false;
111 }
112
113 if std::env::var("CLICOLOR").ok().as_deref() == Some("0") {
114 return false;
115 }
116
117 if std::env::var_os("FORCE_COLOR").is_some()
118 || std::env::var("CLICOLOR_FORCE")
119 .map(|v| v != "0")
120 .unwrap_or(false)
121 {
122 return true;
123 }
124
125 matches!(std::env::var("TERM"), Ok(term) if !term.is_empty() && term != "dumb")
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use std::collections::BTreeMap;
132
133 #[derive(Debug, Default, Clone, Copy)]
134 struct CustomAdapter;
135
136 impl WireRenderAdapter for CustomAdapter {
137 fn render_terminal(&self, value: &WireValue, _caps: TerminalRenderCaps) -> Option<String> {
138 match value {
139 WireValue::Object(obj) => obj
140 .get("kind")
141 .and_then(WireValue::as_str)
142 .filter(|kind| *kind == "CustomError")
143 .map(|_| "custom-rendered".to_string()),
144 _ => None,
145 }
146 }
147 }
148
149 #[test]
150 fn custom_adapter_extends_terminal_render_path() {
151 let mut renderer = WireRenderer::with_default_adapters();
152 renderer.register_adapter(CustomAdapter);
153
154 let mut obj = BTreeMap::new();
155 obj.insert(
156 "kind".to_string(),
157 WireValue::String("CustomError".to_string()),
158 );
159 let value = WireValue::Object(obj);
160
161 let rendered = renderer.render_terminal(&value);
162 assert_eq!(rendered.as_deref(), Some("custom-rendered"));
163 }
164
165 #[test]
166 fn default_renderer_handles_anyerror_html() {
167 let mut payload = BTreeMap::new();
168 payload.insert(
169 "category".to_string(),
170 WireValue::String("AnyError".to_string()),
171 );
172 payload.insert("message".to_string(), WireValue::String("boom".to_string()));
173 payload.insert("payload".to_string(), WireValue::String("boom".to_string()));
174 payload.insert("trace_info".to_string(), WireValue::Null);
175 payload.insert("cause".to_string(), WireValue::Null);
176
177 let html = render_wire_html(&WireValue::Object(payload)).expect("expected html");
178 assert!(html.contains("shape-error"));
179 assert!(html.contains("boom"));
180 }
181}