atproto_devtool/common/
diagnostics.rs1use std::sync::Arc;
4
5use miette::{GraphicalTheme, MietteHandlerOpts, NamedSource};
6
7pub use miette::{LabeledSpan, SourceSpan};
10
11pub fn install_miette_handler(no_color: bool) -> miette::Result<()> {
16 miette::set_hook(Box::new(move |_| {
21 let theme = if no_color {
22 GraphicalTheme::unicode_nocolor()
23 } else {
24 GraphicalTheme::unicode()
25 };
26 Box::new(
27 MietteHandlerOpts::new()
28 .graphical_theme(theme)
29 .context_lines(3)
30 .build(),
31 )
32 }))?;
33
34 miette::set_panic_hook();
36
37 Ok(())
38}
39
40pub fn named_source_from_bytes(name: impl AsRef<str>, bytes: &[u8]) -> NamedSource<Arc<[u8]>> {
45 NamedSource::new(name, Arc::<[u8]>::from(bytes))
46}
47
48pub fn named_source_from_str(name: impl AsRef<str>, text: &str) -> NamedSource<String> {
50 NamedSource::new(name, text.to_string())
51}
52
53pub fn pretty_json_for_display(body: &[u8]) -> Arc<[u8]> {
69 match serde_json::from_slice::<serde_json::Value>(body) {
70 Ok(value) => match serde_json::to_vec_pretty(&value) {
71 Ok(pretty) => Arc::from(pretty),
72 Err(_) => Arc::from(body),
73 },
74 Err(_) => Arc::from(body),
75 }
76}
77
78pub fn span_at_line_column(body: &[u8], line: usize, column: usize) -> SourceSpan {
87 if body.is_empty() {
88 return SourceSpan::new(0.into(), 0);
89 }
90 if line == 0 {
91 let end = body.len().saturating_sub(1);
92 return SourceSpan::new(end.into(), 1);
93 }
94 let mut current_line = 1usize;
95 let mut line_start = 0usize;
96 for (offset, &byte) in body.iter().enumerate() {
97 if current_line == line {
98 let line_end = body[line_start..]
99 .iter()
100 .position(|&b| b == b'\n')
101 .map(|rel| line_start + rel)
102 .unwrap_or(body.len());
103 let column_offset = column.saturating_sub(1);
104 let span_start = line_start + column_offset;
105 if span_start < line_end {
106 return SourceSpan::new(span_start.into(), 1);
107 } else {
108 let len = line_end.saturating_sub(line_start).max(1);
109 return SourceSpan::new(line_start.into(), len);
110 }
111 }
112 if byte == b'\n' {
113 current_line += 1;
114 line_start = offset + 1;
115 }
116 }
117 let end = body.len().saturating_sub(1);
119 SourceSpan::new(end.into(), 1)
120}
121
122pub fn span_for_quoted_literal(bytes: &[u8], literal: &str) -> Option<SourceSpan> {
134 let search = format!("\"{literal}\"");
135 bytes
136 .windows(search.len())
137 .position(|w| w == search.as_bytes())
138 .map(|pos| SourceSpan::new(pos.into(), search.len()))
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn pretty_json_for_display_wraps_compact_body() {
147 let compact = br#"{"a":1,"b":[2,3]}"#;
148 let pretty = pretty_json_for_display(compact);
149 let text = std::str::from_utf8(&pretty).unwrap();
150 assert!(
151 text.contains('\n'),
152 "pretty-printed body should be multi-line"
153 );
154 assert!(text.contains("\"a\""));
155 }
156
157 #[test]
158 fn pretty_json_for_display_passes_non_json_through() {
159 let garbage = b"not valid json <<<";
160 let out = pretty_json_for_display(garbage);
161 assert_eq!(out.as_ref(), garbage);
162 }
163
164 #[test]
165 fn span_at_line_column_known_location() {
166 let body = b"line1\nline2\nline3";
167 let span = span_at_line_column(body, 2, 3);
168 assert_eq!(span.offset(), 8);
170 assert_eq!(span.len(), 1);
171 }
172
173 #[test]
174 fn span_at_line_column_unknown_line_sentinel() {
175 let body = b"abc";
176 let span = span_at_line_column(body, 0, 0);
177 assert_eq!(span.offset(), 2);
178 assert_eq!(span.len(), 1);
179 }
180
181 #[test]
182 fn span_at_line_column_empty_body() {
183 let span = span_at_line_column(b"", 1, 1);
184 assert_eq!(span.offset(), 0);
185 assert_eq!(span.len(), 0);
186 }
187
188 #[test]
189 fn span_at_line_column_column_past_end_of_line() {
190 let body = b"ab\ncd\n";
191 let span = span_at_line_column(body, 1, 99);
192 assert_eq!(span.offset(), 0);
194 assert_eq!(span.len(), 2);
195 }
196
197 #[test]
198 fn span_for_quoted_literal_finds_key() {
199 let json = br#"{"service": [], "other": 123}"#;
200 let span = span_for_quoted_literal(json, "service").unwrap();
201 assert_eq!(
202 &json[span.offset()..span.offset() + span.len()],
203 b"\"service\""
204 );
205 }
206
207 #[test]
208 fn span_for_quoted_literal_finds_value() {
209 let json = br#"{"serviceEndpoint": "https://example.com"}"#;
210 let span = span_for_quoted_literal(json, "https://example.com").unwrap();
211 assert_eq!(
212 &json[span.offset()..span.offset() + span.len()],
213 b"\"https://example.com\""
214 );
215 }
216
217 #[test]
218 fn span_for_quoted_literal_missing_returns_none() {
219 let json = br#"{"other": 123}"#;
220 assert!(span_for_quoted_literal(json, "service").is_none());
221 }
222}