facet_format_toml/
error.rs

1//! Error types for TOML deserialization and serialization.
2
3use alloc::format;
4use alloc::string::{String, ToString};
5use alloc::vec::Vec;
6use core::fmt::{self, Display};
7
8use facet_reflect::{ReflectError, Span};
9
10/// Error type for TOML operations.
11#[derive(Debug, Clone)]
12pub struct TomlError {
13    /// The specific kind of error
14    pub kind: TomlErrorKind,
15    /// Source span where the error occurred
16    pub span: Option<Span>,
17    /// The source input (for diagnostics)
18    pub source_code: Option<String>,
19}
20
21impl Display for TomlError {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        write!(f, "{}", self.kind)
24    }
25}
26
27impl std::error::Error for TomlError {}
28
29impl miette::Diagnostic for TomlError {
30    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
31        Some(Box::new(self.kind.code()))
32    }
33
34    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
35        self.source_code
36            .as_ref()
37            .map(|s| s as &dyn miette::SourceCode)
38    }
39
40    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
41        // Handle MissingField with multiple spans
42        if let TomlErrorKind::MissingField {
43            field,
44            table_start,
45            table_end,
46        } = &self.kind
47        {
48            let mut labels = Vec::new();
49            if let Some(start) = table_start {
50                labels.push(miette::LabeledSpan::new(
51                    Some("table started here".into()),
52                    start.offset,
53                    start.len,
54                ));
55            }
56            if let Some(end) = table_end {
57                labels.push(miette::LabeledSpan::new(
58                    Some(format!("table ended without field `{field}`")),
59                    end.offset,
60                    end.len,
61                ));
62            }
63            if labels.is_empty() {
64                return None;
65            }
66            return Some(Box::new(labels.into_iter()));
67        }
68
69        // Default: single span with label
70        let span = self.span?;
71        Some(Box::new(core::iter::once(miette::LabeledSpan::new(
72            Some(self.kind.label()),
73            span.offset,
74            span.len,
75        ))))
76    }
77}
78
79impl TomlError {
80    /// Create a new error with span information
81    pub fn new(kind: TomlErrorKind, span: Span) -> Self {
82        TomlError {
83            kind,
84            span: Some(span),
85            source_code: None,
86        }
87    }
88
89    /// Create an error without span information
90    pub fn without_span(kind: TomlErrorKind) -> Self {
91        TomlError {
92            kind,
93            span: None,
94            source_code: None,
95        }
96    }
97
98    /// Attach source code for rich diagnostics
99    pub fn with_source(mut self, source: &str) -> Self {
100        self.source_code = Some(source.to_string());
101        self
102    }
103}
104
105/// Specific error kinds for TOML operations
106#[derive(Debug, Clone)]
107pub enum TomlErrorKind {
108    /// Parse error from toml_parser
109    Parse(String),
110    /// Unexpected value type
111    UnexpectedType {
112        /// What type was expected
113        expected: &'static str,
114        /// What type was found
115        got: &'static str,
116    },
117    /// Unexpected end of input
118    UnexpectedEof {
119        /// What was expected before EOF
120        expected: &'static str,
121    },
122    /// Unknown field in table
123    UnknownField {
124        /// The unknown field name
125        field: String,
126        /// List of valid field names
127        expected: Vec<&'static str>,
128        /// Suggested field name (if similar to an expected field)
129        suggestion: Option<&'static str>,
130    },
131    /// Missing required field
132    MissingField {
133        /// The name of the missing field
134        field: &'static str,
135        /// Span of the table start (opening header)
136        table_start: Option<Span>,
137        /// Span of the table end
138        table_end: Option<Span>,
139    },
140    /// Invalid value for type
141    InvalidValue {
142        /// Description of why the value is invalid
143        message: String,
144    },
145    /// Reflection error from facet-reflect
146    Reflect(ReflectError),
147    /// Number out of range
148    NumberOutOfRange {
149        /// The numeric value that was out of range
150        value: String,
151        /// The target type that couldn't hold the value
152        target_type: &'static str,
153    },
154    /// Duplicate key in table
155    DuplicateKey {
156        /// The key that appeared more than once
157        key: String,
158    },
159    /// Invalid UTF-8 in string
160    InvalidUtf8,
161    /// Solver error (for flattened types)
162    Solver(String),
163    /// Serialization error
164    Serialize(String),
165}
166
167impl Display for TomlErrorKind {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        match self {
170            TomlErrorKind::Parse(msg) => write!(f, "parse error: {msg}"),
171            TomlErrorKind::UnexpectedType { expected, got } => {
172                write!(f, "type mismatch: expected {expected}, got {got}")
173            }
174            TomlErrorKind::UnexpectedEof { expected } => {
175                write!(f, "unexpected end of input, expected {expected}")
176            }
177            TomlErrorKind::UnknownField {
178                field,
179                expected,
180                suggestion,
181            } => {
182                write!(f, "unknown field `{field}`, expected one of: {expected:?}")?;
183                if let Some(suggested) = suggestion {
184                    write!(f, " (did you mean `{suggested}`?)")?;
185                }
186                Ok(())
187            }
188            TomlErrorKind::MissingField { field, .. } => {
189                write!(f, "missing required field `{field}`")
190            }
191            TomlErrorKind::InvalidValue { message } => {
192                write!(f, "invalid value: {message}")
193            }
194            TomlErrorKind::Reflect(e) => write!(f, "reflection error: {e}"),
195            TomlErrorKind::NumberOutOfRange { value, target_type } => {
196                write!(f, "number `{value}` out of range for {target_type}")
197            }
198            TomlErrorKind::DuplicateKey { key } => {
199                write!(f, "duplicate key `{key}`")
200            }
201            TomlErrorKind::InvalidUtf8 => write!(f, "invalid UTF-8 sequence"),
202            TomlErrorKind::Solver(msg) => write!(f, "solver error: {msg}"),
203            TomlErrorKind::Serialize(msg) => write!(f, "serialization error: {msg}"),
204        }
205    }
206}
207
208impl TomlErrorKind {
209    /// Get an error code for this kind of error.
210    pub fn code(&self) -> &'static str {
211        match self {
212            TomlErrorKind::Parse(_) => "toml::parse",
213            TomlErrorKind::UnexpectedType { .. } => "toml::type_mismatch",
214            TomlErrorKind::UnexpectedEof { .. } => "toml::unexpected_eof",
215            TomlErrorKind::UnknownField { .. } => "toml::unknown_field",
216            TomlErrorKind::MissingField { .. } => "toml::missing_field",
217            TomlErrorKind::InvalidValue { .. } => "toml::invalid_value",
218            TomlErrorKind::Reflect(_) => "toml::reflect",
219            TomlErrorKind::NumberOutOfRange { .. } => "toml::number_out_of_range",
220            TomlErrorKind::DuplicateKey { .. } => "toml::duplicate_key",
221            TomlErrorKind::InvalidUtf8 => "toml::invalid_utf8",
222            TomlErrorKind::Solver(_) => "toml::solver",
223            TomlErrorKind::Serialize(_) => "toml::serialize",
224        }
225    }
226
227    /// Get a label describing where/what the error points to.
228    pub fn label(&self) -> String {
229        match self {
230            TomlErrorKind::Parse(msg) => format!("parse error: {msg}"),
231            TomlErrorKind::UnexpectedType { expected, got } => {
232                format!("expected {expected}, got {got}")
233            }
234            TomlErrorKind::UnexpectedEof { expected } => format!("expected {expected}"),
235            TomlErrorKind::UnknownField {
236                field, suggestion, ..
237            } => {
238                if let Some(suggested) = suggestion {
239                    format!("unknown field '{field}' - did you mean '{suggested}'?")
240                } else {
241                    format!("unknown field '{field}'")
242                }
243            }
244            TomlErrorKind::MissingField { field, .. } => format!("missing field '{field}'"),
245            TomlErrorKind::InvalidValue { .. } => "invalid value".into(),
246            TomlErrorKind::Reflect(_) => "reflection error".into(),
247            TomlErrorKind::NumberOutOfRange { target_type, .. } => {
248                format!("out of range for {target_type}")
249            }
250            TomlErrorKind::DuplicateKey { key } => format!("duplicate key '{key}'"),
251            TomlErrorKind::InvalidUtf8 => "invalid UTF-8".into(),
252            TomlErrorKind::Solver(_) => "solver error".into(),
253            TomlErrorKind::Serialize(_) => "serialization error".into(),
254        }
255    }
256}
257
258impl From<ReflectError> for TomlError {
259    fn from(err: ReflectError) -> Self {
260        TomlError {
261            kind: TomlErrorKind::Reflect(err),
262            span: None,
263            source_code: None,
264        }
265    }
266}
267
268/// Result type for TOML operations
269#[allow(dead_code)]
270pub type Result<T> = core::result::Result<T, TomlError>;