• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

facet-rs / facet / 16482855623

23 Jul 2025 10:08PM UTC coverage: 58.447% (-0.2%) from 58.68%
16482855623

Pull #855

github

web-flow
Merge dca4c2302 into 5e8e214d1
Pull Request #855: wip: Remove 'shape lifetime

400 of 572 new or added lines in 70 files covered. (69.93%)

3 existing lines in 3 files now uncovered.

11939 of 20427 relevant lines covered (58.45%)

120.58 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

46.0
/facet-args/src/deserialize/error.rs
1
#[cfg(feature = "rich-diagnostics")]
2
use ariadne::{Color, Config, IndexType, Label, Report, ReportKind, Source};
3

4
use alloc::string::String;
5

6
use facet_core::{Shape, Type, UserType};
7
use facet_reflect::{ReflectError, VariantError};
8
use owo_colors::OwoColorize;
9

10
use super::debug::InputDebug;
11
use super::{Cooked, Outcome, Span};
12

13
/// A JSON parse error, with context. Never would've guessed huh.
14
pub struct DeserError<'input, C = Cooked> {
15
    /// The input associated with the error.
16
    pub input: alloc::borrow::Cow<'input, [u8]>,
17

18
    /// Where the error occured
19
    pub span: Span<C>,
20

21
    /// The specific error that occurred while parsing the JSON.
22
    pub kind: DeserErrorKind,
23
}
24

25
impl<'input, C> DeserError<'input, C> {
26
    /// Converts the error into an owned error.
NEW
27
    pub fn into_owned(self) -> DeserError<'static, C> {
×
28
        DeserError {
×
29
            input: self.input.into_owned().into(),
×
30
            span: self.span,
×
31
            kind: self.kind,
×
32
        }
×
33
    }
×
34

35
    /// Sets the span of this error
NEW
36
    pub fn with_span<D>(self, span: Span<D>) -> DeserError<'input, D> {
×
37
        DeserError {
×
38
            input: self.input,
×
39
            span,
×
40
            kind: self.kind,
×
41
        }
×
42
    }
×
43
}
44

