1use std::cmp::Ordering;
62use std::fmt;
63use std::str::FromStr;
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
89pub struct Version {
90 pub major: u32,
92 pub minor: u32,
94 pub patch: u32,
96}
97
98impl Version {
99 pub fn new(major: u32, minor: u32, patch: u32) -> Self {
101 Self {
102 major,
103 minor,
104 patch,
105 }
106 }
107}
108
109impl FromStr for Version {
110 type Err = String;
111
112 fn from_str(s: &str) -> Result<Self, Self::Err> {
113 let parts: Vec<&str> = s.split('.').collect();
114
115 if !matches!(parts.len(), 2 | 3) {
117 return Err(format!(
118 "Invalid version format '{}': expected MAJOR.MINOR.PATCH or MAJOR.MINOR (e.g., '2.1.0' or '2.1')",
119 s
120 ));
121 }
122
123 let major = parts[0]
124 .parse::<u32>()
125 .map_err(|_| format!("Invalid major version '{}': must be a number", parts[0]))?;
126
127 let minor = parts[1]
128 .parse::<u32>()
129 .map_err(|_| format!("Invalid minor version '{}': must be a number", parts[1]))?;
130
131 let patch = if parts.len() == 3 {
133 parts[2]
134 .parse::<u32>()
135 .map_err(|_| format!("Invalid patch version '{}': must be a number", parts[2]))?
136 } else {
137 0
138 };
139
140 Ok(Version {
141 major,
142 minor,
143 patch,
144 })
145 }
146}
147
148impl fmt::Display for Version {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
151 }
152}
153
154impl PartialOrd for Version {
155 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
156 Some(self.cmp(other))
157 }
158}
159
160impl Ord for Version {
161 fn cmp(&self, other: &Self) -> Ordering {
162 match self.major.cmp(&other.major) {
163 Ordering::Equal => match self.minor.cmp(&other.minor) {
164 Ordering::Equal => self.patch.cmp(&other.patch),
165 other => other,
166 },
167 other => other,
168 }
169 }
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Hash)]
186pub enum VersionSelector {
187 Exact(Version),
189 Minor(u32, u32),
191 Major(u32),
193 Latest,
195}
196
197impl FromStr for VersionSelector {
198 type Err = String;
199
200 fn from_str(s: &str) -> Result<Self, Self::Err> {
201 let version_str = s.strip_prefix('@').unwrap_or(s);
203
204 if version_str.is_empty() || version_str == "latest" {
205 return Ok(VersionSelector::Latest);
206 }
207
208 let parts: Vec<&str> = version_str.split('.').collect();
210
211 match parts.len() {
212 3 => {
214 let version = Version::from_str(version_str)?;
215 Ok(VersionSelector::Exact(version))
216 }
217 2 => {
219 let major = parts[0].parse::<u32>().map_err(|_| {
220 format!("Invalid major version '{}': must be a number", parts[0])
221 })?;
222 let minor = parts[1].parse::<u32>().map_err(|_| {
223 format!("Invalid minor version '{}': must be a number", parts[1])
224 })?;
225 Ok(VersionSelector::Minor(major, minor))
226 }
227 1 => {
229 let major = version_str.parse::<u32>().map_err(|_| {
230 format!(
231 "Invalid version selector '{}': expected number, MAJOR.MINOR, MAJOR.MINOR.PATCH, or 'latest'",
232 version_str
233 )
234 })?;
235 Ok(VersionSelector::Major(major))
236 }
237 _ => Err(format!(
238 "Invalid version selector '{}': expected number, MAJOR.MINOR, MAJOR.MINOR.PATCH, or 'latest'",
239 version_str
240 )),
241 }
242 }
243}
244
245impl fmt::Display for VersionSelector {
246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247 match self {
248 VersionSelector::Exact(v) => write!(f, "@{}", v),
249 VersionSelector::Minor(major, minor) => write!(f, "@{}.{}", major, minor),
250 VersionSelector::Major(m) => write!(f, "@{}", m),
251 VersionSelector::Latest => write!(f, "@latest"),
252 }
253 }
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, Hash)]
270pub struct QuillReference {
271 pub name: String,
273 pub selector: VersionSelector,
275}
276
277impl QuillReference {
278 pub fn new(name: String, selector: VersionSelector) -> Self {
280 Self { name, selector }
281 }
282
283 pub fn latest(name: String) -> Self {
285 Self {
286 name,
287 selector: VersionSelector::Latest,
288 }
289 }
290}
291
292impl FromStr for QuillReference {
293 type Err = String;
294
295 fn from_str(s: &str) -> Result<Self, Self::Err> {
296 let separator_idx = s.find('@');
298
299 let (name_part, version_part_opt) = match separator_idx {
300 Some(idx) => (&s[..idx], Some(&s[idx + 1..])),
301 None => (s, None),
302 };
303
304 if name_part.is_empty() {
305 return Err("Quill name cannot be empty".to_string());
306 }
307
308 let name = name_part.to_string();
309
310 if !name
312 .chars()
313 .next()
314 .is_some_and(|c| c.is_ascii_lowercase() || c == '_')
315 {
316 return Err(format!(
317 "Invalid Quill name '{}': must start with lowercase letter or underscore",
318 name
319 ));
320 }
321 if !name
322 .chars()
323 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
324 {
325 return Err(format!(
326 "Invalid Quill name '{}': must contain only lowercase letters, digits, and underscores",
327 name
328 ));
329 }
330
331 let selector = if let Some(version_part) = version_part_opt {
333 VersionSelector::from_str(&format!("@{}", version_part))?
334 } else {
335 VersionSelector::Latest
336 };
337
338 Ok(QuillReference { name, selector })
339 }
340}
341
342impl fmt::Display for QuillReference {
343 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
344 match &self.selector {
345 VersionSelector::Latest => write!(f, "{}", self.name),
346 _ => write!(f, "{}{}", self.name, self.selector),
347 }
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354
355 #[test]
356 fn test_version_parsing() {
357 let v = Version::from_str("2.1.0").unwrap();
359 assert_eq!(v.major, 2);
360 assert_eq!(v.minor, 1);
361 assert_eq!(v.patch, 0);
362 assert_eq!(v.to_string(), "2.1.0");
363
364 let v2 = Version::from_str("1.2.3").unwrap();
366 assert_eq!(v2.major, 1);
367 assert_eq!(v2.minor, 2);
368 assert_eq!(v2.patch, 3);
369 assert_eq!(v2.to_string(), "1.2.3");
370 }
371
372 #[test]
373 fn test_version_parsing_two_segment_backward_compat() {
374 let v = Version::from_str("2.1").unwrap();
376 assert_eq!(v.major, 2);
377 assert_eq!(v.minor, 1);
378 assert_eq!(v.patch, 0);
379 assert_eq!(v.to_string(), "2.1.0");
380 }
381
382 #[test]
383 fn test_version_invalid() {
384 assert!(Version::from_str("2").is_err());
385 assert!(Version::from_str("2.1.0.0").is_err());
386 assert!(Version::from_str("abc").is_err());
387 assert!(Version::from_str("2.x").is_err());
388 assert!(Version::from_str("2.1.x").is_err());
389 }
390
391 #[test]
392 fn test_version_ordering() {
393 let v1_0_0 = Version::new(1, 0, 0);
394 let v1_0_1 = Version::new(1, 0, 1);
395 let v1_1_0 = Version::new(1, 1, 0);
396 let v2_0_0 = Version::new(2, 0, 0);
397 let v2_1_0 = Version::new(2, 1, 0);
398
399 assert!(v1_0_0 < v1_0_1);
400 assert!(v1_0_1 < v1_1_0);
401 assert!(v1_1_0 < v2_0_0);
402 assert!(v2_0_0 < v2_1_0);
403 assert_eq!(v1_0_0, v1_0_0);
404 }
405
406 #[test]
407 fn test_version_selector_parsing() {
408 let exact = VersionSelector::from_str("@2.1.0").unwrap();
410 assert_eq!(exact, VersionSelector::Exact(Version::new(2, 1, 0)));
411
412 let minor = VersionSelector::from_str("@2.1").unwrap();
414 assert_eq!(minor, VersionSelector::Minor(2, 1));
415
416 let major = VersionSelector::from_str("@2").unwrap();
418 assert_eq!(major, VersionSelector::Major(2));
419
420 let latest1 = VersionSelector::from_str("@latest").unwrap();
422 assert_eq!(latest1, VersionSelector::Latest);
423
424 let latest2 = VersionSelector::from_str("").unwrap();
426 assert_eq!(latest2, VersionSelector::Latest);
427 }
428
429 #[test]
430 fn test_version_selector_without_at() {
431 let exact = VersionSelector::from_str("2.1.0").unwrap();
432 assert_eq!(exact, VersionSelector::Exact(Version::new(2, 1, 0)));
433
434 let minor = VersionSelector::from_str("2.1").unwrap();
435 assert_eq!(minor, VersionSelector::Minor(2, 1));
436
437 let major = VersionSelector::from_str("2").unwrap();
438 assert_eq!(major, VersionSelector::Major(2));
439 }
440
441 #[test]
442 fn test_version_selector_display() {
443 assert_eq!(
444 VersionSelector::Exact(Version::new(2, 1, 0)).to_string(),
445 "@2.1.0"
446 );
447 assert_eq!(VersionSelector::Minor(2, 1).to_string(), "@2.1");
448 assert_eq!(VersionSelector::Major(2).to_string(), "@2");
449 assert_eq!(VersionSelector::Latest.to_string(), "@latest");
450 }
451
452 #[test]
453 fn test_quill_reference_parsing() {
454 let ref1 = QuillReference::from_str("resume_template@2.1.0").unwrap();
456 assert_eq!(ref1.name, "resume_template");
457 assert_eq!(ref1.selector, VersionSelector::Exact(Version::new(2, 1, 0)));
458
459 let ref1b = QuillReference::from_str("resume_template@2.1").unwrap();
461 assert_eq!(ref1b.name, "resume_template");
462 assert_eq!(ref1b.selector, VersionSelector::Minor(2, 1));
463
464 let ref2 = QuillReference::from_str("resume_template@2").unwrap();
466 assert_eq!(ref2.name, "resume_template");
467 assert_eq!(ref2.selector, VersionSelector::Major(2));
468
469 let ref3 = QuillReference::from_str("resume_template@latest").unwrap();
471 assert_eq!(ref3.name, "resume_template");
472 assert_eq!(ref3.selector, VersionSelector::Latest);
473
474 let ref4 = QuillReference::from_str("resume_template").unwrap();
476 assert_eq!(ref4.name, "resume_template");
477 assert_eq!(ref4.selector, VersionSelector::Latest);
478 }
479
480 #[test]
481 fn test_quill_reference_invalid_names() {
482 assert!(QuillReference::from_str("Resume@2.1.0").is_err());
484 assert!(QuillReference::from_str("1resume@2.1.0").is_err());
485
486 assert!(QuillReference::from_str("resume-template@2.1.0").is_err());
488 assert!(QuillReference::from_str("resume.template@2.1.0").is_err());
489
490 assert!(QuillReference::from_str("resume_template@2.1.0").is_ok());
492 assert!(QuillReference::from_str("_private@2.1.0").is_ok());
493 assert!(QuillReference::from_str("template2@2.1.0").is_ok());
494 }
495
496 #[test]
497 fn test_quill_reference_display() {
498 let ref1 = QuillReference::new(
499 "resume".to_string(),
500 VersionSelector::Exact(Version::new(2, 1, 0)),
501 );
502 assert_eq!(ref1.to_string(), "resume@2.1.0");
503
504 let ref1b = QuillReference::new("resume".to_string(), VersionSelector::Minor(2, 1));
505 assert_eq!(ref1b.to_string(), "resume@2.1");
506
507 let ref2 = QuillReference::new("resume".to_string(), VersionSelector::Major(2));
508 assert_eq!(ref2.to_string(), "resume@2");
509
510 let ref3 = QuillReference::new("resume".to_string(), VersionSelector::Latest);
511 assert_eq!(ref3.to_string(), "resume");
512 }
513}