1use std::cmp::Ordering;
9use std::fmt;
10use std::str::FromStr;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub struct Version {
16 pub major: u32,
17 pub minor: u32,
18 pub patch: u32,
19}
20
21impl Version {
22 pub fn new(major: u32, minor: u32, patch: u32) -> Self {
23 Self {
24 major,
25 minor,
26 patch,
27 }
28 }
29}
30
31impl FromStr for Version {
32 type Err = String;
33
34 fn from_str(s: &str) -> Result<Self, Self::Err> {
35 let parts: Vec<&str> = s.split('.').collect();
36
37 if !matches!(parts.len(), 2 | 3) {
38 return Err(format!(
39 "Invalid version format '{}': expected MAJOR.MINOR.PATCH or MAJOR.MINOR (e.g., '2.1.0' or '2.1')",
40 s
41 ));
42 }
43
44 let major = parts[0]
45 .parse::<u32>()
46 .map_err(|_| format!("Invalid major version '{}': must be a number", parts[0]))?;
47
48 let minor = parts[1]
49 .parse::<u32>()
50 .map_err(|_| format!("Invalid minor version '{}': must be a number", parts[1]))?;
51
52 let patch = if parts.len() == 3 {
53 parts[2]
54 .parse::<u32>()
55 .map_err(|_| format!("Invalid patch version '{}': must be a number", parts[2]))?
56 } else {
57 0
58 };
59
60 Ok(Version {
61 major,
62 minor,
63 patch,
64 })
65 }
66}
67
68impl fmt::Display for Version {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
71 }
72}
73
74impl PartialOrd for Version {
75 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
76 Some(self.cmp(other))
77 }
78}
79
80impl Ord for Version {
81 fn cmp(&self, other: &Self) -> Ordering {
82 match self.major.cmp(&other.major) {
83 Ordering::Equal => match self.minor.cmp(&other.minor) {
84 Ordering::Equal => self.patch.cmp(&other.patch),
85 other => other,
86 },
87 other => other,
88 }
89 }
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Hash)]
94pub enum VersionSelector {
95 Exact(Version),
97 Minor(u32, u32),
99 Major(u32),
101 Latest,
103}
104
105impl FromStr for VersionSelector {
106 type Err = String;
107
108 fn from_str(s: &str) -> Result<Self, Self::Err> {
109 let version_str = s.strip_prefix('@').unwrap_or(s);
110
111 if version_str.is_empty() || version_str == "latest" {
112 return Ok(VersionSelector::Latest);
113 }
114
115 let parts: Vec<&str> = version_str.split('.').collect();
116
117 match parts.len() {
118 3 => {
119 let version = Version::from_str(version_str)?;
120 Ok(VersionSelector::Exact(version))
121 }
122 2 => {
123 let major = parts[0].parse::<u32>().map_err(|_| {
124 format!("Invalid major version '{}': must be a number", parts[0])
125 })?;
126 let minor = parts[1].parse::<u32>().map_err(|_| {
127 format!("Invalid minor version '{}': must be a number", parts[1])
128 })?;
129 Ok(VersionSelector::Minor(major, minor))
130 }
131 1 => {
132 let major = version_str.parse::<u32>().map_err(|_| {
133 format!(
134 "Invalid version selector '{}': expected number, MAJOR.MINOR, MAJOR.MINOR.PATCH, or 'latest'",
135 version_str
136 )
137 })?;
138 Ok(VersionSelector::Major(major))
139 }
140 _ => Err(format!(
141 "Invalid version selector '{}': expected number, MAJOR.MINOR, MAJOR.MINOR.PATCH, or 'latest'",
142 version_str
143 )),
144 }
145 }
146}
147
148impl fmt::Display for VersionSelector {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 match self {
151 VersionSelector::Exact(v) => write!(f, "@{}", v),
152 VersionSelector::Minor(major, minor) => write!(f, "@{}.{}", major, minor),
153 VersionSelector::Major(m) => write!(f, "@{}", m),
154 VersionSelector::Latest => write!(f, "@latest"),
155 }
156 }
157}
158
159#[derive(Debug, Clone, PartialEq, Eq, Hash)]
163pub struct QuillReference {
164 pub name: String,
165 pub selector: VersionSelector,
166}
167
168impl QuillReference {
169 pub fn new(name: String, selector: VersionSelector) -> Self {
170 Self { name, selector }
171 }
172
173 pub fn latest(name: String) -> Self {
174 Self {
175 name,
176 selector: VersionSelector::Latest,
177 }
178 }
179}
180
181impl FromStr for QuillReference {
182 type Err = String;
183
184 fn from_str(s: &str) -> Result<Self, Self::Err> {
185 let separator_idx = s.find('@');
186
187 let (name_part, version_part_opt) = match separator_idx {
188 Some(idx) => (&s[..idx], Some(&s[idx + 1..])),
189 None => (s, None),
190 };
191
192 if name_part.is_empty() {
193 return Err("Quill name cannot be empty".to_string());
194 }
195
196 let name = name_part.to_string();
197
198 if !name
199 .chars()
200 .next()
201 .is_some_and(|c| c.is_ascii_lowercase() || c == '_')
202 {
203 return Err(format!(
204 "Invalid Quill name '{}': must start with lowercase letter or underscore",
205 name
206 ));
207 }
208 if !name
209 .chars()
210 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
211 {
212 return Err(format!(
213 "Invalid Quill name '{}': must contain only lowercase letters, digits, and underscores",
214 name
215 ));
216 }
217
218 let selector = if let Some(version_part) = version_part_opt {
219 VersionSelector::from_str(&format!("@{}", version_part))?
220 } else {
221 VersionSelector::Latest
222 };
223
224 Ok(QuillReference { name, selector })
225 }
226}
227
228impl fmt::Display for QuillReference {
229 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230 match &self.selector {
231 VersionSelector::Latest => write!(f, "{}", self.name),
232 _ => write!(f, "{}{}", self.name, self.selector),
233 }
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn test_version_parsing() {
243 let v = Version::from_str("2.1.0").unwrap();
244 assert_eq!(v.major, 2);
245 assert_eq!(v.minor, 1);
246 assert_eq!(v.patch, 0);
247 assert_eq!(v.to_string(), "2.1.0");
248
249 let v2 = Version::from_str("1.2.3").unwrap();
250 assert_eq!(v2.major, 1);
251 assert_eq!(v2.minor, 2);
252 assert_eq!(v2.patch, 3);
253 assert_eq!(v2.to_string(), "1.2.3");
254 }
255
256 #[test]
257 fn test_version_parsing_two_segment_backward_compat() {
258 let v = Version::from_str("2.1").unwrap();
259 assert_eq!(v.major, 2);
260 assert_eq!(v.minor, 1);
261 assert_eq!(v.patch, 0);
262 assert_eq!(v.to_string(), "2.1.0");
263 }
264
265 #[test]
266 fn test_version_invalid() {
267 assert!(Version::from_str("2").is_err());
268 assert!(Version::from_str("2.1.0.0").is_err());
269 assert!(Version::from_str("abc").is_err());
270 assert!(Version::from_str("2.x").is_err());
271 assert!(Version::from_str("2.1.x").is_err());
272 }
273
274 #[test]
275 fn test_version_ordering() {
276 let v1_0_0 = Version::new(1, 0, 0);
277 let v1_0_1 = Version::new(1, 0, 1);
278 let v1_1_0 = Version::new(1, 1, 0);
279 let v2_0_0 = Version::new(2, 0, 0);
280 let v2_1_0 = Version::new(2, 1, 0);
281
282 assert!(v1_0_0 < v1_0_1);
283 assert!(v1_0_1 < v1_1_0);
284 assert!(v1_1_0 < v2_0_0);
285 assert!(v2_0_0 < v2_1_0);
286 assert_eq!(v1_0_0, v1_0_0);
287 }
288
289 #[test]
290 fn test_version_selector_parsing() {
291 let exact = VersionSelector::from_str("@2.1.0").unwrap();
292 assert_eq!(exact, VersionSelector::Exact(Version::new(2, 1, 0)));
293
294 let minor = VersionSelector::from_str("@2.1").unwrap();
295 assert_eq!(minor, VersionSelector::Minor(2, 1));
296
297 let major = VersionSelector::from_str("@2").unwrap();
298 assert_eq!(major, VersionSelector::Major(2));
299
300 let latest1 = VersionSelector::from_str("@latest").unwrap();
301 assert_eq!(latest1, VersionSelector::Latest);
302
303 let latest2 = VersionSelector::from_str("").unwrap();
305 assert_eq!(latest2, VersionSelector::Latest);
306 }
307
308 #[test]
309 fn test_version_selector_without_at() {
310 let exact = VersionSelector::from_str("2.1.0").unwrap();
311 assert_eq!(exact, VersionSelector::Exact(Version::new(2, 1, 0)));
312
313 let minor = VersionSelector::from_str("2.1").unwrap();
314 assert_eq!(minor, VersionSelector::Minor(2, 1));
315
316 let major = VersionSelector::from_str("2").unwrap();
317 assert_eq!(major, VersionSelector::Major(2));
318 }
319
320 #[test]
321 fn test_version_selector_display() {
322 assert_eq!(
323 VersionSelector::Exact(Version::new(2, 1, 0)).to_string(),
324 "@2.1.0"
325 );
326 assert_eq!(VersionSelector::Minor(2, 1).to_string(), "@2.1");
327 assert_eq!(VersionSelector::Major(2).to_string(), "@2");
328 assert_eq!(VersionSelector::Latest.to_string(), "@latest");
329 }
330
331 #[test]
332 fn test_quill_reference_parsing() {
333 let ref1 = QuillReference::from_str("resume_template@2.1.0").unwrap();
334 assert_eq!(ref1.name, "resume_template");
335 assert_eq!(ref1.selector, VersionSelector::Exact(Version::new(2, 1, 0)));
336
337 let ref1b = QuillReference::from_str("resume_template@2.1").unwrap();
338 assert_eq!(ref1b.selector, VersionSelector::Minor(2, 1));
339
340 let ref2 = QuillReference::from_str("resume_template@2").unwrap();
341 assert_eq!(ref2.selector, VersionSelector::Major(2));
342
343 let ref3 = QuillReference::from_str("resume_template@latest").unwrap();
344 assert_eq!(ref3.selector, VersionSelector::Latest);
345
346 let ref4 = QuillReference::from_str("resume_template").unwrap();
348 assert_eq!(ref4.name, "resume_template");
349 assert_eq!(ref4.selector, VersionSelector::Latest);
350 }
351
352 #[test]
353 fn test_quill_reference_invalid_names() {
354 assert!(QuillReference::from_str("Resume@2.1.0").is_err());
355 assert!(QuillReference::from_str("1resume@2.1.0").is_err());
356 assert!(QuillReference::from_str("resume-template@2.1.0").is_err());
357 assert!(QuillReference::from_str("resume.template@2.1.0").is_err());
358 assert!(QuillReference::from_str("resume_template@2.1.0").is_ok());
359 assert!(QuillReference::from_str("_private@2.1.0").is_ok());
360 assert!(QuillReference::from_str("template2@2.1.0").is_ok());
361 }
362
363 #[test]
364 fn test_quill_reference_display() {
365 let ref1 = QuillReference::new(
366 "resume".to_string(),
367 VersionSelector::Exact(Version::new(2, 1, 0)),
368 );
369 assert_eq!(ref1.to_string(), "resume@2.1.0");
370
371 let ref1b = QuillReference::new("resume".to_string(), VersionSelector::Minor(2, 1));
372 assert_eq!(ref1b.to_string(), "resume@2.1");
373
374 let ref2 = QuillReference::new("resume".to_string(), VersionSelector::Major(2));
375 assert_eq!(ref2.to_string(), "resume@2");
376
377 let ref3 = QuillReference::new("resume".to_string(), VersionSelector::Latest);
378 assert_eq!(ref3.to_string(), "resume");
379 }
380}