voa_core/identifiers/
base.rs

1//! Base types used in other identifiers.
2
3use std::{
4    fmt::Display,
5    ops::Deref,
6    path::{MAIN_SEPARATOR, Path, PathBuf},
7    str::FromStr,
8};
9
10use winnow::{
11    ModalResult,
12    Parser,
13    combinator::{cut_err, eof, repeat},
14    error::StrContext,
15    token::one_of,
16};
17
18#[cfg(doc)]
19use crate::identifiers::{
20    Context,
21    CustomContext,
22    CustomRole,
23    CustomTechnology,
24    Os,
25    Purpose,
26    Technology,
27};
28use crate::{Error, iter_char_context};
29
30/// The path representation of a segment.
31///
32/// Segments represent the [`Os`], [`Purpose`], [`Context`] or [`Technology`] in the path for
33/// a verifier.
34///
35/// # Note
36///
37/// A segment path is guaranteed to be a relative path, that does not contain a path
38/// separator character.
39#[derive(Debug)]
40pub(crate) struct SegmentPath(PathBuf);
41
42impl SegmentPath {
43    /// Creates a new [`SegmentPath`] from a [`PathBuf`].
44    pub fn new(path: PathBuf) -> Result<Self, Error> {
45        if path.is_absolute() {
46            return Err(Error::InvalidSegmentPath {
47                path,
48                context: "it is absolute".to_string(),
49            });
50        }
51        if path.to_string_lossy().contains(MAIN_SEPARATOR) {
52            return Err(Error::InvalidSegmentPath {
53                path,
54                context: format!("it contains the path separator {MAIN_SEPARATOR} character"),
55            });
56        }
57
58        Ok(Self(path))
59    }
60}
61
62impl AsRef<Path> for SegmentPath {
63    fn as_ref(&self) -> &Path {
64        &self.0
65    }
66}
67
68impl TryFrom<String> for SegmentPath {
69    type Error = Error;
70
71    fn try_from(s: String) -> Result<Self, Self::Error> {
72        Self::new(PathBuf::from(s))
73    }
74}
75
76impl FromStr for SegmentPath {
77    type Err = Error;
78
79    fn from_str(s: &str) -> Result<Self, Self::Err> {
80        Self::new(PathBuf::from(s))
81    }
82}
83
84/// A string that represents a valid VOA identifier.
85///
86/// An [`IdentifierString`] is used e.g. in the components of [`Os`], [`CustomContext`],
87/// [`CustomRole`] or [`CustomTechnology`].
88/// It may only contain characters in the set of lowercase, alphanumeric ASCII characters, or
89/// the special characters `_`, `-` or `.`.
90#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
91#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
92pub struct IdentifierString(String);
93
94impl IdentifierString {
95    /// The list of allowed characters outside of the set of lowercase, alphanumeric ASCII
96    /// characters.
97    pub const SPECIAL_CHARS: &[char; 3] = &['_', '-', '.'];
98
99    /// A parser for characters valid in the context of an [`IdentifierString`].
100    ///
101    /// Consumes a single character from `input` and returns it.
102    /// The character in `input` must be in the set of lowercase, alphanumeric ASCII characters, or
103    /// one of the special characters [`IdentifierString::SPECIAL_CHARS`].
104    ///
105    /// # Errors
106    ///
107    /// Returns an error if a character in `input` is not in the set of lowercase, alphanumeric
108    /// ASCII characters, or one of the special characters [`IdentifierString::SPECIAL_CHARS`].
109    pub fn valid_chars(input: &mut &str) -> ModalResult<char> {
110        one_of((
111            |c: char| c.is_ascii_lowercase(),
112            |c: char| c.is_ascii_digit(),
113            Self::SPECIAL_CHARS,
114        ))
115        .context(StrContext::Expected(
116            winnow::error::StrContextValue::Description("lowercase alphanumeric ASCII characters"),
117        ))
118        .context_with(iter_char_context!(Self::SPECIAL_CHARS))
119        .parse_next(input)
120    }
121
122    /// Recognizes an [`IdentifierString`] in a string slice.
123    ///
124    /// Relies on [`winnow`] to parse `input` and recognizes a valid [`IdentifierString`].
125    /// All characters in `input` must be in the set of lowercase, alphanumeric ASCII characters, or
126    /// the special characters `_`, `-` or `.`.
127    ///
128    /// # Errors
129    ///
130    /// Returns an error if `input` contains characters that are outside of the set of lowercase,
131    /// alphanumeric ASCII characters or the special characters `_`, `-` or `.`.
132    ///
133    /// # Examples
134    ///
135    /// ```
136    /// use voa_core::identifiers::IdentifierString;
137    /// use winnow::Parser;
138    ///
139    /// # fn main() -> Result<(), voa_core::Error> {
140    /// let id_string = "foo-123";
141    /// assert_eq!(
142    ///     id_string,
143    ///     IdentifierString::parser.parse(id_string)?.to_string(),
144    /// );
145    /// # Ok(())
146    /// # }
147    /// ```
148    pub fn parser(input: &mut &str) -> ModalResult<Self> {
149        let id_string = repeat::<_, _, (), _, _>(1.., Self::valid_chars)
150            .take()
151            .context(StrContext::Label("VOA identifier string"))
152            .parse_next(input)?;
153
154        cut_err(eof)
155            .context(StrContext::Label("VOA identifier string"))
156            .context(StrContext::Expected(
157                winnow::error::StrContextValue::Description(
158                    "lowercase alphanumeric ASCII characters",
159                ),
160            ))
161            .context_with(iter_char_context!(Self::SPECIAL_CHARS))
162            .parse_next(input)?;
163
164        Ok(Self(id_string.to_string()))
165    }
166
167    /// Extracts a string slice containing the entire [`String`].
168    pub fn as_str(&self) -> &str {
169        &self.0
170    }
171}
172
173impl AsRef<str> for IdentifierString {
174    fn as_ref(&self) -> &str {
175        &self.0
176    }
177}
178
179impl Deref for IdentifierString {
180    type Target = str;
181    fn deref(&self) -> &Self::Target {
182        self.0.deref()
183    }
184}
185
186impl Display for IdentifierString {
187    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188        write!(f, "{}", self.0)
189    }
190}
191
192impl FromStr for IdentifierString {
193    type Err = crate::Error;
194
195    /// Creates an [`IdentifierString`] from a string slice.
196    ///
197    /// # Note
198    ///
199    /// Delegates to [`IdentifierString::parser`].
200    ///
201    /// # Errors
202    ///
203    /// Returns an error if [`IdentifierString::parser`] fails.
204    fn from_str(s: &str) -> Result<Self, Self::Err> {
205        Ok(Self::parser.parse(s)?)
206    }
207}
208
209#[cfg(test)]
210mod tests {
211
212    use rstest::rstest;
213    use testresult::TestResult;
214
215    use super::*;
216
217    #[rstest]
218    #[case::absolute_path("/example")]
219    #[case::path_contains_path_separator("example/foo")]
220    fn segment_path_from_str_fails(#[case] input: &str) -> TestResult {
221        match SegmentPath::from_str(input) {
222            Err(Error::InvalidSegmentPath { .. }) => {}
223            Err(error) => panic!(
224                "Expected to fail with an Error::InvalidSegmentPath, but failed with a different error instead: {error}"
225            ),
226            Ok(path) => panic!(
227                "Expected to fail with an Error::InvalidSegmentPath, but succeeded instead: {path:?}"
228            ),
229        }
230        match SegmentPath::try_from(input.to_string()) {
231            Err(Error::InvalidSegmentPath { .. }) => {}
232            Err(error) => panic!(
233                "Expected to fail with an Error::InvalidSegmentPath, but failed with a different error instead: {error}"
234            ),
235            Ok(path) => panic!(
236                "Expected to fail with an Error::InvalidSegmentPath, but succeeded instead: {path:?}"
237            ),
238        }
239
240        Ok(())
241    }
242
243    #[test]
244    fn segment_path_from_str_succeeds() -> TestResult {
245        let input = "example";
246        match SegmentPath::from_str(input) {
247            Ok(_) => {}
248            Err(error) => panic!("Expected to succeed, but failed instead: {error}"),
249        }
250        match SegmentPath::try_from(input.to_string()) {
251            Ok(_) => {}
252            Err(error) => panic!("Expected to succeed, but failed instead: {error}"),
253        }
254
255        Ok(())
256    }
257
258    #[rstest]
259    #[case::alpha("foo")]
260    #[case::alpha_numeric("foo123")]
261    #[case::alpha_numeric_special("foo-123")]
262    #[case::alpha_numeric_special("foo_123")]
263    #[case::alpha_numeric_special("foo.123")]
264    #[case::only_special_chars("._-")]
265    fn identifier_string_from_str_valid_chars(#[case] input: &str) -> TestResult {
266        match IdentifierString::from_str(input) {
267            Ok(id_string) => {
268                assert_eq!(id_string, IdentifierString(input.to_string()));
269                Ok(())
270            }
271            Err(error) => {
272                panic!("Should have succeeded to parse {input} but failed: {error}");
273            }
274        }
275    }
276
277    #[rstest]
278    #[case::empty_string("", "\n^")]
279    #[case::all_caps("FOO", "FOO\n^")]
280    #[case::one_caps("foO", "foO\n  ^")]
281    #[case::one_caps("foo:", "foo:\n   ^")]
282    #[case::one_caps("foö", "foö\n  ^")]
283    fn identifier_string_from_str_invalid_chars(
284        #[case] input: &str,
285        #[case] error_msg: &str,
286    ) -> TestResult {
287        match IdentifierString::from_str(input) {
288            Ok(id_string) => {
289                panic!("Should have failed to parse {input} but succeeded: {id_string}");
290            }
291            Err(error) => {
292                assert_eq!(
293                    error.to_string(),
294                    format!(
295                        "Parser error:\n{error_msg}\ninvalid VOA identifier string\nexpected lowercase alphanumeric ASCII characters, `_`, `-`, `.`"
296                    )
297                );
298                Ok(())
299            }
300        }
301    }
302}