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#[derive(Debug, Clone, PartialEq)]
8pub enum Error {
9    InvalidUtf8 {
10        location: Location,
11    },
12    UnsupportedNull {
13        text: String,
14        location: Location,
15    },
16    UnsupportedType {
17        text: String,
18        location: Location,
19        found: &'static str,
20    },
21    Parse {
22        text: String,
23        location: Option<Location>,
24        message: String,
25    },
26}
27
28fn located_message(location: &Location, message: &str) -> String {
29    format!("{message} at {location}")
30}
31
32impl Display for Error {
33    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
34        match self {
35            Self::InvalidUtf8 { location } => {
36                write!(f, "invalid utf-8 in configuration input from {location}")?;
37            }
38            Self::UnsupportedNull { location, .. } => {
39                write!(
40                    f,
41                    "{}",
42                    located_message(
43                        location,
44                        "null values are not supported in configuration input",
45                    )
46                )?;
47            }
48            Self::UnsupportedType {
49                location, found, ..
50            } => {
51                write!(
52                    f,
53                    "{}",
54                    located_message(
55                        location,
56                        &format!("unsupported configuration input type `{found}`"),
57                    )
58                )?;
59            }
60            Self::Parse {
61                location: Some(location),
62                message,
63                ..
64            } => write!(f, "{}", located_message(location, message))?,
65            Self::Parse { message, .. } => write!(f, "{message}")?,
66        }
67
68        if !f.alternate() {
69            return Ok(());
70        }
71
72        let (text, location) = match self {
73            Self::UnsupportedNull { text, location, .. }
74            | Self::UnsupportedType { text, location, .. } => (text.as_str(), location),
75            Self::Parse {
76                text,
77                location: Some(location),
78                ..
79            } => (text.as_str(), location),
80            _ => return Ok(()),
81        };
82
83        let line_number = location.line.map(|line| line.get() as usize);
84        let column = location.column.map(|column| column.get() as usize);
85        let highlight = location
86            .length
87            .map_or(1, |length| length.get() as usize)
88            .max(1);
89
90        if let Some(line_number) = line_number {
91            let lines: Vec<&str> = text.split('\n').collect();
92            let start = if line_number > 1 { line_number - 2 } else { 0 };
93            let end = if line_number + 1 < lines.len() {
94                line_number + 1
95            } else {
96                lines.len()
97            };
98            let gutter_width = end.to_string().len();
99            let mut line_index = start;
100            while line_index < end {
101                let display_line = line_index + 1;
102                let line_text = display_line.to_string();
103                write!(f, "\n  ")?;
104                for _ in 0..gutter_width.saturating_sub(line_text.len()) {
105                    write!(f, " ")?;
106                }
107                write!(f, "{line_text} | ")?;
108                write!(f, "{}", lines[line_index])?;
109                if display_line == line_number {
110                    write!(f, "\n  ")?;
111                    for _ in 0..gutter_width.saturating_sub(line_text.len()) {
112                        write!(f, " ")?;
113                    }
114                    for _ in 0..line_text.len() + 1 {
115                        write!(f, " ")?;
116                    }
117                    write!(f, "| ")?;
118                    if let Some(column_number) = column {
119                        for _ in 1..column_number {
120                            write!(f, " ")?;
121                        }
122                    }
123                    for _ in 0..highlight {
124                        write!(f, "^")?;
125                    }
126                }
127                line_index += 1;
128            }
129        } else {
130            write!(f, "\n  {text}")?;
131            if let Some(column_number) = column {
132                write!(f, "\n  ")?;
133                for _ in 1..column_number {
134                    write!(f, " ")?;
135                }
136                for _ in 0..highlight {
137                    write!(f, "^")?;
138                }
139            }
140        }
141
142        Ok(())
143    }
144}
145
146impl std::error::Error for Error {}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use crate::Location;
152
153    #[test]
154    fn default_display_is_single_line() {
155        let error = Error::UnsupportedNull {
156            text: "foo: bar\nbaz: ~\n".to_string(),
157            location: Location::at("file", "config.yaml", Some(2), Some(7), None),
158        };
159        let message = error.to_string();
160        assert!(!message.contains('\n'));
161        assert!(!message.contains('^'));
162        assert!(message.contains("file:config.yaml:2:7"));
163    }
164
165    #[test]
166    fn alternate_display_underlines_token() {
167        let error = Error::UnsupportedNull {
168            text: "foo: bar\nbaz: null\n".to_string(),
169            location: Location::at("file", "config.yaml", Some(2), Some(6), Some(4)),
170        };
171        let message = format!("{error:#}");
172        assert!(message.contains("^^^^"));
173        assert!(message.contains("baz: null"));
174    }
175
176    #[test]
177    fn alternate_display_aligns_gutter_pipe() {
178        let error = Error::UnsupportedNull {
179            text: "foo: bar\n\nbaz:\n\n  qux: ~\n".to_string(),
180            location: Location::at("file", "config.yaml", Some(5), Some(8), None),
181        };
182        let message = format!("{error:#}");
183        let source_line = message
184            .lines()
185            .find(|line| line.contains("qux: ~"))
186            .expect("source line");
187        let underline_line = message
188            .lines()
189            .find(|line| line.contains('^'))
190            .expect("underline line");
191        let source_pipe = source_line.find('|').expect("source pipe");
192        let underline_pipe = underline_line.find('|').expect("underline pipe");
193        assert_eq!(source_pipe, underline_pipe);
194    }
195}