voa-core 0.4.1

File Hierarchy for the Verification of OS Artifacts (VOA)
Documentation
//! Base types used in other identifiers.

use std::{
    fmt::Display,
    ops::Deref,
    path::{MAIN_SEPARATOR, Path, PathBuf},
    str::FromStr,
};

use winnow::{
    ModalResult,
    Parser,
    combinator::{cut_err, eof, repeat},
    error::StrContext,
    token::one_of,
};

#[cfg(doc)]
use crate::identifiers::{
    Context,
    CustomContext,
    CustomRole,
    CustomTechnology,
    Os,
    Purpose,
    Technology,
};
use crate::{Error, iter_char_context};

/// The path representation of a segment.
///
/// Segments represent the [`Os`], [`Purpose`], [`Context`] or [`Technology`] in the path for
/// a verifier.
///
/// # Note
///
/// A segment path is guaranteed to be a relative path, that does not contain a path
/// separator character.
#[derive(Debug)]
pub(crate) struct SegmentPath(PathBuf);

impl SegmentPath {
    /// Creates a new [`SegmentPath`] from a [`PathBuf`].
    pub fn new(path: PathBuf) -> Result<Self, Error> {
        if path.is_absolute() {
            return Err(Error::InvalidSegmentPath {
                path,
                context: "it is absolute".to_string(),
            });
        }
        if path.to_string_lossy().contains(MAIN_SEPARATOR) {
            return Err(Error::InvalidSegmentPath {
                path,
                context: format!("it contains the path separator {MAIN_SEPARATOR} character"),
            });
        }

        Ok(Self(path))
    }
}

impl AsRef<Path> for SegmentPath {
    fn as_ref(&self) -> &Path {
        &self.0
    }
}

impl TryFrom<String> for SegmentPath {
    type Error = Error;

    fn try_from(s: String) -> Result<Self, Self::Error> {
        Self::new(PathBuf::from(s))
    }
}

impl FromStr for SegmentPath {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::new(PathBuf::from(s))
    }
}

/// A string that represents a valid VOA identifier.
///
/// An [`IdentifierString`] is used e.g. in the components of [`Os`], [`CustomContext`],
/// [`CustomRole`] or [`CustomTechnology`].
/// It may only contain characters in the set of lowercase, alphanumeric ASCII characters, or
/// the special characters `_`, `-` or `.`.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct IdentifierString(String);

impl IdentifierString {
    /// The list of allowed characters outside of the set of lowercase, alphanumeric ASCII
    /// characters.
    pub const SPECIAL_CHARS: &[char; 3] = &['_', '-', '.'];

    /// A parser for characters valid in the context of an [`IdentifierString`].
    ///
    /// Consumes a single character from `input` and returns it.
    /// The character in `input` must be in the set of lowercase, alphanumeric ASCII characters, or
    /// one of the special characters [`IdentifierString::SPECIAL_CHARS`].
    ///
    /// # Errors
    ///
    /// Returns an error if a character in `input` is not in the set of lowercase, alphanumeric
    /// ASCII characters, or one of the special characters [`IdentifierString::SPECIAL_CHARS`].
    pub fn valid_chars(input: &mut &str) -> ModalResult<char> {
        one_of((
            |c: char| c.is_ascii_lowercase(),
            |c: char| c.is_ascii_digit(),
            Self::SPECIAL_CHARS,
        ))
        .context(StrContext::Expected(
            winnow::error::StrContextValue::Description("lowercase alphanumeric ASCII characters"),
        ))
        .context_with(iter_char_context!(Self::SPECIAL_CHARS))
        .parse_next(input)
    }

    /// Recognizes an [`IdentifierString`] in a string slice.
    ///
    /// Relies on [`winnow`] to parse `input` and recognizes a valid [`IdentifierString`].
    /// All characters in `input` must be in the set of lowercase, alphanumeric ASCII characters, or
    /// the special characters `_`, `-` or `.`.
    ///
    /// # Errors
    ///
    /// Returns an error if `input` contains characters that are outside of the set of lowercase,
    /// alphanumeric ASCII characters or the special characters `_`, `-` or `.`.
    ///
    /// # Examples
    ///
    /// ```
    /// use voa_core::identifiers::IdentifierString;
    /// use winnow::Parser;
    ///
    /// # fn main() -> Result<(), voa_core::Error> {
    /// let id_string = "foo-123";
    /// assert_eq!(
    ///     id_string,
    ///     IdentifierString::parser.parse(id_string)?.to_string(),
    /// );
    /// # Ok(())
    /// # }
    /// ```
    pub fn parser(input: &mut &str) -> ModalResult<Self> {
        let id_string = repeat::<_, _, (), _, _>(1.., Self::valid_chars)
            .take()
            .context(StrContext::Label("VOA identifier string"))
            .parse_next(input)?;

        cut_err(eof)
            .context(StrContext::Label("VOA identifier string"))
            .context(StrContext::Expected(
                winnow::error::StrContextValue::Description(
                    "lowercase alphanumeric ASCII characters",
                ),
            ))
            .context_with(iter_char_context!(Self::SPECIAL_CHARS))
            .parse_next(input)?;

        Ok(Self(id_string.to_string()))
    }

