1use colored::Colorize;
18use std::{
19 error::Error as StdError,
20 fmt, io,
21 ops::RangeInclusive,
22 path::{Path, PathBuf},
23};
24
25#[derive(Debug)]
27pub struct Position {
28 line: usize,
29 col: usize,
30}
31
32impl Position {
33 pub fn new(
36 line: usize,
37 col: usize,
38 ) -> Position {
39 Position { line, col }
40 }
41}
42
43pub struct Error<S> {
48 message: String,
49 path: PathBuf,
50 lines: S,
51 position: Position,
52}
53
54impl<S> fmt::Debug for Error<S> {
55 fn fmt(
56 &self,
57 f: &mut fmt::Formatter<'_>,
58 ) -> fmt::Result {
59 f.debug_struct("Error")
60 .field("message", &self.message)
61 .field("path", &self.path.display())
62 .field("position", &self.position)
63 .finish()
64 }
65}
66
67pub fn from_file<M, P>(
73 message: M,
74 path: P,
75 position: Position,
76) -> io::Result<Error<String>>
77where
78 P: AsRef<Path>,
79 M: AsRef<str>,
80{
81 Ok(from_lines(
82 message,
83 path.as_ref(),
84 std::fs::read_to_string(path.as_ref())?,
85 position,
86 ))
87}
88
89pub fn from_lines<M, P, S>(
95 message: M,
96 path: P,
97 lines: S,
98 position: Position,
99) -> Error<S>
100where
101 M: AsRef<str>,
102 P: AsRef<Path>,
103 S: AsRef<str>,
104{
105 Error {
106 message: message.as_ref().into(),
107 path: path.as_ref().into(),
108 lines,
109 position,
110 }
111}
112
113fn line_range(line: usize) -> RangeInclusive<usize> {
114 line.checked_sub(2).unwrap_or_default()..=line.checked_add(2).unwrap_or(std::usize::MAX)
115}
116
117impl<S> fmt::Display for Error<S>
122where
123 S: AsRef<str>,
124{
125 fn fmt(
126 &self,
127 f: &mut fmt::Formatter<'_>,
128 ) -> fmt::Result {
129 let Self {
130 message,
131 path,
132 lines,
133 position,
134 } = self;
135 let Position { line, col } = position;
136 let line_range = line_range(*line);
137 writeln!(f, "⚠️ {}\n", format!("error: {}", message).red())?;
138 writeln!(
139 f,
140 " {}\n",
141 format!("at {}:{}:{}", path.display(), line, col).dimmed()
142 )?;
143 let lines = lines
144 .as_ref()
145 .lines()
146 .enumerate()
147 .filter_map(|(idx, line)| {
148 let line_idx = idx + 1;
149 if line_range.contains(&line_idx) {
150 Some((line_idx, line))
151 } else {
152 None
153 }
154 })
155 .collect::<Vec<_>>();
156 let max_line = lines
157 .last()
158 .map(|(idx, _)| idx.to_string().len())
159 .unwrap_or_default();
160 for (idx, matched) in lines {
161 if idx == *line {
162 write!(f, "{} ", ">".red())?;
163 } else {
164 f.write_str(" ")?;
165 }
166 writeln!(
167 f,
168 " {}{}",
169 format!("{}{} |", " ".repeat(max_line - idx.to_string().len()), idx).dimmed(),
170 matched
171 )?;
172 }
173 Ok(())
174 }
175}
176
177impl<S> StdError for Error<S> where S: AsRef<str> {}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use pretty_assertions::assert_eq;
183 use std::env;
184
185 #[test]
186 fn impl_error() {
187 fn is<E>(_: E)
188 where
189 E: StdError,
190 {
191 }
192 is(from_lines("..", "...", "", Position::new(1, 1)))
193 }
194
195 #[test]
196 fn impl_custom_debug() {
197 assert_eq!(
198 format!(
199 "{:?}",
200 from_lines(
201 "error occurs here",
202 "path/to/file.txt",
203 ":)",
204 Position::new(1, 1)
205 )
206 ),
207 "Error { message: \"error occurs here\", path: \"path/to/file.txt\", position: Position { line: 1, col: 1 } }"
208 )
209 }
210
211 #[test]
212 fn it_works() {
213 env::set_var("NO_COLOR", "");
214 let expected = include_str!("../tests/expect.txt");
215 let err = from_lines(
216 "something is definitely wrong here",
217 "../tests/source.json",
218 include_str!("../tests/source.json"),
219 Position::new(3, 4),
220 );
221 assert_eq!(format!("{}", err), expected)
222 }
223
224 #[test]
225 fn line_range_is_expected() {
226 for (given, expect) in &[
227 (1, (0, 3)),
228 (2, (0, 4)),
229 (3, (1, 5)),
230 (std::usize::MAX, (std::usize::MAX - 2, std::usize::MAX)),
231 ] {
232 let (start, end) = expect;
233 let range = line_range(*given);
234 assert_eq!(start, range.start());
235 assert_eq!(end, range.end());
236 }
237 }
238}