1use std::{borrow::Cow, fmt::Write as _, str::FromStr};
2
3#[derive(Clone, thiserror::Error)]
8pub struct Error {
9 message: String,
11
12 path: Vec<PathEntry>,
17}
18
19impl std::fmt::Display for Error {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 if !self.path.is_empty() {
22 write!(f, "{}: ", self.path())?;
23 }
24
25 f.write_str(&self.message)
26 }
27}
28
29impl std::fmt::Debug for Error {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 f.debug_struct("Error")
32 .field("message", &self.message)
33 .field("path", &self.path())
34 .finish()
35 }
36}
37
38impl Error {
39 pub fn path(&self) -> String {
40 path_str(None, self.path.iter().rev())
41 }
42
43 pub(crate) fn new_static(message: &'static str) -> Self {
45 Self {
46 message: message.to_string(),
47 path: vec![],
48 }
49 }
50}
51
52#[allow(unused)]
55impl Error {
56 pub(crate) fn new(message: String) -> Self {
58 Self {
59 message,
60 path: vec![],
61 }
62 }
63
64 pub(crate) fn with_field(mut self, field: &'static str) -> Self {
66 self.path.push(PathEntry::from(field));
67 self
68 }
69
70 pub(crate) fn with_index(mut self, index: usize) -> Self {
72 self.path.push(PathEntry::Index(index));
73 self
74 }
75}
76
77pub(crate) fn path_str<'a, I, Iter>(prefix: Option<&'static str>, path: I) -> String
82where
83 I: IntoIterator<IntoIter = Iter>,
84 Iter: Iterator<Item = &'a PathEntry> + DoubleEndedIterator,
85{
86 let path_iter = path.into_iter();
87 let mut buf = String::with_capacity(16 + prefix.map_or(0, |s| s.len()));
90
91 if let Some(prefix) = prefix {
92 let _ = buf.write_fmt(format_args!("{prefix}/"));
93 }
94
95 for (i, path_entry) in path_iter.enumerate() {
96 if i > 0 && path_entry.is_field() {
97 buf.push('.');
98 }
99 let _ = write!(&mut buf, "{}", path_entry);
100 }
101
102 buf
103}
104
105#[allow(unused)]
116pub(crate) trait ErrorContext<T>: Sized {
117 fn with_field(self, field: &'static str) -> Result<T, Error>;
118 fn with_index(self, index: usize) -> Result<T, Error>;
119
120 fn with_fields(self, a: &'static str, b: &'static str) -> Result<T, Error> {
123 self.with_field(b).with_field(a)
124 }
125
126 fn with_field_index(self, field: &'static str, index: usize) -> Result<T, Error> {
129 self.with_index(index).with_field(field)
130 }
131}
132
133#[derive(Debug, PartialEq, Eq, Clone)]
136pub(crate) enum PathEntry {
137 Field(Cow<'static, str>),
138 Index(usize),
139}
140
141impl PathEntry {
142 fn is_field(&self) -> bool {
143 matches!(self, PathEntry::Field(_))
144 }
145}
146
147impl std::fmt::Display for PathEntry {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 match self {
150 PathEntry::Field(field) => f.write_str(field),
151 PathEntry::Index(idx) => f.write_fmt(format_args!("[{idx}]")),
152 }
153 }
154}
155
156impl FromStr for PathEntry {
157 type Err = &'static str;
158
159 fn from_str(s: &str) -> Result<Self, Self::Err> {
160 if s.starts_with('[') {
162 if s.len() <= 2 || !s.ends_with(']') {
163 return Err("invalid field index: missing closing bracket");
164 }
165
166 let idx_str = &s[1..s.len() - 1];
169 let idx = idx_str
170 .parse()
171 .map_err(|_| "invalid field index: field index must be a number")?;
172
173 return Ok(PathEntry::Index(idx));
174 }
175
176 Ok(PathEntry::from(s.to_string()))
178 }
179}
180
181impl From<String> for PathEntry {
182 fn from(value: String) -> Self {
183 PathEntry::Field(Cow::Owned(value))
184 }
185}
186
187impl From<&'static str> for PathEntry {
188 fn from(value: &'static str) -> Self {
189 PathEntry::Field(Cow::Borrowed(value))
190 }
191}
192
193impl<T> ErrorContext<T> for Result<T, Error> {
194 fn with_field(self, field: &'static str) -> Result<T, Error> {
195 match self {
196 Ok(v) => Ok(v),
197 Err(err) => Err(err.with_field(field)),
198 }
199 }
200
201 fn with_index(self, index: usize) -> Result<T, Error> {
202 match self {
203 Ok(v) => Ok(v),
204 Err(err) => Err(err.with_index(index)),
205 }
206 }
207}
208
209#[cfg(test)]
210mod test {
211 use super::*;
212
213 #[test]
214 fn test_error_message() {
215 fn baz() -> Result<(), Error> {
216 Err(Error::new_static("it broke"))
217 }
218
219 fn bar() -> Result<(), Error> {
220 baz().with_field_index("baz", 2)
221 }
222
223 fn foo() -> Result<(), Error> {
224 bar().with_field("bar")
225 }
226
227 assert_eq!(foo().unwrap_err().to_string(), "bar.baz[2]: it broke",)
228 }
229
230 #[test]
231 fn test_path_strings() {
232 let path = &[
233 PathEntry::Index(0),
234 PathEntry::from("hi"),
235 PathEntry::from("dr"),
236 PathEntry::Index(2),
237 PathEntry::from("nick"),
238 ];
239 let string = "[0].hi.dr[2].nick";
240 assert_eq!(path_str(None, path), string);
241
242 let path = &[
243 PathEntry::from("hi"),
244 PathEntry::from("dr"),
245 PathEntry::from("nick"),
246 ];
247 let string = "prefix/hi.dr.nick";
248 assert_eq!(path_str(Some("prefix"), path), string);
249 }
250}