    /// Extracts a string slice containing the entire [`String`].
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl AsRef<str> for IdentifierString {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

impl Deref for IdentifierString {
    type Target = str;
    fn deref(&self) -> &Self::Target {
        self.0.deref()
    }
}

impl Display for IdentifierString {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl FromStr for IdentifierString {
    type Err = crate::Error;

    /// Creates an [`IdentifierString`] from a string slice.
    ///
    /// # Note
    ///
    /// Delegates to [`IdentifierString::parser`].
    ///
    /// # Errors
    ///
    /// Returns an error if [`IdentifierString::parser`] fails.
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(Self::parser.parse(s)?)
    }
}

#[cfg(test)]
mod tests {

    use rstest::rstest;
    use testresult::TestResult;

    use super::*;

    #[rstest]
    #[case::absolute_path("/example")]
    #[case::path_contains_path_separator("example/foo")]
    fn segment_path_from_str_fails(#[case] input: &str) -> TestResult {
        match SegmentPath::from_str(input) {
            Err(Error::InvalidSegmentPath { .. }) => {}
            Err(error) => panic!(
                "Expected to fail with an Error::InvalidSegmentPath, but failed with a different error instead: {error}"
            ),
            Ok(path) => panic!(
                "Expected to fail with an Error::InvalidSegmentPath, but succeeded instead: {path:?}"
            ),
        }
        match SegmentPath::try_from(input.to_string()) {
            Err(Error::InvalidSegmentPath { .. }) => {}
            Err(error) => panic!(
                "Expected to fail with an Error::InvalidSegmentPath, but failed with a different error instead: {error}"
            ),
            Ok(path) => panic!(
                "Expected to fail with an Error::InvalidSegmentPath, but succeeded instead: {path:?}"
            ),
        }

        Ok(())
    }

    #[test]
    fn segment_path_from_str_succeeds() -> TestResult {
        let input = "example";
        match SegmentPath::from_str(input) {
            Ok(_) => {}
            Err(error) => panic!("Expected to succeed, but failed instead: {error}"),
        }
        match SegmentPath::try_from(input.to_string()) {
            Ok(_) => {}
            Err(error) => panic!("Expected to succeed, but failed instead: {error}"),
        }

        Ok(())
    }

    #[rstest]
    #[case::alpha("foo")]
    #[case::alpha_numeric("foo123")]
    #[case::alpha_numeric_special("foo-123")]
    #[case::alpha_numeric_special("foo_123")]
    #[case::alpha_numeric_special("foo.123")]
    #[case::only_special_chars("._-")]
    fn identifier_string_from_str_valid_chars(#[case] input: &str) -> TestResult {
        match IdentifierString::from_str(input) {
            Ok(id_string) => {
                assert_eq!(id_string, IdentifierString(input.to_string()));
                Ok(())
            }
            Err(error) => {
                panic!("Should have succeeded to parse {input} but failed: {error}");
            }
        }
    }

    #[rstest]
    #[case::empty_string("", "\n^")]
    #[case::all_caps("FOO", "FOO\n^")]
    #[case::one_caps("foO", "foO\n  ^")]
    #[case::one_caps("foo:", "foo:\n   ^")]
    #[case::one_caps("foö", "foö\n  ^")]
    fn identifier_string_from_str_invalid_chars(
        #[case] input: &str,
        #[case] error_msg: &str,
    ) -> TestResult {
        match IdentifierString::from_str(input) {
            Ok(id_string) => {
                panic!("Should have failed to parse {input} but succeeded: {id_string}");
            }
            Err(error) => {
                assert_eq!(
                    error.to_string(),
                    format!(
                        "Parser error:\n{error_msg}\ninvalid VOA identifier string\nexpected lowercase alphanumeric ASCII characters, `_`, `-`, `.`"
                    )
                );
                Ok(())
            }
        }
    }
}