facet_deserialize/
error.rs

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