45
/// An error kind for JSON parsing.
46
#[derive(Debug, Clone)]
47
pub enum DeserErrorKind {
48
    /// An unexpected byte was encountered in the input.
49
    UnexpectedByte {
50
        /// The byte that was found.
51
        got: u8,
52
        /// The expected value as a string description.
53
        wanted: &'static str,
54
    },
55

56
    /// An unexpected character was encountered in the input.
57
    UnexpectedChar {
58
        /// The character that was found.
59
        got: char,
60
        /// The expected value as a string description.
61
        wanted: &'static str,
62
    },
63

64
    /// An unexpected outcome was encountered in the input.
65
    UnexpectedOutcome {
66
        /// The outcome that was found.
67
        got: Outcome<'static>,
68
        /// The expected value as a string description.
69
        wanted: &'static str,
70
    },
71

72
    /// The input ended unexpectedly while parsing JSON.
73
    UnexpectedEof {
74
        /// The expected value as a string description.
75
        wanted: &'static str,
76
    },
77

78
    /// Indicates a value was expected to follow an element in the input.
79
    MissingValue {
80
        /// Describes what type of value was expected.
81
        expected: &'static str,
82
        /// The element that requires the missing value.
83
        field: String,
84
    },
85

86
    /// A required struct field was missing at the end of JSON input.
87
    MissingField(&'static str),
88

89
    /// A number is out of range.
90
    NumberOutOfRange(f64),
91

92
    /// An unexpected String was encountered in the input.
93
    StringAsNumber(String),
94

95
    /// An unexpected field name was encountered in the input.
96
    UnknownField {
97
        /// The name of the field that was not recognized
98
        field_name: String,
99

100
        /// The shape definition where the unknown field was encountered
101
        shape: &'static Shape,
102
    },
103

104
    /// A string that could not be built into valid UTF-8 Unicode
105
    InvalidUtf8(String),
106

107
    /// An error occurred while reflecting a type.
108
    ReflectError(ReflectError),
109

110
    /// Some feature is not yet implemented (under development).
111
    Unimplemented(&'static str),
112

113
    /// An unsupported type was encountered.
114
    UnsupportedType {
115
        /// The shape we got
116
        got: &'static Shape,
117

118
        /// The shape we wanted
119
        wanted: &'static str,
120
    },
121

122
    /// An enum variant name that doesn't exist in the enum definition.
123
    NoSuchVariant {
124
        /// The name of the variant that was not found
125
        name: String,
126

127
        /// The enum shape definition where the variant was looked up
128
        enum_shape: &'static Shape,
129
    },
130

131
    /// An error occurred when reflecting an enum variant (index) from a user type.
132
    VariantError(VariantError),
133

134
    /// Too many elements for an array.
135
    ArrayOverflow {
136
        /// The array shape
137
        shape: &'static Shape,
138

139
        /// Maximum allowed length
140
        max_len: usize,
141
    },
142

143
    /// Failed to convert numeric type.
144
    NumericConversion {
145
        /// Source type name
146
        from: &'static str,
147

148
        /// Target type name
149
        to: &'static str,
150
    },
151
}
152

153
impl<'input, C> DeserError<'input, C> {
154
    /// Creates a new deser error, preserving input and location context for accurate reporting.
155
    pub fn new<I>(kind: DeserErrorKind, input: &'input I, span: Span<C>) -> Self
19✔
156
    where
19✔
157
        I: ?Sized + 'input + InputDebug,
19✔
158
    {
159
        Self {
19✔
160
            input: input.as_cow(),
19✔
161
            span,
19✔
162
            kind,
19✔
163
        }
19✔
164
    }
19✔
165

166
    /// Constructs a reflection-related deser error, keeping contextual information intact.
167
    pub(crate) fn new_reflect<I>(e: ReflectError, input: &'input I, span: Span<C>) -> Self
3✔
168
    where
3✔
169
        I: ?Sized + 'input + InputDebug,
3✔
170
    {
171
        DeserError::new(DeserErrorKind::ReflectError(e), input, span)
3✔
172
    }
3✔
173

174
    /// Provides a human-friendly message wrapper to improve error readability.
175
    pub fn message(&self) -> DeserErrorMessage<'_, C> {
20✔
176
        DeserErrorMessage(self)
20✔
177
    }
20✔
178
}
179

180
/// A wrapper type for displaying deser error messages
181
pub struct DeserErrorMessage<'input, C = Cooked>(&'input DeserError<'input, C>);
182

183
impl core::fmt::Display for DeserErrorMessage<'_> {
184
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
20✔
185
        match &self.0.kind {
20✔
186
            DeserErrorKind::UnexpectedByte { got, wanted } => write!(
×
187
                f,
×
188
                "Unexpected byte: got 0x{:02X}, wanted {}",
×
189
                got.red(),
×
190
                wanted.yellow()
×
191
            ),
192
            DeserErrorKind::UnexpectedChar { got, wanted } => write!(
×
193
                f,
×
194
                "Unexpected character: got '{}', wanted {}",
×
195
                got.red(),
×
196
                wanted.yellow()
×
197
            ),
198
            DeserErrorKind::UnexpectedOutcome { got, wanted } => {
×
199
                write!(f, "Unexpected {}, wanted {}", got.red(), wanted.yellow())
×
200
            }
201
            DeserErrorKind::UnexpectedEof { wanted } => {
×
202
                write!(f, "Unexpected end of file: wanted {}", wanted.red())
×
203
            }
204
            DeserErrorKind::MissingValue { expected, field } => {
5✔
205
                write!(f, "Missing {} for {}", expected.red(), field.yellow())
5✔
206
            }
207
            DeserErrorKind::MissingField(fld) => write!(f, "Missing required field: {}", fld.red()),
×
208
            DeserErrorKind::NumberOutOfRange(n) => {
×
209
                write!(f, "Number out of range: {}", n.red())
×
210
            }
211
            DeserErrorKind::StringAsNumber(s) => {
×
212
                write!(f, "Expected a string but got number: {}", s.red())
×
213
            }
214
            DeserErrorKind::UnknownField { field_name, shape } => {
5✔
215
                write!(
5✔
216
                    f,
5✔
217
                    "Unknown field: {} for shape {}",
5✔
218
                    field_name.red(),
5✔
219
                    shape.yellow()
5✔
220
                )
221
            }
222
            DeserErrorKind::InvalidUtf8(e) => write!(f, "Invalid UTF-8 encoding: {}", e.red()),
×
223
            DeserErrorKind::ReflectError(e) => write!(f, "{e}"),
6✔
224
            DeserErrorKind::Unimplemented(s) => {
×
225
                write!(f, "Feature not yet implemented: {}", s.yellow())
×
226
            }
227
            DeserErrorKind::UnsupportedType { got, wanted } => {
2✔
228
                write!(
2✔
229
                    f,
2✔
230
                    "Unsupported type: got {}, wanted {}",
2✔
231
                    got.red(),
2✔
232
                    wanted.green()
2✔
233
                )
234
            }
235
            DeserErrorKind::NoSuchVariant { name, enum_shape } => {
×
236
                if let Type::User(UserType::Enum(ed)) = enum_shape.ty {
×
237
                    write!(
×
238
                        f,
×
239
                        "Enum variant not found: {} in enum {}. Available variants: [",
×
240
                        name.red(),
×
241
                        enum_shape.yellow()
×
242
                    )?;
×
243

244
                    let mut first = true;
×
245
                    for variant in ed.variants.iter() {
×
246
                        if !first {
×
247
                            write!(f, ", ")?;
×
248
                        }
×
249
                        write!(f, "{}", variant.name.green())?;
×
250
                        first = false;
×
251
                    }
252

253
                    write!(f, "]")?;
×
254
                    Ok(())
×
255
                } else {
256
                    write!(
×
257
                        f,
×
258
                        "Enum variant not found: {} in non-enum type {}",
×
259
                        name.red(),
×
260
                        enum_shape.yellow()
×
261
                    )?;
×
262
                    Ok(())
×
263
                }
264
            }
265
            DeserErrorKind::VariantError(e) => {
×
266
                write!(f, "Variant error: {e}")
×
267
            }
268
            DeserErrorKind::ArrayOverflow { shape, max_len } => {
×
269
                write!(
×
270
                    f,
×
271
                    "Too many elements for array {}: maximum {} elements allowed",
×
272
                    shape.blue(),
×
273
                    max_len.yellow()
×
274
                )
275
            }
276
            DeserErrorKind::NumericConversion { from, to } => {
2✔
277
                write!(
2✔
278
                    f,
2✔
279
                    "Cannot convert {} to {}: value out of range or precision loss",
2✔
280
                    from.red(),
2✔
281
                    to.green()
2✔
282
                )
283
            }
284
        }
285
    }
20✔
286
}
287

288
#[cfg(not(feature = "rich-diagnostics"))]
289
impl core::fmt::Display for DeserError<'_, '_> {
290
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
291
        write!(f, "{} at byte {}", self.message(), self.span.start(),)
292
    }
293
}
294

