Skip to main content

sqlite_graphrag/spawn/
executor_version.rs

1//! Executor version parsing (v1.0.75 — G22)
2
3use crate::errors::AppError;
4use semver::Version;
5use serde::{Deserialize, Serialize};
6use std::cmp::Ordering;
7use std::str::FromStr;
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub struct ExecutorVersion {
11    pub raw: String,
12    pub semver: Option<Version>,
13    pub channel: Option<String>,
14}
15
16impl ExecutorVersion {
17    pub fn unknown() -> Self {
18        Self {
19            raw: "unknown".to_string(),
20            semver: None,
21            channel: None,
22        }
23    }
24
25    pub fn parse(s: &str) -> Result<Self, AppError> {
26        let trimmed = s.trim();
27        if trimmed.is_empty() {
28            return Err(AppError::Validation("empty version string".to_string()));
29        }
30        let channel_start = trimmed.find('-');
31        let (numeric_part, channel_part) = match channel_start {
32            Some(idx) => (
33                trimmed[..idx].to_string(),
34                Some(trimmed[idx + 1..].to_string()),
35            ),
36            None => (trimmed.to_string(), None),
37        };
38        let semver = if numeric_part
39            .chars()
40            .next()
41            .map(|c| c.is_ascii_digit())
42            .unwrap_or(false)
43        {
44            Version::from_str(&numeric_part).ok()
45        } else {
46            None
47        };
48        Ok(Self {
49            raw: trimmed.to_string(),
50            semver,
51            channel: channel_part,
52        })
53    }
54
55    pub fn major(&self) -> u64 {
56        self.semver.as_ref().map(|v| v.major).unwrap_or(0)
57    }
58
59    pub fn minor(&self) -> u64 {
60        self.semver.as_ref().map(|v| v.minor).unwrap_or(0)
61    }
62
63    pub fn patch(&self) -> u64 {
64        self.semver.as_ref().map(|v| v.patch).unwrap_or(0)
65    }
66
67    pub fn is_at_least(&self, major: u64, minor: u64, patch: u64) -> bool {
68        match &self.semver {
69            Some(v) => (v.major, v.minor, v.patch) >= (major, minor, patch),
70            None => false,
71        }
72    }
73
74    pub fn in_range(&self, min: (u64, u64, u64), max: (u64, u64, u64)) -> bool {
75        self.is_at_least(min.0, min.1, min.2) && {
76            let current = (self.major(), self.minor(), self.patch());
77            current <= max
78        }
79    }
80
81    pub fn compare(&self, other: &ExecutorVersion) -> Ordering {
82        match (&self.semver, &other.semver) {
83            (Some(a), Some(b)) => a.cmp(b),
84            (Some(_), None) => Ordering::Greater,
85            (None, Some(_)) => Ordering::Less,
86            (None, None) => Ordering::Equal,
87        }
88    }
89}
90
91impl std::fmt::Display for ExecutorVersion {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        write!(f, "{}", self.raw)
94    }
95}
96
97impl Default for ExecutorVersion {
98    fn default() -> Self {
99        Self::unknown()
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn parse_simple_semver() {
109        let v = ExecutorVersion::parse("0.137.0").unwrap();
110        assert_eq!(v.major(), 0);
111        assert_eq!(v.minor(), 137);
112        assert_eq!(v.patch(), 0);
113        assert!(v.is_at_least(0, 130, 0));
114        assert!(!v.is_at_least(1, 0, 0));
115    }
116
117    #[test]
118    fn parse_with_channel() {
119        let v = ExecutorVersion::parse("2.1.0-beta.1").unwrap();
120        assert_eq!(v.major(), 2);
121        assert_eq!(v.minor(), 1);
122        assert_eq!(v.channel.as_deref(), Some("beta.1"));
123    }
124
125    #[test]
126    fn parse_unknown() {
127        let v = ExecutorVersion::parse("n/a").unwrap();
128        assert_eq!(v.semver, None);
129        assert!(!v.is_at_least(0, 0, 0));
130    }
131
132    #[test]
133    fn compare_ordering() {
134        let a = ExecutorVersion::parse("0.137.0").unwrap();
135        let b = ExecutorVersion::parse("0.138.0").unwrap();
136        assert_eq!(a.compare(&b), Ordering::Less);
137    }
138
139    #[test]
140    fn empty_string_is_error() {
141        assert!(ExecutorVersion::parse("").is_err());
142    }
143}