use std::sync::Arc;
use miette::{GraphicalTheme, MietteHandlerOpts, NamedSource};
pub use miette::{LabeledSpan, SourceSpan};
pub fn install_miette_handler(no_color: bool) -> miette::Result<()> {
miette::set_hook(Box::new(move |_| {
let theme = if no_color {
GraphicalTheme::unicode_nocolor()
} else {
GraphicalTheme::unicode()
};
Box::new(
MietteHandlerOpts::new()
.graphical_theme(theme)
.context_lines(3)
.build(),
)
}))?;
miette::set_panic_hook();
Ok(())
}
pub fn named_source_from_bytes(name: impl AsRef<str>, bytes: &[u8]) -> NamedSource<Arc<[u8]>> {
NamedSource::new(name, Arc::<[u8]>::from(bytes))
}
pub fn named_source_from_str(name: impl AsRef<str>, text: &str) -> NamedSource<String> {
NamedSource::new(name, text.to_string())
}
pub fn pretty_json_for_display(body: &[u8]) -> Arc<[u8]> {
match serde_json::from_slice::<serde_json::Value>(body) {
Ok(value) => match serde_json::to_vec_pretty(&value) {
Ok(pretty) => Arc::from(pretty),
Err(_) => Arc::from(body),
},
Err(_) => Arc::from(body),
}
}
pub fn span_at_line_column(body: &[u8], line: usize, column: usize) -> SourceSpan {
if body.is_empty() {
return SourceSpan::new(0.into(), 0);
}
if line == 0 {
let end = body.len().saturating_sub(1);
return SourceSpan::new(end.into(), 1);
}
let mut current_line = 1usize;
let mut line_start = 0usize;
for (offset, &byte) in body.iter().enumerate() {
if current_line == line {
let line_end = body[line_start..]
.iter()
.position(|&b| b == b'\n')
.map(|rel| line_start + rel)
.unwrap_or(body.len());
let column_offset = column.saturating_sub(1);
let span_start = line_start + column_offset;
if span_start < line_end {
return SourceSpan::new(span_start.into(), 1);
} else {
let len = line_end.saturating_sub(line_start).max(1);
return SourceSpan::new(line_start.into(), len);
}
}
if byte == b'\n' {
current_line += 1;
line_start = offset + 1;
}
}
let end = body.len().saturating_sub(1);
SourceSpan::new(end.into(), 1)
}
pub fn span_for_quoted_literal(bytes: &[u8], literal: &str) -> Option<SourceSpan> {
let search = format!("\"{literal}\"");
bytes
.windows(search.len())
.position(|w| w == search.as_bytes())
.map(|pos| SourceSpan::new(pos.into(), search.len()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pretty_json_for_display_wraps_compact_body() {
let compact = br#"{"a":1,"b":[2,3]}"#;
let pretty = pretty_json_for_display(compact);
let text = std::str::from_utf8(&pretty).unwrap();
assert!(
text.contains('\n'),
"pretty-printed body should be multi-line"
);
assert!(text.contains("\"a\""));
}
#[test]
fn pretty_json_for_display_passes_non_json_through() {
let garbage = b"not valid json <<<";
let out = pretty_json_for_display(garbage);
assert_eq!(out.as_ref(), garbage);
}
#[test]
fn span_at_line_column_known_location() {
let body = b"line1\nline2\nline3";
let span = span_at_line_column(body, 2, 3);
assert_eq!(span.offset(), 8);
assert_eq!(span.len(), 1);
}
#[test]
fn span_at_line_column_unknown_line_sentinel() {
let body = b"abc";
let span = span_at_line_column(body, 0, 0);
assert_eq!(span.offset(), 2);
assert_eq!(span.len(), 1);
}
#[test]
fn span_at_line_column_empty_body() {
let span = span_at_line_column(b"", 1, 1);
assert_eq!(span.offset(), 0);
assert_eq!(span.len(), 0);
}
#[test]
fn span_at_line_column_column_past_end_of_line() {
let body = b"ab\ncd\n";
let span = span_at_line_column(body, 1, 99);
assert_eq!(span.offset(), 0);
assert_eq!(span.len(), 2);
}
#[test]
fn span_for_quoted_literal_finds_key() {
let json = br#"{"service": [], "other": 123}"#;
let span = span_for_quoted_literal(json, "service").unwrap();
assert_eq!(
&json[span.offset()..span.offset() + span.len()],
b"\"service\""
);
}
#[test]
fn span_for_quoted_literal_finds_value() {
let json = br#"{"serviceEndpoint": "https://example.com"}"#;
let span = span_for_quoted_literal(json, "https://example.com").unwrap();
assert_eq!(
&json[span.offset()..span.offset() + span.len()],
b"\"https://example.com\""
);
}
#[test]
fn span_for_quoted_literal_missing_returns_none() {
let json = br#"{"other": 123}"#;
assert!(span_for_quoted_literal(json, "service").is_none());
}
}