295
#[cfg(feature = "rich-diagnostics")]
296
impl core::fmt::Display for DeserError<'_> {
297
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
20✔
298
        // Try to convert input to utf8 for source display, otherwise fallback to error
299
        let Ok(orig_input_str) = core::str::from_utf8(&self.input[..]) else {
20✔
300
            return write!(f, "(JSON input was invalid UTF-8)");
×
301
        };
302

303
        let mut span_start = self.span.start();
20✔
304
        let mut span_end = self.span.end();
20✔
305
        use alloc::borrow::Cow;
306
        let mut input_str: Cow<'_, str> = Cow::Borrowed(orig_input_str);
20✔
307

308
        // --- Context-sensitive truncation logic ---
309
        // When the error occurs very far into a huge (often one-line) input,
310
        // such as minified JSON, it's annoying to display hundreds or thousands of
311
        // preceding and trailing characters. Instead, we seek to trim the displayed
312
        // "source" to just enough around the offending line/location, but only if
313
        // we can do this cleanly.
314
        //
315
        // Our approach:
316
        // - Find the full line that `span_start` is within, using memchr for newlines before and after.
317
        // - Only proceed if both `span_start` and `span_end` are within this line (i.e., error doesn't span lines).
318
        // - If there are more than 180 characters before/after the span on this line, truncate to show
319
        //   "...<80 chars>SPANTEXT<80 chars>..." and adjust the display offsets to ensure ariadne points
320
        //   to the correct span inside the trimmed display.
321
        //
322
        // Rationale: this avoids a sea of whitespace for extremely long lines (common in compact JSON).
323

324
        let mut did_truncate = false;
20✔
325

