Skip to main content

weaveffi_core/validate/
diagnostic.rs

1//! [`ValidationDiagnostic`]: a [`ValidationError`] plus an optional source
2//! snippet and best-effort span for fancy [`miette`] rendering.
3
4use super::ValidationError;
5use miette::{Diagnostic, NamedSource, SourceSpan};
6
7/// Diagnostic wrapper that attaches an optional source code snippet and a
8/// best-effort byte range to a [`ValidationError`] for fancy rendering via
9/// [`miette`]. The wrapper delegates `help()` and `code()` to the inner error
10/// while exposing its own `source_code` and `labels` so the renderer can
11/// underline the offending identifier in the input.
12#[derive(Debug)]
13pub struct ValidationDiagnostic {
14    pub error: ValidationError,
15    pub src: Option<NamedSource<String>>,
16    pub span: Option<SourceSpan>,
17}
18
19impl std::fmt::Display for ValidationDiagnostic {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        std::fmt::Display::fmt(&self.error, f)
22    }
23}
24
25impl std::error::Error for ValidationDiagnostic {
26    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
27        self.error.source()
28    }
29}
30
31impl Diagnostic for ValidationDiagnostic {
32    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
33        self.error.code()
34    }
35
36    fn severity(&self) -> Option<miette::Severity> {
37        self.error.severity()
38    }
39
40    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
41        self.error.help()
42    }
43
44    fn url<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
45        self.error.url()
46    }
47
48    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
49        self.src
50            .as_ref()
51            .map(|s| s as &dyn miette::SourceCode)
52            .or_else(|| self.error.source_code())
53    }
54
55    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
56        if let Some(span) = self.span {
57            Some(Box::new(std::iter::once(
58                miette::LabeledSpan::new_with_span(Some("here".to_string()), span),
59            )))
60        } else {
61            self.error.labels()
62        }
63    }
64}
65
66impl ValidationDiagnostic {
67    /// Build a [`ValidationDiagnostic`] from a [`ValidationError`] and an
68    /// optional `(filename, contents)` source. When a source is provided the
69    /// constructor performs a best-effort search for the offending identifier
70    /// (e.g. a duplicate module name or unknown type reference) and attaches
71    /// a [`SourceSpan`] for fancy rendering. If no span can be computed the
72    /// label is omitted and miette still produces a nicer message + help
73    /// section than plain `Display`.
74    pub fn new(error: ValidationError, source: Option<(&str, &str)>) -> Self {
75        let (src, span) = match source {
76            Some((filename, contents)) => {
77                let span = find_offending_span(&error, contents);
78                (Some(NamedSource::new(filename, contents.to_string())), span)
79            }
80            None => (None, None),
81        };
82        Self { error, src, span }
83    }
84}
85
86fn find_offending_span(err: &ValidationError, src: &str) -> Option<SourceSpan> {
87    let needle: &str = match err {
88        ValidationError::DuplicateModuleName(n) => Some(n.as_str()),
89        ValidationError::InvalidModuleName(n, _) => Some(n.as_str()),
90        ValidationError::DuplicateFunctionName { function, .. } => Some(function.as_str()),
91        ValidationError::DuplicateParamName { param, .. } => Some(param.as_str()),
92        ValidationError::ReservedKeyword(n) => Some(n.as_str()),
93        ValidationError::InvalidIdentifier(n, _) => Some(n.as_str()),
94        ValidationError::DuplicateErrorName { name, .. } => Some(name.as_str()),
95        ValidationError::InvalidErrorCode { name, .. } => Some(name.as_str()),
96        ValidationError::NameCollisionWithErrorDomain { name, .. } => Some(name.as_str()),
97        ValidationError::DuplicateStructName { name, .. } => Some(name.as_str()),
98        ValidationError::DuplicateStructField { field, .. } => Some(field.as_str()),
99        ValidationError::EmptyStruct { name, .. } => Some(name.as_str()),
100        ValidationError::DuplicateEnumName { name, .. } => Some(name.as_str()),
101        ValidationError::EmptyEnum { name, .. } => Some(name.as_str()),
102        ValidationError::DuplicateEnumVariant { variant, .. } => Some(variant.as_str()),
103        ValidationError::UnknownTypeRef { name } => Some(name.as_str()),
104        ValidationError::DuplicateCallbackName { name, .. } => Some(name.as_str()),
105        ValidationError::UnsupportedCallbackParamType { param, .. } => Some(param.as_str()),
106        ValidationError::ListenerCallbackNotFound { callback, .. } => Some(callback.as_str()),
107        ValidationError::DuplicateListenerName { name, .. } => Some(name.as_str()),
108        ValidationError::BuilderStructEmpty { name, .. } => Some(name.as_str()),
109        ValidationError::UnsupportedSchemaVersion { version, .. } => Some(version.as_str()),
110        ValidationError::AsyncIteratorReturn { function, .. } => Some(function.as_str()),
111        _ => None,
112    }?;
113    let quoted = format!("\"{needle}\"");
114    if let Some(pos) = src.find(&quoted) {
115        return Some(SourceSpan::new(pos.into(), quoted.len()));
116    }
117    src.find(needle)
118        .map(|pos| SourceSpan::new(pos.into(), needle.len()))
119}