facet_deserialize/
error.rs

1#[cfg(feature = "rich-diagnostics")]
2use ariadne::{Color, Config, IndexType, Label, Report, ReportKind, Source};
3
4use alloc::string::String;
5
6use facet_core::{Shape, Type, UserType};
7use facet_reflect::{ReflectError, VariantError};
8use owo_colors::OwoColorize;
9
10use crate::debug::InputDebug;
11use crate::{Cooked, Outcome, Span};
12
13/// A JSON parse error, with context. Never would've guessed huh.
14#[derive(Debug)]
15pub struct DeserError<'input, 'shape, C = Cooked> {
16    /// The input associated with the error.
17    pub input: alloc::borrow::Cow<'input, [u8]>,
18
19    /// Where the error occured
20    pub span: Span<C>,
21
22    /// The specific error that occurred while parsing the JSON.
23    pub kind: DeserErrorKind<'shape>,
24
25    /// The source identifier for error reporting
26    pub source_id: &'static str,
27}
28
29impl<'input, 'shape, C> DeserError<'input, 'shape, C> {
30    /// Converts the error into an owned error.
31    pub fn into_owned(self) -> DeserError<'static, 'shape, C> {
32        DeserError {
33            input: self.input.into_owned().into(),
34            span: self.span,
35            kind: self.kind,
36            source_id: self.source_id,
37        }
38    }
39
40    /// Sets the span of this error
41    pub fn with_span<D>(self, span: Span<D>) -> DeserError<'input, 'shape, D> {
42        DeserError {
43            input: self.input,
44            span,
45            kind: self.kind,
46            source_id: self.source_id,
47        }
48    }
49}
50
51/// An error kind for JSON parsing.
52#[derive(Debug, PartialEq, Clone)]
53pub enum DeserErrorKind<'shape> {
54    /// An unexpected byte was encountered in the input.
55    UnexpectedByte {
56        /// The byte that was found.
57        got: u8,
58        /// The expected value as a string description.
59        wanted: &'static str,
60    },
61
62    /// An unexpected character was encountered in the input.
63    UnexpectedChar {
64        /// The character that was found.
65        got: char,
66        /// The expected value as a string description.
67        wanted: &'static str,
68    },
69
70    /// An unexpected outcome was encountered in the input.
71    UnexpectedOutcome {
72        /// The outcome that was found.
73        got: Outcome<'static>,
74        /// The expected value as a string description.
75        wanted: &'static str,
76    },
77
78    /// The input ended unexpectedly while parsing JSON.
79    UnexpectedEof {
80        /// The expected value as a string description.
81        wanted: &'static str,
82    },
83
84    /// Indicates a value was expected to follow an element in the input.
85    MissingValue {
86        /// Describes what type of value was expected.
87        expected: &'static str,
88        /// The element that requires the missing value.
89        field: String,
90    },
91
92    /// A required struct field was missing at the end of JSON input.
93    MissingField(&'static str),
94
95    /// A number is out of range.
96    NumberOutOfRange(f64),
97
98    /// An unexpected String was encountered in the input.
99    StringAsNumber(String),
100
101    /// An unexpected field name was encountered in the input.
102    UnknownField {
103        /// The name of the field that was not recognized
104        field_name: String,
105
106        /// The shape definition where the unknown field was encountered
107        shape: &'shape Shape<'shape>,
108    },
109
110    /// A string that could not be built into valid UTF-8 Unicode
111    InvalidUtf8(String),
112
113    /// An error occurred while reflecting a type.
114    ReflectError(ReflectError<'shape>),
115
116    /// Some feature is not yet implemented (under development).
117    Unimplemented(&'static str),
118
119    /// An unsupported type was encountered.
120    UnsupportedType {
121        /// The shape we got
122        got: &'shape Shape<'shape>,
123
124        /// The shape we wanted
125        wanted: &'static str,
126    },
127
128    /// An enum variant name that doesn't exist in the enum definition.
129    NoSuchVariant {
130        /// The name of the variant that was not found
131        name: String,
132
133        /// The enum shape definition where the variant was looked up
134        enum_shape: &'shape Shape<'shape>,
135    },
136
137    /// An error occurred when reflecting an enum variant (index) from a user type.
138    VariantError(VariantError),
139}
140
141impl<'input, 'shape, C> DeserError<'input, 'shape, C> {
142    /// Creates a new deser error, preserving input and location context for accurate reporting.
143    pub fn new<I>(
144        kind: DeserErrorKind<'shape>,
145        input: &'input I,
146        span: Span<C>,
147        source_id: &'static str,
148    ) -> Self
149    where
150        I: ?Sized + 'input + InputDebug,
151    {
152        Self {
153            input: input.as_cow(),
154            span,
155            kind,
156            source_id,
157        }
158    }
159
160    /// Constructs a reflection-related deser error, keeping contextual information intact.
161    pub(crate) fn new_reflect<I>(
162        e: ReflectError<'shape>,
163        input: &'input I,
164        span: Span<C>,
165        source_id: &'static str,
166    ) -> Self
167    where
168        I: ?Sized + 'input + InputDebug,
169    {
170        DeserError::new(DeserErrorKind::ReflectError(e), input, span, source_id)
171    }
172
173    /// Sets the source ID for this error
174    pub fn with_source_id(mut self, source_id: &'static str) -> Self {
175        self.source_id = source_id;
176        self
177    }
178
179    /// Provides a human-friendly message wrapper to improve error readability.
180    pub fn message(&self) -> DeserErrorMessage<'_, '_, C> {
181        DeserErrorMessage(self)
182    }
183}
184
185/// A wrapper type for displaying deser error messages
186pub struct DeserErrorMessage<'input, 'shape, C = Cooked>(&'input DeserError<'input, 'shape, C>);
187
188impl core::fmt::Display for DeserErrorMessage<'_, '_> {
189    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
190        match &self.0.kind {
191            DeserErrorKind::UnexpectedByte { got, wanted } => write!(
192                f,
193                "Unexpected byte: got 0x{:02X}, wanted {}",
194                got.red(),
195                wanted.yellow()
196            ),
197            DeserErrorKind::UnexpectedChar { got, wanted } => write!(
198                f,
199                "Unexpected character: got '{}', wanted {}",
200                got.red(),
201                wanted.yellow()
202            ),
203            DeserErrorKind::UnexpectedOutcome { got, wanted } => {
204                write!(f, "Unexpected {}, wanted {}", got.red(), wanted.yellow())
205            }
206            DeserErrorKind::UnexpectedEof { wanted } => {
207                write!(f, "Unexpected end of file: wanted {}", wanted.red())
208            }
209            DeserErrorKind::MissingValue { expected, field } => {
210                write!(f, "Missing {} for {}", expected.red(), field.yellow())
211            }
212            DeserErrorKind::MissingField(fld) => write!(f, "Missing required field: {}", fld.red()),
213            DeserErrorKind::NumberOutOfRange(n) => {
214                write!(f, "Number out of range: {}", n.red())
215            }
216            DeserErrorKind::StringAsNumber(s) => {
217                write!(f, "Expected a string but got number: {}", s.red())
218            }
219            DeserErrorKind::UnknownField { field_name, shape } => {
220                write!(
221                    f,
222                    "Unknown field: {} for shape {}",
223                    field_name.red(),
224                    shape.yellow()
225                )
226            }
227            DeserErrorKind::InvalidUtf8(e) => write!(f, "Invalid UTF-8 encoding: {}", e.red()),
228            DeserErrorKind::ReflectError(e) => write!(f, "{e}"),
229            DeserErrorKind::Unimplemented(s) => {
230                write!(f, "Feature not yet implemented: {}", s.yellow())
231            }
232            DeserErrorKind::UnsupportedType { got, wanted } => {
233                write!(
234                    f,
235                    "Unsupported type: got {}, wanted {}",
236                    got.red(),
237                    wanted.green()
238                )
239            }
240            DeserErrorKind::NoSuchVariant { name, enum_shape } => {
241                if let Type::User(UserType::Enum(ed)) = enum_shape.ty {
242                    write!(
243                        f,
244                        "Enum variant not found: {} in enum {}. Available variants: [",
245                        name.red(),
246                        enum_shape.yellow()
247                    )?;
248
249                    let mut first = true;
250                    for variant in ed.variants.iter() {
251                        if !first {
252                            write!(f, ", ")?;
253                        }
254                        write!(f, "{}", variant.name.green())?;
255                        first = false;
256                    }
257
258                    write!(f, "]")?;
259                    Ok(())
260                } else {
261                    write!(
262                        f,
263                        "Enum variant not found: {} in non-enum type {}",
264                        name.red(),
265                        enum_shape.yellow()
266                    )?;
267                    Ok(())
268                }
269            }
270            DeserErrorKind::VariantError(e) => {
271                write!(f, "Variant error: {e}")
272            }
273        }
274    }
275}
276
277#[cfg(not(feature = "rich-diagnostics"))]
278impl core::fmt::Display for DeserError<'_, '_> {
279    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
280        write!(f, "{} at byte {}", self.message(), self.span.start(),)
281    }
282}
283
284#[cfg(feature = "rich-diagnostics")]
285impl core::fmt::Display for DeserError<'_, '_> {
286    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
287        // Try to convert input to utf8 for source display, otherwise fallback to error
288        let Ok(orig_input_str) = core::str::from_utf8(&self.input[..]) else {
289            return write!(f, "(JSON input was invalid UTF-8)");
290        };
291
292        let source_id = self.source_id;
293        let mut span_start = self.span.start();
294        let mut span_end = self.span.end();
295        use alloc::borrow::Cow;
296        let mut input_str: Cow<'_, str> = Cow::Borrowed(orig_input_str);
297
298        // --- Context-sensitive truncation logic ---
299        // When the error occurs very far into a huge (often one-line) input,
300        // such as minified JSON, it's annoying to display hundreds or thousands of
301        // preceding and trailing characters. Instead, we seek to trim the displayed
302        // "source" to just enough around the offending line/location, but only if
303        // we can do this cleanly.
304        //
305        // Our approach:
306        // - Find the full line that `span_start` is within, using memchr for newlines before and after.
307        // - Only proceed if both `span_start` and `span_end` are within this line (i.e., error doesn't span lines).
308        // - If there are more than 180 characters before/after the span on this line, truncate to show
309        //   "...<80 chars>SPANTEXT<80 chars>..." and adjust the display offsets to ensure ariadne points
310        //   to the correct span inside the trimmed display.
311        //
312        // Rationale: this avoids a sea of whitespace for extremely long lines (common in compact JSON).
313
314        let mut did_truncate = false;
315
316        {
317            // Find the line bounds containing span_start
318            let bytes = self.input.as_ref();
319            let line_start = bytes[..span_start]
320                .iter()
321                .rposition(|&b| b == b'\n')
322                .map(|pos| pos + 1)
323                .unwrap_or(0);
324            let line_end = bytes[span_start..]
325                .iter()
326                .position(|&b| b == b'\n')
327                .map(|pos| span_start + pos)
328                .unwrap_or(bytes.len());
329
330            // Check if span fits within one line
331            if span_end <= line_end {
332                // How much context do we have before and after the span in this line?
333                let before_chars = span_start - line_start;
334                let after_chars = line_end.saturating_sub(span_end);
335
336                // Only trim if context is long enough
337                if before_chars > 180 || after_chars > 180 {
338                    let trim_left = if before_chars > 180 {
339                        before_chars - 80
340                    } else {
341                        0
342                    };
343                    let trim_right = if after_chars > 180 {
344                        after_chars - 80
345                    } else {
346                        0
347                    };
348
349                    let new_start = line_start + trim_left;
350                    let new_end = line_end - trim_right;
351
352                    let truncated = &orig_input_str[new_start..new_end];
353
354                    let left_ellipsis = if trim_left > 0 { "…" } else { "" };
355                    let right_ellipsis = if trim_right > 0 { "…" } else { "" };
356
357                    let mut buf = String::with_capacity(
358                        left_ellipsis.len() + truncated.len() + right_ellipsis.len(),
359                    );
360                    buf.push_str(left_ellipsis);
361                    buf.push_str(truncated);
362                    buf.push_str(right_ellipsis);
363
364                    // Adjust span offsets to align with the trimmed string
365                    span_start = span_start - new_start + left_ellipsis.len();
366                    span_end = span_end - new_start + left_ellipsis.len();
367
368                    input_str = Cow::Owned(buf);
369
370                    did_truncate = true; // mark that truncation occurred
371                    // Done!
372                }
373            }
374            // If the span goes across lines or we cannot cleanly trim, display the full input as fallback
375        }
376
377        if did_truncate {
378            writeln!(
379                f,
380                "{}",
381                "WARNING: Input was truncated for display. Byte indexes in the error below do not match original input.".yellow().bold()
382            )?;
383        }
384
385        let mut report = Report::build(ReportKind::Error, (source_id, span_start..span_end))
386            .with_config(Config::new().with_index_type(IndexType::Byte));
387
388        let label = Label::new((source_id, span_start..span_end))
389            .with_message(self.message())
390            .with_color(Color::Red);
391
392        report = report.with_label(label);
393
394        let source = Source::from(input_str);
395
396        struct FmtWriter<'a, 'b: 'a> {
397            f: &'a mut core::fmt::Formatter<'b>,
398            error: Option<core::fmt::Error>,
399        }
400
401        impl core::fmt::Write for FmtWriter<'_, '_> {
402            fn write_str(&mut self, s: &str) -> core::fmt::Result {
403                if self.error.is_some() {
404                    // Already failed, do nothing
405                    return Err(core::fmt::Error);
406                }
407                if let Err(e) = self.f.write_str(s) {
408                    self.error = Some(e);
409                    Err(core::fmt::Error)
410                } else {
411                    Ok(())
412                }
413            }
414        }
415
416        struct IoWriter<'a, 'b: 'a> {
417            inner: FmtWriter<'a, 'b>,
418        }
419
420        impl std::io::Write for IoWriter<'_, '_> {
421            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
422                match core::str::from_utf8(buf) {
423                    Ok(s) => match core::fmt::Write::write_str(&mut self.inner, s) {
424                        Ok(()) => Ok(buf.len()),
425                        Err(_) => Err(std::io::ErrorKind::Other.into()),
426                    },
427                    Err(_) => Err(std::io::ErrorKind::InvalidData.into()),
428                }
429            }
430            fn flush(&mut self) -> std::io::Result<()> {
431                Ok(())
432            }
433        }
434
435        let cache = (source_id, &source);
436
437        let fmt_writer = FmtWriter { f, error: None };
438        let mut io_writer = IoWriter { inner: fmt_writer };
439
440        if report.finish().write(cache, &mut io_writer).is_err() {
441            return write!(f, "Error formatting with ariadne");
442        }
443
444        // Check if our adapter ran into a formatting error
445        if io_writer.inner.error.is_some() {
446            return write!(f, "Error writing ariadne output to fmt::Formatter");
447        }
448
449        Ok(())
450    }
451}
452
453impl core::error::Error for DeserError<'_, '_> {}