voa-core 0.4.1

File Hierarchy for the Verification of OS Artifacts (VOA)
Documentation
use std::{
    fmt::{Display, Formatter},
    str::FromStr,
};

use strum::IntoStaticStr;
use winnow::{
    ModalResult,
    Parser,
    combinator::{alt, cut_err, eof},
    error::{StrContext, StrContextValue},
};

use crate::{
    Error,
    identifiers::{IdentifierString, SegmentPath},
};

/// The name of a technology backend.
///
/// Technology-specific backends implement the logic for each supported verification technology
/// in VOA.
///
/// See <https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/#technology>
#[derive(Clone, Debug, strum::Display, Eq, Hash, IntoStaticStr, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum Technology {
    /// The [OpenPGP] technology.
    ///
    /// [OpenPGP]: https://www.openpgp.org/
    #[strum(to_string = "openpgp")]
    #[cfg_attr(feature = "serde", serde(rename = "openpgp"))]
    Openpgp,

    /// The [SSH] technology.
    ///
    /// [SSH]: https://www.openssh.com/
    #[strum(to_string = "ssh")]
    #[cfg_attr(feature = "serde", serde(rename = "ssh"))]
    SSH,

    /// Defines a custom [`Technology`] name.
    #[strum(to_string = "{0}")]
    Custom(CustomTechnology),
}

impl Technology {
    /// Returns the path segment for this technology.
    pub(crate) fn path_segment(&self) -> Result<SegmentPath, Error> {
        format!("{self}").try_into()
    }

    /// Recognizes a [`Technology`] in a string slice.
    ///
    /// Consumes all of its `input`.
    ///
    /// # Errors
    ///
    /// Returns an error if
    ///
    /// - `input` does not contain a variant of [`Technology`],
    /// - or one of the characters in `input` is not covered by [`IdentifierString::valid_chars`].
    ///
    /// # Examples
    ///
    /// ```
    /// use voa_core::identifiers::Technology;
    /// use winnow::Parser;
    ///
    /// # fn main() -> Result<(), voa_core::Error> {
    /// assert_eq!(Technology::parser.parse("openpgp")?, Technology::Openpgp);
    /// assert_eq!(Technology::parser.parse("ssh")?, Technology::SSH);
    /// assert_eq!(Technology::parser.parse("test")?.to_string(), "test");
    /// # Ok(())
    /// # }
    /// ```
    pub fn parser(input: &mut &str) -> ModalResult<Self> {
        cut_err(alt((
            ("openpgp", eof).value(Self::Openpgp),
            ("ssh", eof).value(Self::SSH),
            CustomTechnology::parser.map(Self::Custom),
        )))
        .context(StrContext::Label("a valid VOA technology"))
        .context(StrContext::Expected(StrContextValue::Description(
            "'opengpg', 'ssh', or a custom value",
        )))
        .parse_next(input)
    }
}

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

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

/// A [`CustomTechnology`] defines a technology name that is not covered by the variants defined in
/// [`Technology`].
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
pub struct CustomTechnology(IdentifierString);

impl CustomTechnology {
    /// Creates a new [`CustomTechnology`] instance.
    pub fn new(value: IdentifierString) -> Self {
        Self(value)
    }

    /// Recognizes a [`CustomTechnology`] in a string slice.
    ///
    /// Consumes all of its `input`.
    ///
    /// # Errors
    ///
    /// Returns an error if one of the characters in `input` is not covered by
    /// [`IdentifierString::valid_chars`].
    ///
    /// # Examples
    ///
    /// ```
    /// use voa_core::identifiers::CustomTechnology;
    /// use winnow::Parser;
    ///
    /// # fn main() -> Result<(), voa_core::Error> {
    /// assert_eq!(CustomTechnology::parser.parse("test")?.to_string(), "test");
    /// # Ok(())
    /// # }
    /// ```
    pub fn parser(input: &mut &str) -> ModalResult<Self> {
        IdentifierString::parser
            .map(Self)
            .context(StrContext::Label("custom technology for VOA"))
            .parse_next(input)
    }
}

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

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

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

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

#[cfg(test)]
mod tests {
    use rstest::rstest;
    use testresult::TestResult;

    use super::*;

    #[rstest]
    #[case(Technology::Openpgp, "openpgp")]
    #[case(Technology::Custom(CustomTechnology::new("foo".parse()?)), "foo")]
    fn technology_display(
        #[case] technology: Technology,
        #[case] display: &str,
    ) -> testresult::TestResult {
        assert_eq!(format!("{technology}",), display);

        Ok(())
    }

    #[test]
    fn custom_as_ref() -> TestResult {
        let custom = CustomTechnology::new("foo".parse()?);
        assert_eq!(custom.as_ref(), "foo");

        Ok(())
    }

    #[rstest]
    #[case::default("openpgp", Technology::Openpgp)]
    #[case::default("ssh", Technology::SSH)]
    #[case::custom("test", Technology::Custom(CustomTechnology::new("test".parse()?)))]
    fn technology_from_str_succeeds(
        #[case] input: &str,
        #[case] expected: Technology,
    ) -> TestResult {
        assert_eq!(Technology::from_str(input)?, expected);
        Ok(())
    }

    #[rstest]
    #[case::invalid_character(
        "test$",
        "test$\n    ^\ninvalid VOA identifier string\nexpected lowercase alphanumeric ASCII characters, `_`, `-`, `.`, 'opengpg', 'ssh', or a custom value"
    )]
    #[case::all_caps(
        "TEST",
        "TEST\n^\ninvalid VOA identifier string\nexpected lowercase alphanumeric ASCII characters, `_`, `-`, `.`, 'opengpg', 'ssh', or a custom value"
    )]
    #[case::empty_string(
        "",
        "\n^\ninvalid VOA identifier string\nexpected lowercase alphanumeric ASCII characters, `_`, `-`, `.`, 'opengpg', 'ssh', or a custom value"
    )]
    fn technology_from_str_fails(#[case] input: &str, #[case] error_msg: &str) -> TestResult {
        match Technology::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}"));
                Ok(())
            }
        }
    }
}