Skip to main content

lintel_check/
diagnostics.rs

1use miette::{Diagnostic, LabeledSpan, NamedSource, SourceSpan};
2use thiserror::Error;
3
4/// A parse error with exact source location.
5#[derive(Debug, Error, Diagnostic)]
6#[error("{message}")]
7pub struct ParseDiagnostic {
8    #[source_code]
9    pub src: NamedSource<String>,
10
11    #[label("here")]
12    pub span: SourceSpan,
13
14    pub message: String,
15}
16
17/// A schema validation error for a specific file.
18#[derive(Debug, Error)]
19#[error("{message}")]
20pub struct ValidationDiagnostic {
21    pub src: NamedSource<String>,
22
23    pub span: SourceSpan,
24
25    pub path: String,
26
27    pub instance_path: String,
28
29    pub message: String,
30}
31
32impl Diagnostic for ValidationDiagnostic {
33    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
34        Some(&self.src)
35    }
36
37    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
38        let label = if self.instance_path.is_empty() {
39            "here".to_string()
40        } else {
41            self.instance_path.clone()
42        };
43        Some(Box::new(std::iter::once(LabeledSpan::new(
44            Some(label),
45            self.span.offset(),
46            self.span.len(),
47        ))))
48    }
49}
50
51/// An I/O or schema-fetch error associated with a file path.
52#[derive(Debug, Error, Diagnostic)]
53#[error("{path}: {message}")]
54pub struct FileDiagnostic {
55    pub path: String,
56    pub message: String,
57}
58
59/// Try to find the byte offset of a JSON pointer path segment in the source text.
60///
61/// For an `instance_path` like `/properties/name`, searches for the last segment `name`
62/// as a JSON key (`"name"`) or YAML key (`name:`). Falls back to 0 if not found.
63pub fn find_instance_path_offset(content: &str, instance_path: &str) -> usize {
64    if instance_path.is_empty() || instance_path == "/" {
65        return 0;
66    }
67
68    // Get the last path segment (e.g., "/foo/bar/baz" -> "baz")
69    let segment = instance_path.rsplit('/').next().unwrap_or("");
70    if segment.is_empty() {
71        return 0;
72    }
73
74    // Try JSON-style key: "segment"
75    let json_key = format!("\"{segment}\"");
76    if let Some(pos) = content.find(&json_key) {
77        return pos;
78    }
79
80    // Try YAML-style key: segment: (at line start or after whitespace)
81    let yaml_key = format!("{segment}:");
82    let quoted_yaml_key = format!("\"{segment}\":");
83    let mut offset = 0;
84    for line in content.lines() {
85        let trimmed = line.trim_start();
86        if trimmed.starts_with(&yaml_key) || trimmed.starts_with(&quoted_yaml_key) {
87            let key_start = line.len() - trimmed.len();
88            return offset + key_start;
89        }
90        offset += line.len() + 1; // +1 for newline
91    }
92
93    0
94}