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