use std::{
fmt::{Display, Formatter},
str::FromStr,
};
use strum::IntoStaticStr;
use winnow::{
ModalResult,
Parser,
combinator::{alt, eof},
error::StrContext,
};
#[cfg(doc)]
use crate::identifiers::{Os, Purpose};
use crate::{
Error,
identifiers::{IdentifierString, SegmentPath},
};
#[derive(
Clone, Debug, Default, strum::Display, Eq, Hash, IntoStaticStr, Ord, PartialEq, PartialOrd,
)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum Context {
#[default]
#[strum(to_string = "default")]
#[cfg_attr(feature = "serde", serde(rename = "default"))]
Default,
#[strum(to_string = "{0}")]
#[cfg_attr(feature = "serde", serde(rename = "custom"))]
Custom(CustomContext),
}
impl Context {
pub(crate) fn path_segment(&self) -> Result<SegmentPath, Error> {
match self {
Self::Default => SegmentPath::from_str("default"),
Self::Custom(custom) => SegmentPath::from_str(custom.as_ref()),
}
}
pub fn parser(input: &mut &str) -> ModalResult<Self> {
alt((
("default", eof).value(Self::Default),
CustomContext::parser.map(Self::Custom),
))
.parse_next(input)
}
}
impl FromStr for Context {
type Err = crate::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::parser.parse(s)?)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename = "kebab-case"))]
pub struct CustomContext(IdentifierString);
impl CustomContext {
pub fn new(value: IdentifierString) -> Self {
Self(value)
}
pub fn parser(input: &mut &str) -> ModalResult<Self> {
IdentifierString::parser
.map(Self)
.context(StrContext::Label("custom context for VOA"))
.parse_next(input)
}
}
impl Display for CustomContext {
fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
write!(fmt, "{}", self.0)
}
}
impl AsRef<str> for CustomContext {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl FromStr for CustomContext {
type Err = crate::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::parser.parse(s)?)
}
}
impl From<CustomContext> for Context {
fn from(val: CustomContext) -> Self {
Context::Custom(val)
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use testresult::TestResult;
use super::*;
#[rstest]
#[case(Context::Default, "default")]
#[case(Context::Custom(CustomContext::new("abc".parse()?)), "abc")]
fn context_display(#[case] context: Context, #[case] display: &str) -> TestResult {
assert_eq!(format!("{context}"), display);
Ok(())
}
#[rstest]
#[case::default("default", Context::Default)]
#[case::custom("test", Context::Custom(CustomContext::new("test".parse()?)))]
fn context_from_str_succeeds(#[case] input: &str, #[case] expected: Context) -> TestResult {
assert_eq!(Context::from_str(input)?, expected);
Ok(())
}
#[rstest]
#[case::invalid_character(
"test$",
"test$\n ^\ninvalid VOA identifier string\nexpected lowercase alphanumeric ASCII characters, `_`, `-`, `.`"
)]
#[case::all_caps(
"TEST",
"TEST\n^\ninvalid VOA identifier string\nexpected lowercase alphanumeric ASCII characters, `_`, `-`, `.`"
)]
#[case::empty_string(
"",
"\n^\ninvalid VOA identifier string\nexpected lowercase alphanumeric ASCII characters, `_`, `-`, `.`"
)]
fn context_from_str_fails(#[case] input: &str, #[case] error_msg: &str) -> TestResult {
match Context::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(())
}
}
}
}