facet_json/deserialize/
error.rs

1#[cfg(feature = "alloc")]
2use alloc::format;
3#[cfg(feature = "alloc")]
4use alloc::string::String;
5
6#[cfg(feature = "rich-diagnostics")]
7use ariadne::{Color, Config, IndexType, Label, Report, ReportKind, Source};
8use facet_reflect::ReflectError;
9use owo_colors::OwoColorize;
10
11use super::{Token, TokenErrorKind, tokenizer::Span};
12use facet_core::Shape;
13
14/// A JSON parse error, with context. Never would've guessed huh.
15pub struct JsonError<'input> {
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,
21
22    /// Where we were in the struct when the error occured
23    pub path: String,
24
25    /// The specific error that occurred while parsing the JSON.
26    pub kind: JsonErrorKind,
27}
28
29impl<'input> JsonError<'input> {
30    /// Creates a new `JsonParseErrorWithContext`.
31    ///
32    /// # Arguments
33    ///
34    /// * `kind` - The kind of JSON error encountered.
35    /// * `input` - The original input being parsed.
36    /// * `pos` - The position in the input where the error occurred.
37    pub fn new(kind: JsonErrorKind, input: &'input [u8], span: Span, path: String) -> Self {
38        Self {
39            input: alloc::borrow::Cow::Borrowed(input),
40            span,
41            kind,
42            path,
43        }
44    }
45
46    /// Returns a human-readable error message for this JSON error.
47    pub fn message(&self) -> String {
48        match &self.kind {
49            JsonErrorKind::UnexpectedEof(msg) => format!("Unexpected end of file: {}", msg.red()),
50            JsonErrorKind::MissingField(fld) => format!("Missing required field: {}", fld.red()),
51            JsonErrorKind::UnexpectedToken { got, wanted } => {
52                format!(
53                    "Unexpected token: got {}, wanted {}",
54                    got.red(),
55                    wanted.green()
56                )
57            }
58            JsonErrorKind::NumberOutOfRange(n) => {
59                format!("Number out of range: {}", n.red())
60            }
61            JsonErrorKind::StringAsNumber(s) => {
62                format!("Expected a string but got number: {}", s.red())
63            }
64            JsonErrorKind::UnknownField { field_name, shape } => {
65                format!(
66                    "Unknown field: {} for shape {}",
67                    field_name.red(),
68                    shape.yellow()
69                )
70            }
71            JsonErrorKind::InvalidUtf8(e) => format!("Invalid UTF-8 encoding: {}", e.red()),
72            JsonErrorKind::ReflectError(e) => format!("{e}"),
73            JsonErrorKind::SyntaxError(e) => format!("{e}"),
74            JsonErrorKind::Unimplemented(s) => {
75                format!("Feature not yet implemented: {}", s.yellow())
76            }
77            JsonErrorKind::UnsupportedType { got, wanted } => {
78                format!(
79                    "Unsupported type: got {}, wanted {}",
80                    got.red(),
81                    wanted.green()
82                )
83            }
84            JsonErrorKind::NoSuchVariant { name, enum_shape } => {
85                format!(
86                    "Enum variant not found: {} in enum {}",
87                    name.red(),
88                    enum_shape.yellow()
89                )
90            }
91        }
92    }
93}
94
95/// An error kind for JSON parsing.
96#[derive(Debug, PartialEq, Clone)]
97pub enum JsonErrorKind {
98    /// The input ended unexpectedly while parsing JSON.
99    UnexpectedEof(&'static str),
100    /// A required struct field was missing at the end of JSON input.
101    MissingField(&'static str),
102    /// An unexpected token was encountered in the input.
103    UnexpectedToken {
104        /// The hero we got
105        got: Token,
106
107        /// The hero we wanted
108        wanted: &'static str,
109    },
110    /// A number is out of range.
111    NumberOutOfRange(f64),
112    /// An unexpected String was encountered in the input.
113    StringAsNumber(String),
114    /// An unexpected field name was encountered in the input.
115    UnknownField {
116        /// The name of the field that was not recognized
117        field_name: String,
118        /// The shape definition where the unknown field was encountered
119        shape: &'static Shape,
120    },
121    /// A string that could not be built into valid UTF-8 Unicode
122    InvalidUtf8(String),
123    /// An error occurred while reflecting a type.
124    ReflectError(ReflectError),
125    /// An error occurred while parsing a token.
126    SyntaxError(TokenErrorKind),
127    /// Some feature is not yet implemented (under development).
128    Unimplemented(&'static str),
129    /// An unsupported type was encountered.
130    UnsupportedType {
131        /// The shape we got
132        got: &'static Shape,
133
134        /// The shape we wanted
135        wanted: &'static str,
136    },
137    /// An enum variant name that doesn't exist in the enum definition.
138    NoSuchVariant {
139        /// The name of the variant that was not found
140        name: String,
141
142        /// The enum shape definition where the variant was looked up
143        enum_shape: &'static Shape,
144    },
145}
146
147impl From<ReflectError> for JsonErrorKind {
148    fn from(err: ReflectError) -> Self {
149        JsonErrorKind::ReflectError(err)
150    }
151}
152
153#[cfg(not(feature = "rich-diagnostics"))]
154impl core::fmt::Display for JsonError<'_> {
155    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
156        write!(
157            f,
158            "{} at byte {} in path {}",
159            self.message(),
160            self.span.start,
161            self.path
162        )
163    }
164}
165
166#[cfg(feature = "rich-diagnostics")]
167impl core::fmt::Display for JsonError<'_> {
168    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
169        let Ok(input_str) = core::str::from_utf8(&self.input[..]) else {
170            return write!(f, "(JSON input was invalid UTF-8)");
171        };
172
173        let source_id = "json";
174        let span_start = self.span.start();
175        let span_end = self.span.end();
176
177        let mut report = Report::build(ReportKind::Error, (source_id, span_start..span_end))
178            .with_message(format!("Error at {}", self.path.yellow()))
179            .with_config(Config::new().with_index_type(IndexType::Byte));
180
181        let label = Label::new((source_id, span_start..span_end))
182            .with_message(self.message())
183            .with_color(Color::Red);
184
185        report = report.with_label(label);
186
187        let source = Source::from(input_str);
188
189        let mut writer = Vec::new();
190        let cache = (source_id, &source);
191
192        if report.finish().write(cache, &mut writer).is_err() {
193            return write!(f, "Error formatting with ariadne");
194        }
195
196        if let Ok(output) = String::from_utf8(writer) {
197            write!(f, "{}", output)
198        } else {
199            write!(f, "Error converting ariadne output to string")
200        }
201    }
202}
203
204impl core::fmt::Debug for JsonError<'_> {
205    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
206        core::fmt::Display::fmt(self, f)
207    }
208}
209
210impl core::error::Error for JsonError<'_> {}