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},
};
#[derive(Clone, Debug, strum::Display, Eq, Hash, IntoStaticStr, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum Technology {
#[strum(to_string = "openpgp")]
#[cfg_attr(feature = "serde", serde(rename = "openpgp"))]
Openpgp,
#[strum(to_string = "ssh")]
#[cfg_attr(feature = "serde", serde(rename = "ssh"))]
SSH,
#[strum(to_string = "{0}")]
Custom(CustomTechnology),
}
impl Technology {
pub(crate) fn path_segment(&self) -> Result<SegmentPath, Error> {
format!("{self}").try_into()
}
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;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::parser.parse(s)?)
}
}
#[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 {
pub fn new(value: IdentifierString) -> Self {
Self(value)
}
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;
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(())
}
}
}
}