326
        {
327
            // Find the line bounds containing span_start
328
            let bytes = self.input.as_ref();
20✔
329
            let line_start = bytes[..span_start]
20✔
330
                .iter()
20✔
331
                .rposition(|&b| b == b'\n')
110✔
332
                .map(|pos| pos + 1)
20✔
333
                .unwrap_or(0);
20✔
334
            let line_end = bytes[span_start..]
20✔
335
                .iter()
20✔
336
                .position(|&b| b == b'\n')
238✔
337
                .map(|pos| span_start + pos)
20✔
338
                .unwrap_or(bytes.len());
20✔
339

340
            // Check if span fits within one line
341
            if span_end <= line_end {
20✔
342
                // How much context do we have before and after the span in this line?
343
                let before_chars = span_start - line_start;
20✔
344
                let after_chars = line_end.saturating_sub(span_end);
20✔
345

346
                // Only trim if context is long enough
347
                if before_chars > 180 || after_chars > 180 {
20✔
348
                    let trim_left = if before_chars > 180 {
×
349
                        before_chars - 80
×
350
                    } else {
351
                        0
×
352
                    };
353
                    let trim_right = if after_chars > 180 {
×
354
                        after_chars - 80
×
355
                    } else {
356
                        0
×
357
                    };
358

359
                    let new_start = line_start + trim_left;
×
360
                    let new_end = line_end - trim_right;
×
361

362
                    let truncated = &orig_input_str[new_start..new_end];
×
363

364
                    let left_ellipsis = if trim_left > 0 { "…" } else { "" };
×
365
                    let right_ellipsis = if trim_right > 0 { "…" } else { "" };
×
366

367
                    let mut buf = String::with_capacity(
×
368
                        left_ellipsis.len() + truncated.len() + right_ellipsis.len(),
×
369
                    );
370
                    buf.push_str(left_ellipsis);
×
371
                    buf.push_str(truncated);
×
372
                    buf.push_str(right_ellipsis);
×
373

374
                    // Adjust span offsets to align with the trimmed string
375
                    span_start = span_start - new_start + left_ellipsis.len();
×
376
                    span_end = span_end - new_start + left_ellipsis.len();
×
377

378
                    input_str = Cow::Owned(buf);
×
379

380
                    did_truncate = true; // mark that truncation occurred
×
381
                    // Done!
382
                }
20✔
383
            }
×
384
            // If the span goes across lines or we cannot cleanly trim, display the full input as fallback
385
        }
386

387
        if did_truncate {
20✔
388
            writeln!(
×
389
                f,
×
390
                "{}",
×
391
                "WARNING: Input was truncated for display. Byte indexes in the error below do not match original input.".yellow().bold()
×
392
            )?;
×
393
        }
20✔
394

395
        let mut report = Report::build(ReportKind::Error, span_start..span_end)
20✔
396
            .with_config(Config::new().with_index_type(IndexType::Byte));
20✔
397

398
        let label = Label::new(span_start..span_end)
20✔
399
            .with_message(self.message())
20✔
400
            .with_color(Color::Red);
20✔
401

402
        report = report.with_label(label);
20✔
403

404
        let source = Source::from(input_str);
20✔
405

406
        struct FmtWriter<'a, 'b: 'a> {
407
            f: &'a mut core::fmt::Formatter<'b>,
408
            error: Option<core::fmt::Error>,
409
        }
410

411
        impl core::fmt::Write for FmtWriter<'_, '_> {
412
            fn write_str(&mut self, s: &str) -> core::fmt::Result {
5,872✔
413
                if self.error.is_some() {
5,872✔
414
                    // Already failed, do nothing
415
                    return Err(core::fmt::Error);
×
416
                }
5,872✔
417
                if let Err(e) = self.f.write_str(s) {
5,872✔
418
                    self.error = Some(e);
×
419
                    Err(core::fmt::Error)
×
420
                } else {
421
                    Ok(())
5,872✔
422
                }
423
            }
5,872✔
424
        }
425

426
        struct IoWriter<'a, 'b: 'a> {
427
            inner: FmtWriter<'a, 'b>,
428
        }
429

430
        impl std::io::Write for IoWriter<'_, '_> {
431
            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
5,872✔
432
                match core::str::from_utf8(buf) {
5,872✔
433
                    Ok(s) => match core::fmt::Write::write_str(&mut self.inner, s) {
5,872✔
434
                        Ok(()) => Ok(buf.len()),
5,872✔
435
                        Err(_) => Err(std::io::ErrorKind::Other.into()),
×
436
                    },
437
                    Err(_) => Err(std::io::ErrorKind::InvalidData.into()),
×
438
                }
439
            }
5,872✔
440
            fn flush(&mut self) -> std::io::Result<()> {
×
441
                Ok(())
×
442
            }
×
443
        }
444

445
        let cache = &source;
20✔
446

447
        let fmt_writer = FmtWriter { f, error: None };
20✔
448
        let mut io_writer = IoWriter { inner: fmt_writer };
20✔
449

450
        if report.finish().write(cache, &mut io_writer).is_err() {
20✔
451
            return write!(f, "Error formatting with ariadne");
×
452
        }
20✔
453

454
        // Check if our adapter ran into a formatting error
455
        if io_writer.inner.error.is_some() {
20✔
456
            return write!(f, "Error writing ariadne output to fmt::Formatter");
×
457
        }
20✔
458

459
        Ok(())
20✔
460
    }
20✔
461
}
462

463
impl core::fmt::Debug for DeserError<'_> {
464
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
×
465
        core::fmt::Display::fmt(self, f)
×
466
    }
×
467
}
468

469
impl core::error::Error for DeserError<'_> {}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc