use std::cmp::Ordering;
use std::fmt;
use std::ops::Deref;
use std::str::FromStr;
use crate::constants::MAX_PATH_SEGMENT_LENGTH;
use crate::error::PathSegmentError;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PathSegment(String);
impl PathSegment {
pub fn parse(input: &str) -> Result<Self, PathSegmentError> {
if input.is_empty() {
return Err(PathSegmentError::Empty);
}
if input.len() > MAX_PATH_SEGMENT_LENGTH {
return Err(PathSegmentError::TooLong {
max: MAX_PATH_SEGMENT_LENGTH,
actual: input.len(),
});
}
for (i, c) in input.chars().enumerate() {
if !Self::is_valid_char(c) {
return Err(PathSegmentError::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_lowercase() || c.is_ascii_digit() || c == '-'
}
}
impl fmt::Display for PathSegment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for PathSegment {
type Err = PathSegmentError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl AsRef<str> for PathSegment {
fn as_ref(&self) -> &str {
&self.0
}
}
impl TryFrom<&str> for PathSegment {
type Error = PathSegmentError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Self::parse(s)
}
}
impl Deref for PathSegment {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl PartialOrd for PathSegment {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PathSegment {
fn cmp(&self, other: &Self) -> Ordering {
self.0.cmp(&other.0)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for PathSegment {
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 PathSegment {
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_valid_segment() {
let seg = PathSegment::parse("chat").unwrap();
assert_eq!(seg.as_str(), "chat");
}
#[test]
fn parse_segment_with_hyphen() {
let seg = PathSegment::parse("code-interpreter").unwrap();
assert_eq!(seg.as_str(), "code-interpreter");
}
#[test]
fn parse_segment_with_digits() {
let seg = PathSegment::parse("v2").unwrap();
assert_eq!(seg.as_str(), "v2");
}
#[test]
fn parse_empty_fails() {
let result = PathSegment::parse("");
assert!(matches!(result, Err(PathSegmentError::Empty)));
}
#[test]
fn parse_too_long_fails() {
let long = "a".repeat(65);
let result = PathSegment::parse(&long);
assert!(matches!(result, Err(PathSegmentError::TooLong { max: 64, actual: 65 })));
}
#[test]
fn parse_uppercase_fails() {
let result = PathSegment::parse("Chat");
assert!(matches!(result, Err(PathSegmentError::InvalidChar { char: 'C', position: 0 })));
}
#[test]
fn parse_underscore_fails() {
let result = PathSegment::parse("code_interpreter");
assert!(matches!(result, Err(PathSegmentError::InvalidChar { char: '_', position: 4 })));
}
}