1use std::cmp::Ordering;
4use std::fmt;
5use std::ops::Deref;
6use std::str::FromStr;
7
8use crate::error::FragmentError;
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23pub struct Fragment(String);
24
25impl Fragment {
26 pub fn parse(input: &str) -> Result<Self, FragmentError> {
32 for (i, c) in input.chars().enumerate() {
33 if !Self::is_valid_char(c) {
34 return Err(FragmentError::InvalidChar { char: c, position: i });
35 }
36 }
37 Ok(Self(input.to_string()))
38 }
39
40 #[must_use]
42 pub fn as_str(&self) -> &str {
43 &self.0
44 }
45
46 #[must_use]
48 pub const fn is_valid_char(c: char) -> bool {
49 c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '/' | ':')
50 }
51}
52
53impl fmt::Display for Fragment {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 write!(f, "{}", self.0)
56 }
57}
58
59impl FromStr for Fragment {
60 type Err = FragmentError;
61
62 fn from_str(s: &str) -> Result<Self, Self::Err> {
63 Self::parse(s)
64 }
65}
66
67impl AsRef<str> for Fragment {
68 fn as_ref(&self) -> &str {
69 &self.0
70 }
71}
72
73impl TryFrom<&str> for Fragment {
74 type Error = FragmentError;
75
76 fn try_from(s: &str) -> Result<Self, Self::Error> {
77 Self::parse(s)
78 }
79}
80
81impl Deref for Fragment {
82 type Target = str;
83
84 fn deref(&self) -> &Self::Target {
85 &self.0
86 }
87}
88
89impl PartialOrd for Fragment {
90 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
91 Some(self.cmp(other))
92 }
93}
94
95impl Ord for Fragment {
96 fn cmp(&self, other: &Self) -> Ordering {
97 self.0.cmp(&other.0)
98 }
99}
100
101#[cfg(feature = "serde")]
102impl serde::Serialize for Fragment {
103 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
104 where
105 S: serde::Serializer,
106 {
107 serializer.serialize_str(&self.0)
108 }
109}
110
111#[cfg(feature = "serde")]
112impl<'de> serde::Deserialize<'de> for Fragment {
113 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
114 where
115 D: serde::Deserializer<'de>,
116 {
117 let s = String::deserialize(deserializer)?;
118 Self::parse(&s).map_err(serde::de::Error::custom)
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn parse_simple_fragment() {
128 let frag = Fragment::parse("summarization").unwrap();
129 assert_eq!(frag.as_str(), "summarization");
130 }
131
132 #[test]
133 fn parse_fragment_with_special_chars() {
134 let frag = Fragment::parse("sub/path:v2").unwrap();
135 assert_eq!(frag.as_str(), "sub/path:v2");
136 }
137
138 #[test]
139 fn parse_fragment_with_hyphen() {
140 let frag = Fragment::parse("sub-capability").unwrap();
141 assert_eq!(frag.as_str(), "sub-capability");
142 }
143
144 #[test]
145 fn parse_fragment_with_underscore() {
146 let frag = Fragment::parse("sub_capability").unwrap();
147 assert_eq!(frag.as_str(), "sub_capability");
148 }
149
150 #[test]
151 fn parse_fragment_with_dot() {
152 let frag = Fragment::parse("v1.0").unwrap();
153 assert_eq!(frag.as_str(), "v1.0");
154 }
155
156 #[test]
157 fn parse_empty_fragment() {
158 let frag = Fragment::parse("").unwrap();
160 assert_eq!(frag.as_str(), "");
161 }
162
163 #[test]
164 fn parse_invalid_char_fails() {
165 let result = Fragment::parse("test@value");
166 assert!(matches!(result, Err(FragmentError::InvalidChar { char: '@', position: 4 })));
167 }
168
169 #[test]
170 fn parse_space_fails() {
171 let result = Fragment::parse("test value");
172 assert!(matches!(result, Err(FragmentError::InvalidChar { char: ' ', position: 4 })));
173 }
174}