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};
#[derive(Debug)]
pub(crate) struct SegmentPath(PathBuf);
impl SegmentPath {
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))
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct IdentifierString(String);
impl IdentifierString {
pub const SPECIAL_CHARS: &[char; 3] = &['_', '-', '.'];
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)
}
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()))
}
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;
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(())
}
}
}
}