use crate::{
WireValue, render_any_error_ansi, render_any_error_html, render_any_error_plain,
render_any_error_terminal,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TerminalRenderCaps {
pub ansi: bool,
}
impl TerminalRenderCaps {
pub fn detect() -> Self {
Self {
ansi: terminal_supports_ansi(),
}
}
}
pub trait WireRenderAdapter: Send + Sync {
fn render_terminal(&self, value: &WireValue, caps: TerminalRenderCaps) -> Option<String>;
fn render_html(&self, value: &WireValue) -> Option<String> {
let _ = value;
None
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct AnyErrorWireRenderAdapter;
impl WireRenderAdapter for AnyErrorWireRenderAdapter {
fn render_terminal(&self, value: &WireValue, caps: TerminalRenderCaps) -> Option<String> {
if caps.ansi {
render_any_error_ansi(value)
} else {
render_any_error_plain(value)
}
}
fn render_html(&self, value: &WireValue) -> Option<String> {
render_any_error_html(value)
}
}
#[derive(Default)]
pub struct WireRenderer {
adapters: Vec<Box<dyn WireRenderAdapter>>,
}
impl WireRenderer {
pub fn with_default_adapters() -> Self {
let mut renderer = Self::default();
renderer.adapters.push(Box::new(AnyErrorWireRenderAdapter));
renderer
}
pub fn register_adapter<A: WireRenderAdapter + 'static>(&mut self, adapter: A) {
self.adapters.push(Box::new(adapter));
}
pub fn render_terminal(&self, value: &WireValue) -> Option<String> {
let caps = TerminalRenderCaps::detect();
for adapter in &self.adapters {
if let Some(rendered) = adapter.render_terminal(value, caps) {
return Some(rendered);
}
}
None
}
pub fn render_html(&self, value: &WireValue) -> Option<String> {
for adapter in &self.adapters {
if let Some(rendered) = adapter.render_html(value) {
return Some(rendered);
}
}
None
}
}
pub fn render_wire_terminal(value: &WireValue) -> Option<String> {
if let Some(rendered) = render_any_error_terminal(value) {
return Some(rendered);
}
WireRenderer::with_default_adapters().render_terminal(value)
}
pub fn render_wire_html(value: &WireValue) -> Option<String> {
WireRenderer::with_default_adapters().render_html(value)
}
fn terminal_supports_ansi() -> bool {
if std::env::var_os("NO_COLOR").is_some() {
return false;
}
if std::env::var("CLICOLOR").ok().as_deref() == Some("0") {
return false;
}
if std::env::var_os("FORCE_COLOR").is_some()
|| std::env::var("CLICOLOR_FORCE")
.map(|v| v != "0")
.unwrap_or(false)
{
return true;
}
matches!(std::env::var("TERM"), Ok(term) if !term.is_empty() && term != "dumb")
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
#[derive(Debug, Default, Clone, Copy)]
struct CustomAdapter;
impl WireRenderAdapter for CustomAdapter {
fn render_terminal(&self, value: &WireValue, _caps: TerminalRenderCaps) -> Option<String> {
match value {
WireValue::Object(obj) => obj
.get("kind")
.and_then(WireValue::as_str)
.filter(|kind| *kind == "CustomError")
.map(|_| "custom-rendered".to_string()),
_ => None,
}
}
}
#[test]
fn custom_adapter_extends_terminal_render_path() {
let mut renderer = WireRenderer::with_default_adapters();
renderer.register_adapter(CustomAdapter);
let mut obj = BTreeMap::new();
obj.insert(
"kind".to_string(),
WireValue::String("CustomError".to_string()),
);
let value = WireValue::Object(obj);
let rendered = renderer.render_terminal(&value);
assert_eq!(rendered.as_deref(), Some("custom-rendered"));
}
#[test]
fn default_renderer_handles_anyerror_html() {
let mut payload = BTreeMap::new();
payload.insert(
"category".to_string(),
WireValue::String("AnyError".to_string()),
);
payload.insert("message".to_string(), WireValue::String("boom".to_string()));
payload.insert("payload".to_string(), WireValue::String("boom".to_string()));
payload.insert("trace_info".to_string(), WireValue::Null);
payload.insert("cause".to_string(), WireValue::Null);
let html = render_wire_html(&WireValue::Object(payload)).expect("expected html");
assert!(html.contains("shape-error"));
assert!(html.contains("boom"));
}
}