Skip to main content

tanzim_value/
error.rs

1use crate::Location;
2use std::fmt::{self, Display, Formatter};
3
4/// Error while deserializing configuration input.
5///
6/// [`Display`] is one line by default; use `{error:#}` for source context and caret.
7///
8/// [`Location`] is boxed so the whole [`Error`] stays small enough to return by value without
9/// tripping `clippy::result_large_err` (a [`Location`] now carries the full originating
10/// [`tanzim_source::Source`]).
11#[derive(Debug, Clone, PartialEq)]
12pub enum Error {
13    /// Configuration input was not valid UTF-8.
14    InvalidUtf8 {
15        /// Where the invalid input came from.
16        location: Box<Location>,
17    },
18    /// A parsed value's type is not supported in this context.
19    UnsupportedType {
20        /// Where the unsupported value came from.
21        location: Box<Location>,
22        /// Name of the offending type.
23        found: &'static str,
24    },
25    /// A source failed to parse into a [`crate::Value`] tree.
26    Parse {
27        /// Where the parse failure occurred, if known.
28        location: Option<Box<Location>>,
29        /// Human-readable description of the failure.
30        message: String,
31    },
32    /// A value could not be deserialized into the requested type (`serde` Cargo feature).
33    ///
34    /// The offending node's [`Location`] is stamped on by the deserializer (see
35    /// `Error::or_location`); because every parsed node's `Location` already carries its
36    /// pre-rendered source [`snippet`](Location::snippet), `{error:#}` renders a caret underline
37    /// without any post-hoc source lookup.
38    #[cfg(feature = "serde")]
39    Deserialize {
40        /// Human-readable description of the failure.
41        message: String,
42        /// Where the failure occurred, if known.
43        location: Option<Box<Location>>,
44    },
45}
46
47impl Error {
48    /// Stamp `location` onto a [`Error::Deserialize`] that has none yet, so errors bubbling up from
49    /// a leaf get the nearest enclosing node's position (and its pre-rendered snippet). Errors that
50    /// already carry a location (or are not deserialize errors) are returned unchanged.
51    #[cfg(feature = "serde")]
52    pub(crate) fn or_location(mut self, location: &Location) -> Self {
53        if let Self::Deserialize {
54            location: slot @ None,
55            ..
56        } = &mut self
57        {
58            *slot = Some(Box::new(location.clone()));
59        }
60        self
61    }
62}
63
64fn located_message(location: &Location, message: &str) -> String {
65    format!("{message} at {location}")
66}
67
68impl Display for Error {
69    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
70        match self {
71            Self::InvalidUtf8 { location } => {
72                write!(f, "invalid utf-8 in configuration input from {location}")?;
73            }
74            Self::UnsupportedType { location, found } => {
75                write!(
76                    f,
77                    "{}",
78                    located_message(
79                        location,
80                        &format!("unsupported configuration input type `{found}`"),
81                    )
82                )?;
83            }
84            Self::Parse {
85                location: Some(location),
86                message,
87            } => write!(f, "{}", located_message(location, message))?,
88            Self::Parse { message, .. } => write!(f, "{message}")?,
89            #[cfg(feature = "serde")]
90            Self::Deserialize {
91                message,
92                location: Some(location),
93            } => write!(
94                f,
95                "{}",
96                located_message(
97                    location,
98                    &format!("failed to deserialize configuration: {message}"),
99                )
100            )?,
101            #[cfg(feature = "serde")]
102            Self::Deserialize { message, .. } => {
103                write!(f, "failed to deserialize configuration: {message}")?
104            }
105        }
106
107        // Alternate form appends the offending value's pre-rendered source snippet (gutter + caret),
108        // computed once at parse time and stored on the `Location` — nothing is recomputed here.
109        if f.alternate() {
110            let location = match self {
111                Self::InvalidUtf8 { location } | Self::UnsupportedType { location, .. } => {
112                    Some(location.as_ref())
113                }
114                Self::Parse {
115                    location: Some(location),
116                    ..
117                } => Some(location.as_ref()),
118                #[cfg(feature = "serde")]
119                Self::Deserialize {
120                    location: Some(location),
121                    ..
122                } => Some(location.as_ref()),
123                _ => None,
124            };
125            if let Some(location) = location
126                && !location.snippet.is_empty()
127            {
128                write!(f, "\n{}", location.snippet)?;
129            }
130        }
131
132        Ok(())
133    }
134}
135
136impl std::error::Error for Error {}