Skip to main content

mii_http/
diag.rs

1//! Diagnostic helpers using ariadne.
2
3use ariadne::{Color, Label, Report, ReportKind, Source};
4use serde::Serialize;
5use std::ops::Range;
6
7#[derive(Debug, Clone)]
8pub struct Diag {
9    pub kind: DiagKind,
10    pub message: String,
11    pub label: String,
12    pub span: Range<usize>,
13    pub note: Option<String>,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum DiagKind {
18    Error,
19    Warning,
20}
21
22#[derive(Debug, Clone, Serialize)]
23pub struct DiagnosticReport {
24    pub ok: bool,
25    pub error_count: usize,
26    pub warning_count: usize,
27    pub endpoint_count: Option<usize>,
28    pub diagnostics: Vec<JsonDiag>,
29}
30
31#[derive(Debug, Clone, Serialize)]
32pub struct JsonDiag {
33    pub kind: &'static str,
34    pub message: String,
35    pub label: String,
36    pub note: Option<String>,
37    pub span: JsonSpan,
38}
39
40#[derive(Debug, Clone, Serialize)]
41pub struct JsonSpan {
42    pub start: usize,
43    pub end: usize,
44    pub start_line: usize,
45    pub start_column: usize,
46    pub end_line: usize,
47    pub end_column: usize,
48}
49
50impl Diag {
51    pub fn error(message: impl Into<String>, span: Range<usize>, label: impl Into<String>) -> Self {
52        Self {
53            kind: DiagKind::Error,
54            message: message.into(),
55            label: label.into(),
56            span,
57            note: None,
58        }
59    }
60
61    pub fn warning(
62        message: impl Into<String>,
63        span: Range<usize>,
64        label: impl Into<String>,
65    ) -> Self {
66        Self {
67            kind: DiagKind::Warning,
68            message: message.into(),
69            label: label.into(),
70            span,
71            note: None,
72        }
73    }
74
75    pub fn with_note(mut self, note: impl Into<String>) -> Self {
76        self.note = Some(note.into());
77        self
78    }
79
80    pub fn emit(&self, file_name: &str, source: &str) {
81        let kind = match self.kind {
82            DiagKind::Error => ReportKind::Error,
83            DiagKind::Warning => ReportKind::Warning,
84        };
85        let span = clamp_span(&self.span, source.len());
86        let mut builder = Report::build(kind, (file_name, span.clone()))
87            .with_message(&self.message)
88            .with_label(
89                Label::new((file_name, span))
90                    .with_message(&self.label)
91                    .with_color(match self.kind {
92                        DiagKind::Error => Color::Red,
93                        DiagKind::Warning => Color::Yellow,
94                    }),
95            );
96        if let Some(note) = &self.note {
97            builder = builder.with_note(note);
98        }
99        let _ = builder.finish().eprint((file_name, Source::from(source)));
100    }
101}
102
103fn clamp_span(span: &Range<usize>, max: usize) -> Range<usize> {
104    let start = span.start.min(max);
105    let end = span.end.min(max).max(start);
106    start..end
107}
108
109pub fn emit_all(diags: &[Diag], file_name: &str, source: &str) {
110    for d in diags {
111        d.emit(file_name, source);
112    }
113}
114
115pub fn report(diags: &[Diag], source: &str, endpoint_count: Option<usize>) -> DiagnosticReport {
116    let diagnostics: Vec<_> = diags
117        .iter()
118        .map(|d| JsonDiag {
119            kind: match d.kind {
120                DiagKind::Error => "error",
121                DiagKind::Warning => "warning",
122            },
123            message: d.message.clone(),
124            label: d.label.clone(),
125            note: d.note.clone(),
126            span: json_span(&d.span, source),
127        })
128        .collect();
129    let error_count = diagnostics.iter().filter(|d| d.kind == "error").count();
130    let warning_count = diagnostics.iter().filter(|d| d.kind == "warning").count();
131    DiagnosticReport {
132        ok: error_count == 0,
133        error_count,
134        warning_count,
135        endpoint_count,
136        diagnostics,
137    }
138}
139
140fn json_span(span: &Range<usize>, source: &str) -> JsonSpan {
141    let span = clamp_span(span, source.len());
142    let (start_line, start_column) = line_column(source, span.start);
143    let (end_line, end_column) = line_column(source, span.end);
144    JsonSpan {
145        start: span.start,
146        end: span.end,
147        start_line,
148        start_column,
149        end_line,
150        end_column,
151    }
152}
153
154fn line_column(source: &str, byte_offset: usize) -> (usize, usize) {
155    let byte_offset = previous_char_boundary(source, byte_offset.min(source.len()));
156    let mut line = 0usize;
157    let mut line_start = 0usize;
158    for (idx, ch) in source.char_indices() {
159        if idx >= byte_offset {
160            break;
161        }
162        if ch == '\n' {
163            line += 1;
164            line_start = idx + ch.len_utf8();
165        }
166    }
167    let column = source[line_start..byte_offset].chars().count();
168    (line, column)
169}
170
171fn previous_char_boundary(source: &str, mut byte_offset: usize) -> usize {
172    while byte_offset > 0 && !source.is_char_boundary(byte_offset) {
173        byte_offset -= 1;
174    }
175    byte_offset
176}