error_accumulator/
path.rs

1//! [`SourcePath`] to identify the path to the source of an accumulated error.
2
3use std::{borrow::Cow, fmt, num::ParseIntError, str::FromStr};
4
5const INVALID_FIELD_NAME_CHARS: [char; 3] = ['.', '[', ']'];
6
7/// Errors parsing a [`SourcePath`] or its components.
8#[derive(Debug, thiserror::Error)]
9pub enum Error {
10    /// Invalid [`FieldName`].
11    #[error(
12        "failed to parse '{0}' as it contains at least one invalid character: {INVALID_FIELD_NAME_CHARS:?}"
13    )]
14    InvalidCharInName(String),
15    /// Incomplete array path.
16    #[error("array segment '{0}' does not contain proper brackets")]
17    IncompleteArraySegment(String),
18    /// Invalid index in array path.
19    #[error("invalid index")]
20    InvalidIdx(#[from] ParseIntError),
21}
22
23/// The full path to source of error from the input.
24///
25/// Composed of [`PathSegment`]s.
26#[derive(Debug, Clone, Default, PartialEq, Eq)]
27pub struct SourcePath {
28    segments: Vec<PathSegment>,
29}
30
31/// A segment of a full [`SourcePath`].
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum PathSegment {
34    /// The segment references a field.
35    Field(FieldName),
36    /// The segement references an element of an array.
37    Array {
38        /// The array's name.
39        name: FieldName,
40        /// The element's position within the array.
41        index: usize,
42    },
43}
44
45/// A valid name of an input's field.
46///
47/// At the moment most characters are allowed excluding `.`, `[`, and `]`. This
48/// might change in the future.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct FieldName(Cow<'static, str>);
51
52impl SourcePath {
53    /// Construct a new, empty path.
54    pub fn new() -> Self {
55        Default::default()
56    }
57
58    /// Append a new segment to the path.
59    pub fn join(&self, segment: PathSegment) -> Self {
60        let mut new = self.clone();
61        new.segments.push(segment);
62        new
63    }
64
65    /// Check if the other path has the same base as the path at hand.
66    ///
67    /// For example: `foo.bar` is the base of `foo.bar.baz`.
68    pub fn is_matching_base(&self, base: &Self) -> bool {
69        base.segments
70            .iter()
71            .zip(&self.segments)
72            .all(|(base, to_match)| base == to_match)
73    }
74}
75
76impl fmt::Display for SourcePath {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        if self.segments.is_empty() {
79            f.write_str("root")
80        } else {
81            let mut segments = self.segments.iter();
82            let start = segments.next().expect("segments is not empty");
83            write!(f, "{start}")?;
84            for segment in segments {
85                write!(f, ".{segment}")?;
86            }
87            Ok(())
88        }
89    }
90}
91
92impl FromStr for SourcePath {
93    type Err = Error;
94
95    fn from_str(s: &str) -> Result<Self, Self::Err> {
96        let segments = s
97            .split('.')
98            .map(|segment| segment.parse())
99            .collect::<Result<Vec<_>, _>>()?;
100        Ok(Self { segments })
101    }
102}
103
104impl PathSegment {
105    /// Construct a field segment.
106    pub fn field(name: FieldName) -> Self {
107        Self::Field(name)
108    }
109
110    /// Construct an array segment.
111    pub fn array(name: FieldName, index: usize) -> Self {
112        Self::Array { name, index }
113    }
114}
115
116impl fmt::Display for PathSegment {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        match self {
119            PathSegment::Field(field) => write!(f, "{field}"),
120            PathSegment::Array { name, index } => write!(f, "{name}[{index}]"),
121        }
122    }
123}
124
125impl FromStr for PathSegment {
126    type Err = Error;
127
128    fn from_str(s: &str) -> Result<Self, Self::Err> {
129        if s.ends_with(']') {
130            if let Some(idx) = s.find('[') {
131                let idx_str = &s[idx + 1..s.len() - 1];
132                let field_idx = idx_str.parse()?;
133                return Ok(Self::Array {
134                    name: s[..idx].parse()?,
135                    index: field_idx,
136                });
137            } else {
138                return Err(Error::IncompleteArraySegment(s.to_string()));
139            }
140        }
141
142        Ok(Self::Field(s.parse()?))
143    }
144}
145
146impl fmt::Display for FieldName {
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        f.write_str(&self.0)
149    }
150}
151
152impl FieldName {
153    // Used in `field!` macro.
154    #[doc(hidden)]
155    pub const fn new_unchecked(name: &'static str) -> Self {
156        Self(Cow::Borrowed(name))
157    }
158
159    /// Access the inner name as a string slice.
160    pub fn as_str(&self) -> &str {
161        self.as_ref()
162    }
163}
164
165impl AsRef<str> for FieldName {
166    fn as_ref(&self) -> &str {
167        &self.0
168    }
169}
170
171impl FromStr for FieldName {
172    type Err = Error;
173
174    fn from_str(s: &str) -> Result<Self, Self::Err> {
175        validate_str_as_field_name(s)?;
176
177        Ok(Self(Cow::Owned(s.to_string())))
178    }
179}
180
181impl TryFrom<String> for FieldName {
182    type Error = Error;
183
184    fn try_from(name: String) -> Result<Self, Self::Error> {
185        validate_str_as_field_name(&name)?;
186        Ok(Self(Cow::Owned(name)))
187    }
188}
189
190impl<'a> TryFrom<&'a str> for FieldName {
191    type Error = <Self as FromStr>::Err;
192
193    fn try_from(name: &'a str) -> Result<Self, Self::Error> {
194        name.parse()
195    }
196}
197
198fn validate_str_as_field_name(name: &str) -> Result<(), Error> {
199    if name.contains(INVALID_FIELD_NAME_CHARS) {
200        Err(Error::InvalidCharInName(name.to_string()))
201    } else {
202        Ok(())
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use crate::test_util::n;
210
211    #[test]
212    fn should_serialize_root() {
213        let path = SourcePath::new();
214
215        let string = path.to_string();
216
217        assert_eq!(string.as_str(), "root");
218    }
219
220    #[test]
221    fn should_display_multi_segment_path() {
222        let path = SourcePath::new()
223            .join(PathSegment::field(n("foo")))
224            .join(PathSegment::array(n("bar"), 42))
225            .join(PathSegment::field(n("baz")));
226
227        let string = path.to_string();
228
229        assert_eq!(string.as_str(), "foo.bar[42].baz");
230    }
231
232    #[test]
233    fn should_parse_path() {
234        let expect = SourcePath::new()
235            .join(PathSegment::array(n("foo"), 21))
236            .join(PathSegment::field(n("bar")))
237            .join(PathSegment::field(n("xyz")));
238        let path = "foo[21].bar.xyz";
239
240        let parsed = path.parse::<SourcePath>().unwrap();
241
242        assert_eq!(parsed, expect);
243    }
244}