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 VersionSelector {
106 pub fn matches(&self, v: Version) -> bool {
111 match self {
112 VersionSelector::Exact(want) => *want == v,
113 VersionSelector::Minor(major, minor) => v.major == *major && v.minor == *minor,
114 VersionSelector::Major(major) => v.major == *major,
115 VersionSelector::Latest => true,
116 }
117 }
118}
119
120impl FromStr for VersionSelector {
121 type Err = String;
122
123 fn from_str(s: &str) -> Result<Self, Self::Err> {
124 let version_str = s.strip_prefix('@').unwrap_or(s);
125
126 if version_str.is_empty() || version_str == "latest" {
127 return Ok(VersionSelector::Latest);
128 }
129
130 let parts: Vec<&str> = version_str.split('.').collect();
131
132 match parts.len() {
133 3 => {
134 let version = Version::from_str(version_str)?;
135 Ok(VersionSelector::Exact(version))
136 }
137 2 => {
138 let major = parts[0].parse::<u32>().map_err(|_| {
139 format!("Invalid major version '{}': must be a number", parts[0])
140 })?;
141 let minor = parts[1].parse::<u32>().map_err(|_| {
142 format!("Invalid minor version '{}': must be a number", parts[1])
143 })?;
144 Ok(VersionSelector::Minor(major, minor))
145 }
146 1 => {
147 let major = version_str.parse::<u32>().map_err(|_| {
148 format!(
149 "Invalid version selector '{}': expected number, MAJOR.MINOR, MAJOR.MINOR.PATCH, or 'latest'",
150 version_str
151 )
152 })?;
153 Ok(VersionSelector::Major(major))
154 }
155 _ => Err(format!(
156 "Invalid version selector '{}': expected number, MAJOR.MINOR, MAJOR.MINOR.PATCH, or 'latest'",
157 version_str
158 )),
159 }
160 }
161}
162
163impl fmt::Display for VersionSelector {
164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165 match self {
166 VersionSelector::Exact(v) => write!(f, "@{}", v),
167 VersionSelector::Minor(major, minor) => write!(f, "@{}.{}", major, minor),
168 VersionSelector::Major(m) => write!(f, "@{}", m),
169 VersionSelector::Latest => write!(f, "@latest"),
170 }
171 }
172}
173
174const QUILL_REF_HINT: &str = "A $quill reference is `<name>` or `<name>@<selector>`. \
176The name must match `[a-z_][a-z0-9_]*` (start with a lowercase letter or underscore, then \
177lowercase letters, digits, or underscores). The optional version selector is \
178`@MAJOR.MINOR.PATCH` (exact), `@MAJOR.MINOR` (latest patch in that minor series), `@MAJOR` \
179(latest in that major series), or `@latest`; omitting the selector means latest.";
180
181pub fn quill_ref_hint() -> &'static str {
187 QUILL_REF_HINT
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, Hash)]
194pub struct QuillReference {
195 pub name: String,
196 pub selector: VersionSelector,
197}
198
199impl QuillReference {
200 pub fn new(name: String, selector: VersionSelector) -> Self {
201 Self { name, selector }
202 }
203
204 pub fn latest(name: String) -> Self {
205 Self {
206 name,
207 selector: VersionSelector::Latest,
208 }
209 }
210}
211
212impl FromStr for QuillReference {
213 type Err = String;
214
215 fn from_str(s: &str) -> Result<Self, Self::Err> {
216 let separator_idx = s.find('@');
217
218 let (name_part, version_part_opt) = match separator_idx {
219 Some(idx) => (&s[..idx], Some(&s[idx + 1..])),
220 None => (s, None),
221 };
222
223 if name_part.is_empty() {
224 return Err("Quill name cannot be empty".to_string());
225 }
226
227 let name = name_part.to_string();
228
229 if !name
230 .chars()
231 .next()
232 .is_some_and(|c| c.is_ascii_lowercase() || c == '_')
233 {
234 return Err(format!(
235 "Invalid Quill name '{}': must start with lowercase letter or underscore",
236 name
237 ));
238 }
239 if !name
240 .chars()
241 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
242 {
243 return Err(format!(
244 "Invalid Quill name '{}': must contain only lowercase letters, digits, and underscores",
245 name
246 ));
247 }
248
249 let selector = if let Some(version_part) = version_part_opt {
250 VersionSelector::from_str(&format!("@{}", version_part))?
251 } else {
252 VersionSelector::Latest
253 };
254
255 Ok(QuillReference { name, selector })
256 }
257}
258
259impl fmt::Display for QuillReference {
260 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261 match &self.selector {
262 VersionSelector::Latest => write!(f, "{}", self.name),
263 _ => write!(f, "{}{}", self.name, self.selector),
264 }
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn test_version_parsing() {
274 let v = Version::from_str("2.1.0").unwrap();
275 assert_eq!(v.major, 2);
276 assert_eq!(v.minor, 1);
277 assert_eq!(v.patch, 0);
278 assert_eq!(v.to_string(), "2.1.0");
279
280 let v2 = Version::from_str("1.2.3").unwrap();
281 assert_eq!(v2.major, 1);
282 assert_eq!(v2.minor, 2);
283 assert_eq!(v2.patch, 3);
284 assert_eq!(v2.to_string(), "1.2.3");
285 }
286
287 #[test]
288 fn test_version_parsing_two_segment_backward_compat() {
289 let v = Version::from_str("2.1").unwrap();
290 assert_eq!(v.major, 2);
291 assert_eq!(v.minor, 1);
292 assert_eq!(v.patch, 0);
293 assert_eq!(v.to_string(), "2.1.0");
294 }
295
296 #[test]
297 fn test_version_invalid() {
298 assert!(Version::from_str("2").is_err());
299 assert!(Version::from_str("2.1.0.0").is_err());
300 assert!(Version::from_str("abc").is_err());
301 assert!(Version::from_str("2.x").is_err());
302 assert!(Version::from_str("2.1.x").is_err());
303 }
304
305 #[test]
306 fn test_version_ordering() {
307 let v1_0_0 = Version::new(1, 0, 0);
308 let v1_0_1 = Version::new(1, 0, 1);
309 let v1_1_0 = Version::new(1, 1, 0);
310 let v2_0_0 = Version::new(2, 0, 0);
311 let v2_1_0 = Version::new(2, 1, 0);
312
313 assert!(v1_0_0 < v1_0_1);
314 assert!(v1_0_1 < v1_1_0);
315 assert!(v1_1_0 < v2_0_0);
316 assert!(v2_0_0 < v2_1_0);
317 assert_eq!(v1_0_0, v1_0_0);
318 }
319
320 #[test]
321 fn test_version_selector_parsing() {
322 let exact = VersionSelector::from_str("@2.1.0").unwrap();
323 assert_eq!(exact, VersionSelector::Exact(Version::new(2, 1, 0)));
324
325 let minor = VersionSelector::from_str("@2.1").unwrap();
326 assert_eq!(minor, VersionSelector::Minor(2, 1));
327
328 let major = VersionSelector::from_str("@2").unwrap();
329 assert_eq!(major, VersionSelector::Major(2));
330
331 let latest1 = VersionSelector::from_str("@latest").unwrap();
332 assert_eq!(latest1, VersionSelector::Latest);
333
334 let latest2 = VersionSelector::from_str("").unwrap();
336 assert_eq!(latest2, VersionSelector::Latest);
337 }
338
339 #[test]
340 fn test_version_selector_without_at() {
341 let exact = VersionSelector::from_str("2.1.0").unwrap();
342 assert_eq!(exact, VersionSelector::Exact(Version::new(2, 1, 0)));
343
344 let minor = VersionSelector::from_str("2.1").unwrap();
345 assert_eq!(minor, VersionSelector::Minor(2, 1));
346
347 let major = VersionSelector::from_str("2").unwrap();
348 assert_eq!(major, VersionSelector::Major(2));
349 }
350
351 #[test]
352 fn test_version_selector_matches() {
353 let v2_1_0 = Version::new(2, 1, 0);
354 let v2_1_3 = Version::new(2, 1, 3);
355 let v2_2_0 = Version::new(2, 2, 0);
356 let v3_0_0 = Version::new(3, 0, 0);
357
358 let exact = VersionSelector::Exact(v2_1_0);
360 assert!(exact.matches(v2_1_0));
361 assert!(!exact.matches(v2_1_3));
362 assert!(!exact.matches(v2_2_0));
363
364 let minor = VersionSelector::Minor(2, 1);
366 assert!(minor.matches(v2_1_0));
367 assert!(minor.matches(v2_1_3));
368 assert!(!minor.matches(v2_2_0));
369 assert!(!minor.matches(v3_0_0));
370
371 let major = VersionSelector::Major(2);
373 assert!(major.matches(v2_1_0));
374 assert!(major.matches(v2_2_0));
375 assert!(!major.matches(v3_0_0));
376
377 let latest = VersionSelector::Latest;
379 assert!(latest.matches(v2_1_0));
380 assert!(latest.matches(v3_0_0));
381 }
382
383 #[test]
384 fn test_version_selector_display() {
385 assert_eq!(
386 VersionSelector::Exact(Version::new(2, 1, 0)).to_string(),
387 "@2.1.0"
388 );
389 assert_eq!(VersionSelector::Minor(2, 1).to_string(), "@2.1");
390 assert_eq!(VersionSelector::Major(2).to_string(), "@2");
391 assert_eq!(VersionSelector::Latest.to_string(), "@latest");
392 }
393
394 #[test]
395 fn test_quill_reference_parsing() {
396 let ref1 = QuillReference::from_str("resume_template@2.1.0").unwrap();
397 assert_eq!(ref1.name, "resume_template");
398 assert_eq!(ref1.selector, VersionSelector::Exact(Version::new(2, 1, 0)));
399
400 let ref1b = QuillReference::from_str("resume_template@2.1").unwrap();
401 assert_eq!(ref1b.selector, VersionSelector::Minor(2, 1));
402
403 let ref2 = QuillReference::from_str("resume_template@2").unwrap();
404 assert_eq!(ref2.selector, VersionSelector::Major(2));
405
406 let ref3 = QuillReference::from_str("resume_template@latest").unwrap();
407 assert_eq!(ref3.selector, VersionSelector::Latest);
408
409 let ref4 = QuillReference::from_str("resume_template").unwrap();
411 assert_eq!(ref4.name, "resume_template");
412 assert_eq!(ref4.selector, VersionSelector::Latest);
413 }
414
415 #[test]
416 fn test_quill_reference_invalid_names() {
417 assert!(QuillReference::from_str("Resume@2.1.0").is_err());
418 assert!(QuillReference::from_str("1resume@2.1.0").is_err());
419 assert!(QuillReference::from_str("resume-template@2.1.0").is_err());
420 assert!(QuillReference::from_str("resume.template@2.1.0").is_err());
421 assert!(QuillReference::from_str("resume_template@2.1.0").is_ok());
422 assert!(QuillReference::from_str("_private@2.1.0").is_ok());
423 assert!(QuillReference::from_str("template2@2.1.0").is_ok());
424 }
425
426 #[test]
427 fn test_quill_reference_display() {
428 let ref1 = QuillReference::new(
429 "resume".to_string(),
430 VersionSelector::Exact(Version::new(2, 1, 0)),
431 );
432 assert_eq!(ref1.to_string(), "resume@2.1.0");
433
434 let ref1b = QuillReference::new("resume".to_string(), VersionSelector::Minor(2, 1));
435 assert_eq!(ref1b.to_string(), "resume@2.1");
436
437 let ref2 = QuillReference::new("resume".to_string(), VersionSelector::Major(2));
438 assert_eq!(ref2.to_string(), "resume@2");
439
440 let ref3 = QuillReference::new("resume".to_string(), VersionSelector::Latest);
441 assert_eq!(ref3.to_string(), "resume");
442 }
443
444 #[test]
445 fn test_quill_ref_hint_describes_the_grammar() {
446 let hint = quill_ref_hint();
447 assert!(!hint.is_empty());
448 assert!(hint.contains("[a-z_][a-z0-9_]*"), "got: {hint}");
450 assert!(hint.contains("@latest"), "got: {hint}");
451 assert!(hint.contains("@MAJOR.MINOR.PATCH"), "got: {hint}");
452 }
453}