1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
use std::{borrow::Cow, fmt::Write as _, str::FromStr};

/// An error converting a Junction API type into another type.
///
/// Errors should be treated as opaque, and contain a message about what went
/// wrong and a jsonpath style path to the field that caused problems.
#[derive(Clone, thiserror::Error)]
pub struct Error {
    // an error message
    message: String,

    // the reversed path to the field where the conversion error happened.
    //
    // the leaf of the path is built up at path[0] with the root of the
    // struct at the end. see ErrorContext for how this gets done.
    path: Vec<PathEntry>,
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if !self.path.is_empty() {
            write!(f, "{}: ", self.path())?;
        }

        f.write_str(&self.message)
    }
}

impl std::fmt::Debug for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Error")
            .field("message", &self.message)
            .field("path", &self.path())
            .finish()
    }
}

impl Error {
    pub fn path(&self) -> String {
        path_str(None, self.path.iter().rev())
    }

    /// Create a new error with a static message.
    pub(crate) fn new_static(message: &'static str) -> Self {
        Self {
            message: message.to_string(),
            path: vec![],
        }
    }
}

// these are handy, but mostly used in xds/kube conversion. don't gate them
// behind feature flags for now, just allow them to be unused.
#[allow(unused)]
impl Error {
    /// Create a new error with a message.
    pub(crate) fn new(message: String) -> Self {
        Self {
            message,
            path: vec![],
        }
    }

    /// Append a new field to this error's path.
    pub(crate) fn with_field(mut self, field: &'static str) -> Self {
        self.path.push(PathEntry::from(field));
        self
    }

    /// Append a new field index to this error's path.
    pub(crate) fn with_index(mut self, index: usize) -> Self {
        self.path.push(PathEntry::Index(index));
        self
    }
}

/// Join an iterator of PathEntry together into a path string.
///
/// This isn't quite `entries.join('.')` because index fields exist and have to
/// be bracketed.
pub(crate) fn path_str<'a, I, Iter>(prefix: Option<&'static str>, path: I) -> String
where
    I: IntoIterator<IntoIter = Iter>,
    Iter: Iterator<Item = &'a PathEntry> + DoubleEndedIterator,
{
    let path_iter = path.into_iter();
    // this is a random guess based on the fact that we'll often be allocating
    // something, but probably won't ever be allocating much.
    let mut buf = String::with_capacity(16 + prefix.map_or(0, |s| s.len()));

    if let Some(prefix) = prefix {
        let _ = buf.write_fmt(format_args!("{prefix}/"));
    }

    for (i, path_entry) in path_iter.enumerate() {
        if i > 0 && path_entry.is_field() {
            buf.push('.');
        }
        let _ = write!(&mut buf, "{}", path_entry);
    }

    buf
}

/// Add field-path context to an error by appending an entry to its path. Because
/// Context is added at the callsite this means a function can add its own fields
/// and the path ends up in the appropriate order.
///
/// This trait isn't meant to be implemented, but it's not explicitly sealed
/// because it's only `pub(crate)`. Don't implement it!
///
/// This trait is mostly used in xds/kube conversions, but leave it available
/// for now. It's not much code and may be helpful for identifying errors in
/// routes etc.
#[allow(unused)]
pub(crate) trait ErrorContext<T>: Sized {
    fn with_field(self, field: &'static str) -> Result<T, Error>;
    fn with_index(self, index: usize) -> Result<T, Error>;

    /// Shorthand for `with_field(b).with_field(a)` but in a more intuitive
    /// order.
    fn with_fields(self, a: &'static str, b: &'static str) -> Result<T, Error> {
        self.with_field(b).with_field(a)
    }

    /// Shorthand for `with_index(idx).with_field(name)`, but in a slightly more
    /// inutitive order.
    fn with_field_index(self, field: &'static str, index: usize) -> Result<T, Error> {
        self.with_index(index).with_field(field)
    }
}

/// A JSON-path style path entry. An entry is either a field name or an index
/// into a sequence.
#[derive(Debug, PartialEq, Eq, Clone)]
pub(crate) enum PathEntry {
    Field(Cow<'static, str>),
    Index(usize),
}

impl PathEntry {
    fn is_field(&self) -> bool {
        matches!(self, PathEntry::Field(_))
    }
}

impl std::fmt::Display for PathEntry {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PathEntry::Field(field) => f.write_str(field),
            PathEntry::Index(idx) => f.write_fmt(format_args!("[{idx}]")),
        }
    }
}

impl FromStr for PathEntry {
    type Err = &'static str;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        // an index is always at least 3 chars, starts with [ and ends with ]
        if s.starts_with('[') {
            if s.len() <= 2 || !s.ends_with(']') {
                return Err("invalid field index: missing closing bracket");
            }

            // safety: we know the first and last chars are [] so it's safe to
            // slice single bytes off the front and back.
            let idx_str = &s[1..s.len() - 1];
            let idx = idx_str
                .parse()
                .map_err(|_| "invalid field index: field index must be a number")?;

            return Ok(PathEntry::Index(idx));
        }

        // parse anything that's not an index as a field name
        Ok(PathEntry::from(s.to_string()))
    }
}

impl From<String> for PathEntry {
    fn from(value: String) -> Self {
        PathEntry::Field(Cow::Owned(value))
    }
}

impl From<&'static str> for PathEntry {
    fn from(value: &'static str) -> Self {
        PathEntry::Field(Cow::Borrowed(value))
    }
}

impl<T> ErrorContext<T> for Result<T, Error> {
    fn with_field(self, field: &'static str) -> Result<T, Error> {
        match self {
            Ok(v) => Ok(v),
            Err(err) => Err(err.with_field(field)),
        }
    }

    fn with_index(self, index: usize) -> Result<T, Error> {
        match self {
            Ok(v) => Ok(v),
            Err(err) => Err(err.with_index(index)),
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_error_message() {
        fn baz() -> Result<(), Error> {
            Err(Error::new_static("it broke"))
        }

        fn bar() -> Result<(), Error> {
            baz().with_field_index("baz", 2)
        }

        fn foo() -> Result<(), Error> {
            bar().with_field("bar")
        }

        assert_eq!(foo().unwrap_err().to_string(), "bar.baz[2]: it broke",)
    }

    #[test]
    fn test_path_strings() {
        let path = &[
            PathEntry::Index(0),
            PathEntry::from("hi"),
            PathEntry::from("dr"),
            PathEntry::Index(2),
            PathEntry::from("nick"),
        ];
        let string = "[0].hi.dr[2].nick";
        assert_eq!(path_str(None, path), string);

        let path = &[
            PathEntry::from("hi"),
            PathEntry::from("dr"),
            PathEntry::from("nick"),
        ];
        let string = "prefix/hi.dr.nick";
        assert_eq!(path_str(Some("prefix"), path), string);
    }
}