error_accumulator/
path.rs1use std::{borrow::Cow, fmt, num::ParseIntError, str::FromStr};
4
5const INVALID_FIELD_NAME_CHARS: [char; 3] = ['.', '[', ']'];
6
7#[derive(Debug, thiserror::Error)]
9pub enum Error {
10 #[error(
12 "failed to parse '{0}' as it contains at least one invalid character: {INVALID_FIELD_NAME_CHARS:?}"
13 )]
14 InvalidCharInName(String),
15 #[error("array segment '{0}' does not contain proper brackets")]
17 IncompleteArraySegment(String),
18 #[error("invalid index")]
20 InvalidIdx(#[from] ParseIntError),
21}
22
23#[derive(Debug, Clone, Default, PartialEq, Eq)]
27pub struct SourcePath {
28 segments: Vec<PathSegment>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum PathSegment {
34 Field(FieldName),
36 Array {
38 name: FieldName,
40 index: usize,
42 },
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct FieldName(Cow<'static, str>);
51
52impl SourcePath {
53 pub fn new() -> Self {
55 Default::default()
56 }
57
58 pub fn join(&self, segment: PathSegment) -> Self {
60 let mut new = self.clone();
61 new.segments.push(segment);
62 new
63 }
64
65 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 pub fn field(name: FieldName) -> Self {
107 Self::Field(name)
108 }
109
110 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 #[doc(hidden)]
155 pub const fn new_unchecked(name: &'static str) -> Self {
156 Self(Cow::Borrowed(name))
157 }
158
159 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}