Skip to main content

quillmark_core/
version.rs

1//! # Version Management
2//!
3//! Version types and parsing for Quill template versioning.
4//!
5//! ## Overview
6//!
7//! This module provides types for managing Quill template versions using
8//! semantic versioning (MAJOR.MINOR.PATCH). This follows industry standard
9//! semver conventions for compatibility signaling.
10//!
11//! ## Key Types
12//!
13//! - [`Version`]: Semantic version number (MAJOR.MINOR.PATCH)
14//! - [`VersionSelector`]: Specifies which version to use (exact, major, minor, or latest)
15//! - [`QuillReference`]: Complete reference to a Quill with name and version
16//!
17//! ## Examples
18//!
19//! ### Parsing Versions
20//!
21//! ```
22//! use quillmark_core::version::Version;
23//! use std::str::FromStr;
24//!
25//! let v = Version::from_str("2.1.0").unwrap();
26//! assert_eq!(v.major, 2);
27//! assert_eq!(v.minor, 1);
28//! assert_eq!(v.patch, 0);
29//! assert_eq!(v.to_string(), "2.1.0");
30//!
31//! // Two-segment versions are also supported (patch defaults to 0)
32//! let v2 = Version::from_str("2.1").unwrap();
33//! assert_eq!(v2.patch, 0);
34//! ```
35//!
36//! ### Version Comparison
37//!
38//! ```
39//! use quillmark_core::version::Version;
40//! use std::str::FromStr;
41//!
42//! let v1 = Version::from_str("1.0.0").unwrap();
43//! let v2 = Version::from_str("2.1.0").unwrap();
44//! assert!(v1 < v2);
45//! ```
46//!
47//! ### Parsing Quill References
48//!
49//! ```
50//! use quillmark_core::version::QuillReference;
51//! use std::str::FromStr;
52//!
53//! let ref1 = QuillReference::from_str("resume_template@2.1.0").unwrap();
54//! assert_eq!(ref1.name, "resume_template");
55//!
56//! let ref2 = QuillReference::from_str("resume_template@2").unwrap();
57//! let ref3 = QuillReference::from_str("resume_template@latest").unwrap();
58//! let ref4 = QuillReference::from_str("resume_template").unwrap();
59//! ```
60
61use std::cmp::Ordering;
62use std::fmt;
63use std::str::FromStr;
64
65/// Semantic version number (MAJOR.MINOR.PATCH)
66///
67/// Versions follow semantic versioning conventions where:
68/// - MAJOR indicates breaking changes
69/// - MINOR indicates new features (backward-compatible)
70/// - PATCH indicates bug fixes (backward-compatible)
71///
72/// # Examples
73///
74/// ```
75/// use quillmark_core::version::Version;
76/// use std::str::FromStr;
77///
78/// let v = Version::new(2, 1, 0);
79/// assert_eq!(v.to_string(), "2.1.0");
80///
81/// let parsed = Version::from_str("2.1.0").unwrap();
82/// assert_eq!(parsed, v);
83///
84/// // Two-segment versions are also supported (patch defaults to 0)
85/// let v2 = Version::from_str("2.1").unwrap();
86/// assert_eq!(v2, Version::new(2, 1, 0));
87/// ```
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
89pub struct Version {
90    /// Major version number (breaking changes)
91    pub major: u32,
92    /// Minor version number (new features, backward-compatible)
93    pub minor: u32,
94    /// Patch version number (bug fixes, backward-compatible)
95    pub patch: u32,
96}
97
98impl Version {
99    /// Create a new version
100    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        // Support both two-segment (MAJOR.MINOR) and three-segment (MAJOR.MINOR.PATCH)
116        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        // Default patch to 0 for backward compatibility with two-segment versions
132        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/// Specifies which version of a Quill template to use
173///
174/// # Examples
175///
176/// ```
177/// use quillmark_core::version::VersionSelector;
178/// use std::str::FromStr;
179///
180/// let exact = VersionSelector::from_str("@2.1.0").unwrap();
181/// let minor = VersionSelector::from_str("@2.1").unwrap();
182/// let major = VersionSelector::from_str("@2").unwrap();
183/// let latest = VersionSelector::from_str("@latest").unwrap();
184/// ```
185#[derive(Debug, Clone, PartialEq, Eq, Hash)]
186pub enum VersionSelector {
187    /// Match exactly this version (e.g., "@2.1.0")
188    Exact(Version),
189    /// Match latest patch version in this minor series (e.g., "@2.1")
190    Minor(u32, u32),
191    /// Match latest minor/patch version in this major series (e.g., "@2")
192    Major(u32),
193    /// Match the highest version available (e.g., "@latest" or unspecified)
194    Latest,
195}
196
197impl FromStr for VersionSelector {
198    type Err = String;
199
200    fn from_str(s: &str) -> Result<Self, Self::Err> {
201        // Strip leading @ if present
202        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        // Count segments to determine selector type
209        let parts: Vec<&str> = version_str.split('.').collect();
210
211        match parts.len() {
212            // Three segments: exact version (MAJOR.MINOR.PATCH)
213            3 => {
214                let version = Version::from_str(version_str)?;
215                Ok(VersionSelector::Exact(version))
216            }
217            // Two segments: minor selector (MAJOR.MINOR -> latest MAJOR.MINOR.x)
218            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            // One segment: major selector or invalid
228            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/// Complete reference to a Quill template with name and version selector
257///
258/// # Examples
259///
260/// ```
261/// use quillmark_core::version::QuillReference;
262/// use std::str::FromStr;
263///
264/// let ref1 = QuillReference::from_str("resume_template@2.1").unwrap();
265/// assert_eq!(ref1.name, "resume_template");
266///
267/// let ref2 = QuillReference::from_str("resume_template").unwrap();
268/// ```
269#[derive(Debug, Clone, PartialEq, Eq, Hash)]
270pub struct QuillReference {
271    /// Template name (e.g., "resume_template")
272    pub name: String,
273    /// Version selector (defaults to Latest if not specified)
274    pub selector: VersionSelector,
275}
276
277impl QuillReference {
278    /// Create a new QuillReference
279    pub fn new(name: String, selector: VersionSelector) -> Self {
280        Self { name, selector }
281    }
282
283    /// Create a QuillReference with Latest selector
284    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        // Find separator index (first occurrence of '@')
297        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        // Validate name format: [a-z_][a-z0-9_]*
311        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        // Parse version selector if present
332        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        // Three-segment semver
358        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        // Three-segment with non-zero patch
365        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        // Two-segment backward compatibility (patch defaults to 0)
375        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        // Exact version (three segments)
409        let exact = VersionSelector::from_str("@2.1.0").unwrap();
410        assert_eq!(exact, VersionSelector::Exact(Version::new(2, 1, 0)));
411
412        // Minor selector (two segments)
413        let minor = VersionSelector::from_str("@2.1").unwrap();
414        assert_eq!(minor, VersionSelector::Minor(2, 1));
415
416        // Major selector (one segment)
417        let major = VersionSelector::from_str("@2").unwrap();
418        assert_eq!(major, VersionSelector::Major(2));
419
420        // Latest (explicit)
421        let latest1 = VersionSelector::from_str("@latest").unwrap();
422        assert_eq!(latest1, VersionSelector::Latest);
423
424        // Latest (empty string)
425        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        // Exact version (three segments)
455        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        // Minor selector (two segments)
460        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        // Major selector
465        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        // Latest (explicit)
470        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        // Latest (implicit)
475        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        // Must start with lowercase or underscore
483        assert!(QuillReference::from_str("Resume@2.1.0").is_err());
484        assert!(QuillReference::from_str("1resume@2.1.0").is_err());
485
486        // Must contain only lowercase, digits, underscores
487        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        // Valid names
491        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}