CLI app for developers prototyping atproto functionality
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 278 lines 10 kB view raw
1//! Shared miette configuration and `NamedSource` helpers. 2 3// pattern: Mixed (unavoidable) 4// 5// `install_miette_handler` mutates global process state via 6// `miette::set_hook` (a side effect, called once from `cli::run`); the 7// other helpers (`named_source_from_bytes`, `pretty_json_for_display`, 8// `span_at_line_column`, `span_for_quoted_literal`) are pure and could 9// live in a sibling file, but the module is small enough that splitting 10// would obscure the shared diagnostic concern. 11 12use std::sync::Arc; 13 14use miette::{GraphicalTheme, MietteHandlerOpts, NamedSource, SourceSpan}; 15 16/// Install the miette panic hook and graphical report handler. 17/// 18/// Honours `NO_COLOR=1` by dropping to an unstyled theme. Call this exactly once 19/// from `main` before any `miette::Result`-returning code runs. 20pub fn install_miette_handler(no_color: bool) -> miette::Result<()> { 21 // `NO_COLOR` is also respected automatically by miette when set in the 22 // environment; passing an explicit theme here covers the `--no-color` flag 23 // path without having to touch process-wide env vars (which is `unsafe` in 24 // Rust 2024). 25 miette::set_hook(Box::new(move |_| { 26 let theme = if no_color { 27 GraphicalTheme::unicode_nocolor() 28 } else { 29 GraphicalTheme::unicode() 30 }; 31 Box::new( 32 MietteHandlerOpts::new() 33 .graphical_theme(theme) 34 .context_lines(3) 35 .build(), 36 ) 37 }))?; 38 39 // Install miette's panic hook so panics render through the same handler. 40 miette::set_panic_hook(); 41 42 Ok(()) 43} 44 45/// Build a `NamedSource` from a name and raw bytes. 46pub(crate) fn named_source_from_bytes( 47 name: impl AsRef<str>, 48 bytes: Arc<[u8]>, 49) -> NamedSource<Arc<[u8]>> { 50 NamedSource::new(name, bytes) 51} 52 53/// Build a `NamedSource` from a name and a slice. 54/// 55/// The bytes are cloned into an `Arc<[u8]>` via miette's constructor, 56/// so callers may drop the original slice after this returns. 57pub(crate) fn named_source_from_slice( 58 name: impl AsRef<str>, 59 bytes: &[u8], 60) -> NamedSource<Arc<[u8]>> { 61 NamedSource::new(name, Arc::<[u8]>::from(bytes)) 62} 63 64/// Pretty-print `body` as JSON for display in a `NamedSource`. 65/// 66/// Real atproto servers routinely emit JSON as a single enormous line, which 67/// makes miette's source-span visualization illegible. Any diagnostic that 68/// embeds a JSON payload should run it through this helper first so that 69/// line-based caret rendering lands somewhere readable. 70/// 71/// If `body` parses as JSON, returns an `Arc<[u8]>` holding the pretty-printed 72/// form. If it does not parse, or re-serialization fails, returns an `Arc<[u8]>` 73/// containing the original bytes unchanged so callers can still hand something 74/// to `NamedSource`. 75/// 76/// Spans computed against the returned bytes must be derived from those same 77/// bytes — do not mix a pretty body with a span calculated against the raw 78/// body, or vice versa. 79pub fn pretty_json_for_display(body: &[u8]) -> Arc<[u8]> { 80 match serde_json::from_slice::<serde_json::Value>(body) { 81 Ok(value) => match serde_json::to_vec_pretty(&value) { 82 Ok(pretty) => Arc::from(pretty), 83 Err(_) => Arc::from(body), 84 }, 85 Err(_) => Arc::from(body), 86 } 87} 88 89/// Convert a 1-based `(line, column)` pair (as produced by `serde_json::Error`) 90/// into a `SourceSpan` pointing at that byte inside `body`. 91/// 92/// The returned span has length 1 so miette renders a caret at the exact 93/// failure site. `line == 0` is the `serde_json` sentinel for "unknown 94/// location" and produces a 1-byte span at the last byte of `body`. If the 95/// column runs past the end of the matched line, the span is clamped to the 96/// last byte of that line. 97pub fn span_at_line_column(body: &[u8], line: usize, column: usize) -> SourceSpan { 98 if body.is_empty() { 99 return SourceSpan::new(0.into(), 0); 100 } 101 if line == 0 { 102 let end = body.len().saturating_sub(1); 103 return SourceSpan::new(end.into(), 1); 104 } 105 let mut current_line = 1usize; 106 let mut line_start = 0usize; 107 for (offset, &byte) in body.iter().enumerate() { 108 if current_line == line { 109 let line_end = body[line_start..] 110 .iter() 111 .position(|&b| b == b'\n') 112 .map(|rel| line_start + rel) 113 .unwrap_or(body.len()); 114 let column_offset = column.saturating_sub(1); 115 let span_start = line_start + column_offset; 116 if span_start < line_end { 117 return SourceSpan::new(span_start.into(), 1); 118 } else { 119 let len = line_end.saturating_sub(line_start).max(1); 120 return SourceSpan::new(line_start.into(), len); 121 } 122 } 123 if byte == b'\n' { 124 current_line += 1; 125 line_start = offset + 1; 126 } 127 } 128 // Requested line is past the end of the body — clamp to the last byte. 129 let end = body.len().saturating_sub(1); 130 SourceSpan::new(end.into(), 1) 131} 132 133/// Find the span of a JSON quoted literal (key or string value) inside `bytes`. 134/// 135/// Scans for the literal `"<literal>"` pattern and returns the span covering 136/// the entire quoted string including the quotes. This works for both JSON 137/// keys and string values since both are quoted identically in JSON. Returns 138/// `None` if the literal is not present. 139/// 140/// A substring search is acceptable here because the payloads we render are 141/// always small (DID documents, single records, query responses) and we only 142/// invoke this to highlight an already-extracted key or value — so the risk 143/// of a false match inside an unrelated string is negligible in practice. 144pub fn span_for_quoted_literal(bytes: &[u8], literal: &str) -> Option<SourceSpan> { 145 let search = format!("\"{literal}\""); 146 bytes 147 .windows(search.len()) 148 .position(|w| w == search.as_bytes()) 149 .map(|pos| SourceSpan::new(pos.into(), search.len())) 150} 151 152/// Find all spans of a JSON quoted literal inside `bytes`. 153/// 154/// Like `span_for_quoted_literal` but returns every occurrence of the literal 155/// rather than just the first one. Returns a Vec of SourceSpans, empty if no 156/// matches found. 157pub fn all_spans_for_quoted_literal(bytes: &[u8], literal: &str) -> Vec<SourceSpan> { 158 let search = format!("\"{literal}\""); 159 let search_bytes = search.as_bytes(); 160 let mut spans = Vec::new(); 161 let mut start = 0; 162 while start + search_bytes.len() <= bytes.len() { 163 if let Some(rel) = bytes[start..] 164 .windows(search_bytes.len()) 165 .position(|w| w == search_bytes) 166 { 167 let abs = start + rel; 168 spans.push(SourceSpan::new(abs.into(), search_bytes.len())); 169 start = abs + search_bytes.len(); 170 } else { 171 break; 172 } 173 } 174 spans 175} 176 177#[cfg(test)] 178mod tests { 179 use super::*; 180 181 #[test] 182 fn pretty_json_for_display_wraps_compact_body() { 183 let compact = br#"{"a":1,"b":[2,3]}"#; 184 let pretty = pretty_json_for_display(compact); 185 let text = std::str::from_utf8(&pretty).unwrap(); 186 assert!( 187 text.contains('\n'), 188 "pretty-printed body should be multi-line" 189 ); 190 assert!(text.contains("\"a\"")); 191 } 192 193 #[test] 194 fn pretty_json_for_display_passes_non_json_through() { 195 let garbage = b"not valid json <<<"; 196 let out = pretty_json_for_display(garbage); 197 assert_eq!(out.as_ref(), garbage); 198 } 199 200 #[test] 201 fn span_at_line_column_known_location() { 202 let body = b"line1\nline2\nline3"; 203 let span = span_at_line_column(body, 2, 3); 204 // "line2" starts at byte 6, column 3 (1-based) is byte 8 ('n'). 205 assert_eq!(span.offset(), 8); 206 assert_eq!(span.len(), 1); 207 } 208 209 #[test] 210 fn span_at_line_column_unknown_line_sentinel() { 211 let body = b"abc"; 212 let span = span_at_line_column(body, 0, 0); 213 assert_eq!(span.offset(), 2); 214 assert_eq!(span.len(), 1); 215 } 216 217 #[test] 218 fn span_at_line_column_empty_body() { 219 let span = span_at_line_column(b"", 1, 1); 220 assert_eq!(span.offset(), 0); 221 assert_eq!(span.len(), 0); 222 } 223 224 #[test] 225 fn span_at_line_column_column_past_end_of_line() { 226 let body = b"ab\ncd\n"; 227 let span = span_at_line_column(body, 1, 99); 228 // Clamped to the remainder of line 1. 229 assert_eq!(span.offset(), 0); 230 assert_eq!(span.len(), 2); 231 } 232 233 #[test] 234 fn span_for_quoted_literal_finds_key() { 235 let json = br#"{"service": [], "other": 123}"#; 236 let span = span_for_quoted_literal(json, "service").unwrap(); 237 assert_eq!( 238 &json[span.offset()..span.offset() + span.len()], 239 b"\"service\"" 240 ); 241 } 242 243 #[test] 244 fn span_for_quoted_literal_finds_value() { 245 let json = br#"{"serviceEndpoint": "https://example.com"}"#; 246 let span = span_for_quoted_literal(json, "https://example.com").unwrap(); 247 assert_eq!( 248 &json[span.offset()..span.offset() + span.len()], 249 b"\"https://example.com\"" 250 ); 251 } 252 253 #[test] 254 fn span_for_quoted_literal_missing_returns_none() { 255 let json = br#"{"other": 123}"#; 256 assert!(span_for_quoted_literal(json, "service").is_none()); 257 } 258 259 #[test] 260 fn all_spans_for_quoted_literal_finds_all_occurrences() { 261 let json = br#"{"kid":"k1","keys":[{"kid":"k1"}]}"#; 262 let spans = all_spans_for_quoted_literal(json, "k1"); 263 assert_eq!(spans.len(), 2); 264 // First occurrence at position 7 265 assert_eq!(spans[0].offset(), 7); 266 assert_eq!(spans[0].len(), 4); // "k1" 267 // Second occurrence at position 27 268 assert_eq!(spans[1].offset(), 27); 269 assert_eq!(spans[1].len(), 4); 270 } 271 272 #[test] 273 fn all_spans_for_quoted_literal_missing_returns_empty() { 274 let json = br#"{"other": 123}"#; 275 let spans = all_spans_for_quoted_literal(json, "missing"); 276 assert!(spans.is_empty()); 277 } 278}