use std::cmp::Ordering;
use std::fmt;
use std::ops::Deref;
use std::str::FromStr;
use crate::constants::MAX_URI_LENGTH;
use crate::error::FragmentError;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Fragment(String);
impl Fragment {
pub fn parse(input: &str) -> Result<Self, FragmentError> {
if input.is_empty() {
return Err(FragmentError::Empty);
}
if input.len() > MAX_URI_LENGTH {
return Err(FragmentError::TooLong {
max: MAX_URI_LENGTH,
actual: input.len(),
});
}
for (i, c) in input.chars().enumerate() {
if !Self::is_valid_char(c) {
return Err(FragmentError::InvalidChar {
char: c,
position: i,
});
}
}
Ok(Self(input.to_string()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub const fn is_valid_char(c: char) -> bool {
c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '/' | ':')
}
}
impl fmt::Display for Fragment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for Fragment {
type Err = FragmentError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl AsRef<str> for Fragment {
fn as_ref(&self) -> &str {
&self.0
}
}
impl TryFrom<&str> for Fragment {
type Error = FragmentError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Self::parse(s)
}
}
impl Deref for Fragment {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl PartialOrd for Fragment {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Fragment {
fn cmp(&self, other: &Self) -> Ordering {
self.0.cmp(&other.0)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for Fragment {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.0)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Fragment {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::parse(&s).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_simple_fragment() {
let frag = Fragment::parse("summarization").unwrap();
assert_eq!(frag.as_str(), "summarization");
}
#[test]
fn parse_fragment_with_special_chars() {
let frag = Fragment::parse("sub/path:v2").unwrap();
assert_eq!(frag.as_str(), "sub/path:v2");
}
#[test]
fn parse_fragment_with_hyphen() {
let frag = Fragment::parse("sub-capability").unwrap();
assert_eq!(frag.as_str(), "sub-capability");
}
#[test]
fn parse_fragment_with_underscore() {
let frag = Fragment::parse("sub_capability").unwrap();
assert_eq!(frag.as_str(), "sub_capability");
}
#[test]
fn parse_fragment_with_dot() {
let frag = Fragment::parse("v1.0").unwrap();
assert_eq!(frag.as_str(), "v1.0");
}
#[test]
fn parse_empty_fragment_is_rejected() {
assert!(matches!(Fragment::parse(""), Err(FragmentError::Empty)));
}
#[test]
fn from_str_empty_fragment_is_rejected() {
assert!(matches!("".parse::<Fragment>(), Err(FragmentError::Empty)));
}
#[test]
fn try_from_empty_fragment_is_rejected() {
assert!(matches!(Fragment::try_from(""), Err(FragmentError::Empty)));
}
#[test]
fn fragment_at_maximum_length_parses() {
let input = "a".repeat(MAX_URI_LENGTH);
let fragment = Fragment::parse(&input).unwrap();
assert_eq!(fragment.as_str().len(), MAX_URI_LENGTH);
}
#[test]
fn fragment_over_maximum_length_reports_limit_and_actual_length() {
let input = "a".repeat(MAX_URI_LENGTH + 1);
let error = Fragment::parse(&input).unwrap_err();
let FragmentError::TooLong { max, actual } = error else {
panic!("expected FragmentError::TooLong");
};
assert_eq!(max, MAX_URI_LENGTH);
assert_eq!(actual, input.len());
}
#[test]
fn fragment_length_cap_precedes_character_validation() {
let input = format!("@{}", "a".repeat(MAX_URI_LENGTH));
assert!(matches!(
Fragment::parse(&input),
Err(FragmentError::TooLong {
max: MAX_URI_LENGTH,
actual
}) if actual == input.len()
));
}
#[test]
fn parse_invalid_char_fails() {
let result = Fragment::parse("test@value");
assert!(matches!(
result,
Err(FragmentError::InvalidChar {
char: '@',
position: 4
})
));
}
#[test]
fn parse_space_fails() {
let result = Fragment::parse("test value");
assert!(matches!(
result,
Err(FragmentError::InvalidChar {
char: ' ',
position: 4
})
));
}
}