1use std::cmp::Ordering;
57use std::fmt;
58use std::str::FromStr;
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
79pub struct Version {
80 pub major: u32,
82 pub minor: u32,
84}
85
86impl Version {
87 pub fn new(major: u32, minor: u32) -> Self {
89 Self { major, minor }
90 }
91}
92
93impl FromStr for Version {
94 type Err = String;
95
96 fn from_str(s: &str) -> Result<Self, Self::Err> {
97 let parts: Vec<&str> = s.split('.').collect();
98 if parts.len() != 2 {
99 return Err(format!(
100 "Invalid version format '{}': expected MAJOR.MINOR (e.g., '2.1')",
101 s
102 ));
103 }
104
105 let major = parts[0]
106 .parse::<u32>()
107 .map_err(|_| format!("Invalid major version '{}': must be a number", parts[0]))?;
108
109 let minor = parts[1]
110 .parse::<u32>()
111 .map_err(|_| format!("Invalid minor version '{}': must be a number", parts[1]))?;
112
113 Ok(Version { major, minor })
114 }
115}
116
117impl fmt::Display for Version {
118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119 write!(f, "{}.{}", self.major, self.minor)
120 }
121}
122
123impl PartialOrd for Version {
124 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
125 Some(self.cmp(other))
126 }
127}
128
129impl Ord for Version {
130 fn cmp(&self, other: &Self) -> Ordering {
131 match self.major.cmp(&other.major) {
132 Ordering::Equal => self.minor.cmp(&other.minor),
133 other => other,
134 }
135 }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Hash)]
151pub enum VersionSelector {
152 Exact(Version),
154 Major(u32),
156 Latest,
158}
159
160impl FromStr for VersionSelector {
161 type Err = String;
162
163 fn from_str(s: &str) -> Result<Self, Self::Err> {
164 let version_str = s.strip_prefix('@').unwrap_or(s);
166
167 if version_str.is_empty() || version_str == "latest" {
168 return Ok(VersionSelector::Latest);
169 }
170
171 if version_str.contains('.') {
173 let version = Version::from_str(version_str)?;
174 return Ok(VersionSelector::Exact(version));
175 }
176
177 let major = version_str.parse::<u32>().map_err(|_| {
179 format!(
180 "Invalid version selector '{}': expected number, MAJOR.MINOR, or 'latest'",
181 version_str
182 )
183 })?;
184
185 Ok(VersionSelector::Major(major))
186 }
187}
188
189impl fmt::Display for VersionSelector {
190 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191 match self {
192 VersionSelector::Exact(v) => write!(f, "@{}", v),
193 VersionSelector::Major(m) => write!(f, "@{}", m),
194 VersionSelector::Latest => write!(f, "@latest"),
195 }
196 }
197}
198
199#[derive(Debug, Clone, PartialEq, Eq, Hash)]
213pub struct QuillReference {
214 pub name: String,
216 pub selector: VersionSelector,
218}
219
220impl QuillReference {
221 pub fn new(name: String, selector: VersionSelector) -> Self {
223 Self { name, selector }
224 }
225
226 pub fn latest(name: String) -> Self {
228 Self {
229 name,
230 selector: VersionSelector::Latest,
231 }
232 }
233}
234
235impl FromStr for QuillReference {
236 type Err = String;
237
238 fn from_str(s: &str) -> Result<Self, Self::Err> {
239 let separator_idx = s.find('@').or_else(|| s.find(':'));
241
242 let (name_part, version_part_opt) = match separator_idx {
243 Some(idx) => (&s[..idx], Some(&s[idx + 1..])),
244 None => (s, None),
245 };
246
247 if name_part.is_empty() {
248 return Err("Quill name cannot be empty".to_string());
249 }
250
251 let name = name_part.to_string();
252
253 if !name
255 .chars()
256 .next()
257 .is_some_and(|c| c.is_ascii_lowercase() || c == '_')
258 {
259 return Err(format!(
260 "Invalid Quill name '{}': must start with lowercase letter or underscore",
261 name
262 ));
263 }
264 if !name
265 .chars()
266 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
267 {
268 return Err(format!(
269 "Invalid Quill name '{}': must contain only lowercase letters, digits, and underscores",
270 name
271 ));
272 }
273
274 let selector = if let Some(version_part) = version_part_opt {
276 VersionSelector::from_str(&format!("@{}", version_part))?
277 } else {
278 VersionSelector::Latest
279 };
280
281 Ok(QuillReference { name, selector })
282 }
283}
284
285impl fmt::Display for QuillReference {
286 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287 match &self.selector {
288 VersionSelector::Latest => write!(f, "{}", self.name),
289 _ => write!(f, "{}{}", self.name, self.selector),
290 }
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn test_version_parsing() {
300 let v = Version::from_str("2.1").unwrap();
301 assert_eq!(v.major, 2);
302 assert_eq!(v.minor, 1);
303 assert_eq!(v.to_string(), "2.1");
304 }
305
306 #[test]
307 fn test_version_invalid() {
308 assert!(Version::from_str("2").is_err());
309 assert!(Version::from_str("2.1.0").is_err());
310 assert!(Version::from_str("abc").is_err());
311 assert!(Version::from_str("2.x").is_err());
312 }
313
314 #[test]
315 fn test_version_ordering() {
316 let v1_0 = Version::new(1, 0);
317 let v1_1 = Version::new(1, 1);
318 let v2_0 = Version::new(2, 0);
319 let v2_1 = Version::new(2, 1);
320
321 assert!(v1_0 < v1_1);
322 assert!(v1_1 < v2_0);
323 assert!(v2_0 < v2_1);
324 assert_eq!(v1_0, v1_0);
325 }
326
327 #[test]
328 fn test_version_selector_parsing() {
329 let exact = VersionSelector::from_str("@2.1").unwrap();
330 assert_eq!(exact, VersionSelector::Exact(Version::new(2, 1)));
331
332 let major = VersionSelector::from_str("@2").unwrap();
333 assert_eq!(major, VersionSelector::Major(2));
334
335 let latest1 = VersionSelector::from_str("@latest").unwrap();
336 assert_eq!(latest1, VersionSelector::Latest);
337
338 let latest2 = VersionSelector::from_str("").unwrap();
339 assert_eq!(latest2, VersionSelector::Latest);
340 }
341
342 #[test]
343 fn test_version_selector_without_at() {
344 let exact = VersionSelector::from_str("2.1").unwrap();
345 assert_eq!(exact, VersionSelector::Exact(Version::new(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_quill_reference_parsing() {
353 let ref1 = QuillReference::from_str("resume_template@2.1").unwrap();
354 assert_eq!(ref1.name, "resume_template");
355 assert_eq!(ref1.selector, VersionSelector::Exact(Version::new(2, 1)));
356
357 let ref2 = QuillReference::from_str("resume_template@2").unwrap();
358 assert_eq!(ref2.name, "resume_template");
359 assert_eq!(ref2.selector, VersionSelector::Major(2));
360
361 let ref3 = QuillReference::from_str("resume_template@latest").unwrap();
362 assert_eq!(ref3.name, "resume_template");
363 assert_eq!(ref3.selector, VersionSelector::Latest);
364
365 let ref4 = QuillReference::from_str("resume_template").unwrap();
366 assert_eq!(ref4.name, "resume_template");
367 assert_eq!(ref4.selector, VersionSelector::Latest);
368 }
369
370 #[test]
371 fn test_quill_reference_invalid_names() {
372 assert!(QuillReference::from_str("Resume@2.1").is_err());
374 assert!(QuillReference::from_str("1resume@2.1").is_err());
375
376 assert!(QuillReference::from_str("resume-template@2.1").is_err());
378 assert!(QuillReference::from_str("resume.template@2.1").is_err());
379
380 assert!(QuillReference::from_str("resume_template@2.1").is_ok());
382 assert!(QuillReference::from_str("_private@2.1").is_ok());
383 assert!(QuillReference::from_str("template2@2.1").is_ok());
384 }
385
386 #[test]
387 fn test_quill_reference_display() {
388 let ref1 = QuillReference::new(
389 "resume".to_string(),
390 VersionSelector::Exact(Version::new(2, 1)),
391 );
392 assert_eq!(ref1.to_string(), "resume@2.1");
393
394 let ref2 = QuillReference::new("resume".to_string(), VersionSelector::Major(2));
395 assert_eq!(ref2.to_string(), "resume@2");
396
397 let ref3 = QuillReference::new("resume".to_string(), VersionSelector::Latest);
398 assert_eq!(ref3.to_string(), "resume");
399 }
400 #[test]
401 fn test_quill_reference_parsing_with_colon() {
402 let ref1 = QuillReference::from_str("usaf_memo:0.1").unwrap();
403 assert_eq!(ref1.name, "usaf_memo");
404 assert_eq!(ref1.selector, VersionSelector::Exact(Version::new(0, 1)));
405
406 let ref2 = QuillReference::from_str("name:latest").unwrap();
407 assert_eq!(ref2.name, "name");
408 assert_eq!(ref2.selector, VersionSelector::Latest);
409 }
410}