1use std::cmp::Ordering;
4use std::fmt;
5use std::ops::Deref;
6use std::str::FromStr;
7
8use crate::constants::MAX_URI_LENGTH;
9use crate::error::FragmentError;
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
26pub struct Fragment(String);
27
28impl Fragment {
29 pub fn parse(input: &str) -> Result<Self, FragmentError> {
38 if input.is_empty() {
39 return Err(FragmentError::Empty);
40 }
41
42 if input.len() > MAX_URI_LENGTH {
43 return Err(FragmentError::TooLong {
44 max: MAX_URI_LENGTH,
45 actual: input.len(),
46 });
47 }
48
49 for (i, c) in input.chars().enumerate() {
50 if !Self::is_valid_char(c) {
51 return Err(FragmentError::InvalidChar {
52 char: c,
53 position: i,
54 });
55 }
56 }
57 Ok(Self(input.to_string()))
58 }
59
60 #[must_use]
62 pub fn as_str(&self) -> &str {
63 &self.0
64 }
65
66 #[must_use]
68 pub const fn is_valid_char(c: char) -> bool {
69 c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '/' | ':')
70 }
71}
72
73impl fmt::Display for Fragment {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 write!(f, "{}", self.0)
76 }
77}
78
79impl FromStr for Fragment {
80 type Err = FragmentError;
81
82 fn from_str(s: &str) -> Result<Self, Self::Err> {
83 Self::parse(s)
84 }
85}
86
87impl AsRef<str> for Fragment {
88 fn as_ref(&self) -> &str {
89 &self.0
90 }
91}
92
93impl TryFrom<&str> for Fragment {
94 type Error = FragmentError;
95
96 fn try_from(s: &str) -> Result<Self, Self::Error> {
97 Self::parse(s)
98 }
99}
100
101impl Deref for Fragment {
102 type Target = str;
103
104 fn deref(&self) -> &Self::Target {
105 &self.0
106 }
107}
108
109impl PartialOrd for Fragment {
110 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
111 Some(self.cmp(other))
112 }
113}
114
115impl Ord for Fragment {
116 fn cmp(&self, other: &Self) -> Ordering {
117 self.0.cmp(&other.0)
118 }
119}
120
121#[cfg(feature = "serde")]
122impl serde::Serialize for Fragment {
123 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
124 where
125 S: serde::Serializer,
126 {
127 serializer.serialize_str(&self.0)
128 }
129}
130
131#[cfg(feature = "serde")]
132impl<'de> serde::Deserialize<'de> for Fragment {
133 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
134 where
135 D: serde::Deserializer<'de>,
136 {
137 let s = String::deserialize(deserializer)?;
138 Self::parse(&s).map_err(serde::de::Error::custom)
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn parse_simple_fragment() {
148 let frag = Fragment::parse("summarization").unwrap();
149 assert_eq!(frag.as_str(), "summarization");
150 }
151
152 #[test]
153 fn parse_fragment_with_special_chars() {
154 let frag = Fragment::parse("sub/path:v2").unwrap();
155 assert_eq!(frag.as_str(), "sub/path:v2");
156 }
157
158 #[test]
159 fn parse_fragment_with_hyphen() {
160 let frag = Fragment::parse("sub-capability").unwrap();
161 assert_eq!(frag.as_str(), "sub-capability");
162 }
163
164 #[test]
165 fn parse_fragment_with_underscore() {
166 let frag = Fragment::parse("sub_capability").unwrap();
167 assert_eq!(frag.as_str(), "sub_capability");
168 }
169
170 #[test]
171 fn parse_fragment_with_dot() {
172 let frag = Fragment::parse("v1.0").unwrap();
173 assert_eq!(frag.as_str(), "v1.0");
174 }
175
176 #[test]
177 fn parse_empty_fragment_is_rejected() {
178 assert!(matches!(Fragment::parse(""), Err(FragmentError::Empty)));
179 }
180
181 #[test]
182 fn from_str_empty_fragment_is_rejected() {
183 assert!(matches!("".parse::<Fragment>(), Err(FragmentError::Empty)));
184 }
185
186 #[test]
187 fn try_from_empty_fragment_is_rejected() {
188 assert!(matches!(Fragment::try_from(""), Err(FragmentError::Empty)));
189 }
190
191 #[test]
192 fn fragment_at_maximum_length_parses() {
193 let input = "a".repeat(MAX_URI_LENGTH);
194
195 let fragment = Fragment::parse(&input).unwrap();
196
197 assert_eq!(fragment.as_str().len(), MAX_URI_LENGTH);
198 }
199
200 #[test]
201 fn fragment_over_maximum_length_reports_limit_and_actual_length() {
202 let input = "a".repeat(MAX_URI_LENGTH + 1);
203
204 let error = Fragment::parse(&input).unwrap_err();
205 let FragmentError::TooLong { max, actual } = error else {
206 panic!("expected FragmentError::TooLong");
207 };
208
209 assert_eq!(max, MAX_URI_LENGTH);
210 assert_eq!(actual, input.len());
211 }
212
213 #[test]
214 fn fragment_length_cap_precedes_character_validation() {
215 let input = format!("@{}", "a".repeat(MAX_URI_LENGTH));
216
217 assert!(matches!(
218 Fragment::parse(&input),
219 Err(FragmentError::TooLong {
220 max: MAX_URI_LENGTH,
221 actual
222 }) if actual == input.len()
223 ));
224 }
225
226 #[test]
227 fn parse_invalid_char_fails() {
228 let result = Fragment::parse("test@value");
229 assert!(matches!(
230 result,
231 Err(FragmentError::InvalidChar {
232 char: '@',
233 position: 4
234 })
235 ));
236 }
237
238 #[test]
239 fn parse_space_fails() {
240 let result = Fragment::parse("test value");
241 assert!(matches!(
242 result,
243 Err(FragmentError::InvalidChar {
244 char: ' ',
245 position: 4
246 })
247 ));
248 }
249}