dx_forge/
version.rs

1//! DX Tool Version Management System
2//!
3//! Provides semantic versioning support, version comparison, compatibility checking,
4//! and tool version registry for the DX tools ecosystem.
5
6use anyhow::{anyhow, Context, Result};
7use serde::{Deserialize, Serialize};
8use std::cmp::Ordering;
9use std::collections::HashMap;
10use std::fmt;
11use std::path::{Path, PathBuf};
12use std::str::FromStr;
13
14/// Semantic version following semver 2.0.0 specification
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct Version {
17    pub major: u64,
18    pub minor: u64,
19    pub patch: u64,
20    pub pre_release: Option<String>,
21    pub build: Option<String>,
22}
23
24impl Version {
25    /// Create a new version
26    pub fn new(major: u64, minor: u64, patch: u64) -> Self {
27        Self {
28            major,
29            minor,
30            patch,
31            pre_release: None,
32            build: None,
33        }
34    }
35
36    /// Check if this version is compatible with another (same major version)
37    pub fn is_compatible_with(&self, other: &Version) -> bool {
38        self.major == other.major && self.major > 0
39    }
40
41    /// Check if this version satisfies a requirement
42    pub fn satisfies(&self, req: &VersionReq) -> bool {
43        match req {
44            VersionReq::Exact(v) => self == v,
45            VersionReq::GreaterThan(v) => self > v,
46            VersionReq::GreaterOrEqual(v) => self >= v,
47            VersionReq::LessThan(v) => self < v,
48            VersionReq::LessOrEqual(v) => self <= v,
49            VersionReq::Compatible(v) => self.is_compatible_with(v) && self >= v,
50            VersionReq::Any => true,
51        }
52    }
53
54    /// Check if this is a pre-release version
55    pub fn is_prerelease(&self) -> bool {
56        self.pre_release.is_some()
57    }
58
59    /// Check if this is a stable version (1.0.0 or greater, no pre-release)
60    pub fn is_stable(&self) -> bool {
61        self.major >= 1 && !self.is_prerelease()
62    }
63}
64
65impl fmt::Display for Version {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
68        if let Some(pre) = &self.pre_release {
69            write!(f, "-{}", pre)?;
70        }
71        if let Some(build) = &self.build {
72            write!(f, "+{}", build)?;
73        }
74        Ok(())
75    }
76}
77
78impl FromStr for Version {
79    type Err = anyhow::Error;
80
81    fn from_str(s: &str) -> Result<Self> {
82        // Remove 'v' prefix if present
83        let s = s.strip_prefix('v').unwrap_or(s);
84
85        // Split on '+' for build metadata
86        let (version_pre, build) = match s.split_once('+') {
87            Some((v, b)) => (v, Some(b.to_string())),
88            None => (s, None),
89        };
90
91        // Split on '-' for pre-release
92        let (version, pre_release) = match version_pre.split_once('-') {
93            Some((v, p)) => (v, Some(p.to_string())),
94            None => (version_pre, None),
95        };
96
97        // Parse major.minor.patch
98        let parts: Vec<&str> = version.split('.').collect();
99        if parts.len() != 3 {
100            return Err(anyhow!("Invalid version format: {}", s));
101        }
102
103        let major = parts[0]
104            .parse()
105            .context("Failed to parse major version")?;
106        let minor = parts[1]
107            .parse()
108            .context("Failed to parse minor version")?;
109        let patch = parts[2]
110            .parse()
111            .context("Failed to parse patch version")?;
112
113        Ok(Self {
114            major,
115            minor,
116            patch,
117            pre_release,
118            build,
119        })
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        // Compare major.minor.patch
132        match self.major.cmp(&other.major) {
133            Ordering::Equal => {}
134            ord => return ord,
135        }
136        match self.minor.cmp(&other.minor) {
137            Ordering::Equal => {}
138            ord => return ord,
139        }
140        match self.patch.cmp(&other.patch) {
141            Ordering::Equal => {}
142            ord => return ord,
143        }
144
145        // Pre-release versions have lower precedence
146        match (&self.pre_release, &other.pre_release) {
147            (None, None) => Ordering::Equal,
148            (Some(_), None) => Ordering::Less,
149            (None, Some(_)) => Ordering::Greater,
150            (Some(a), Some(b)) => a.cmp(b),
151        }
152    }
153}
154
155/// Version requirement for dependency resolution
156#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
157pub enum VersionReq {
158    /// Exact version match (=1.2.3)
159    Exact(Version),
160    /// Greater than (>1.2.3)
161    GreaterThan(Version),
162    /// Greater or equal (>=1.2.3)
163    GreaterOrEqual(Version),
164    /// Less than (<1.2.3)
165    LessThan(Version),
166    /// Less or equal (<=1.2.3)
167    LessOrEqual(Version),
168    /// Compatible (^1.2.3 - same major, >= minor.patch)
169    Compatible(Version),
170    /// Any version (*)
171    Any,
172}
173
174impl fmt::Display for VersionReq {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        match self {
177            VersionReq::Exact(v) => write!(f, "={}", v),
178            VersionReq::GreaterThan(v) => write!(f, ">{}", v),
179            VersionReq::GreaterOrEqual(v) => write!(f, ">={}", v),
180            VersionReq::LessThan(v) => write!(f, "<{}", v),
181            VersionReq::LessOrEqual(v) => write!(f, "<={}", v),
182            VersionReq::Compatible(v) => write!(f, "^{}", v),
183            VersionReq::Any => write!(f, "*"),
184        }
185    }
186}
187
188impl FromStr for VersionReq {
189    type Err = anyhow::Error;
190
191    fn from_str(s: &str) -> Result<Self> {
192        let s = s.trim();
193
194        if s == "*" {
195            return Ok(VersionReq::Any);
196        }
197
198        if let Some(v) = s.strip_prefix(">=") {
199            return Ok(VersionReq::GreaterOrEqual(v.trim().parse()?));
200        }
201        if let Some(v) = s.strip_prefix('>') {
202            return Ok(VersionReq::GreaterThan(v.trim().parse()?));
203        }
204        if let Some(v) = s.strip_prefix("<=") {
205            return Ok(VersionReq::LessOrEqual(v.trim().parse()?));
206        }
207        if let Some(v) = s.strip_prefix('<') {
208            return Ok(VersionReq::LessThan(v.trim().parse()?));
209        }
210        if let Some(v) = s.strip_prefix('=') {
211            return Ok(VersionReq::Exact(v.trim().parse()?));
212        }
213        if let Some(v) = s.strip_prefix('^') {
214            return Ok(VersionReq::Compatible(v.trim().parse()?));
215        }
216
217        // Default to compatible
218        Ok(VersionReq::Compatible(s.parse()?))
219    }
220}
221
222/// Tool information for registry
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct ToolInfo {
225    pub name: String,
226    pub version: Version,
227    pub installed_at: chrono::DateTime<chrono::Utc>,
228    pub source: ToolSource,
229    pub dependencies: HashMap<String, VersionReq>,
230}
231
232/// Source of a tool installation
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub enum ToolSource {
235    /// Local development
236    Local(PathBuf),
237    /// Published crate
238    Crate { version: String },
239    /// Git repository
240    Git { url: String, rev: String },
241    /// R2 storage
242    R2 { bucket: String, key: String },
243}
244
245/// DX Tool Version Registry
246///
247/// Manages installed tool versions, dependencies, and compatibility
248pub struct ToolRegistry {
249    registry_path: PathBuf,
250    tools: HashMap<String, ToolInfo>,
251}
252
253impl ToolRegistry {
254    /// Create or load a tool registry
255    pub fn new(forge_dir: &Path) -> Result<Self> {
256        let registry_path = forge_dir.join("tool_registry.json");
257
258        let tools = if registry_path.exists() {
259            let content = std::fs::read_to_string(&registry_path)
260                .context("Failed to read tool registry")?;
261            serde_json::from_str(&content).unwrap_or_default()
262        } else {
263            HashMap::new()
264        };
265
266        Ok(Self {
267            registry_path,
268            tools,
269        })
270    }
271
272    /// Register a new tool
273    pub fn register(
274        &mut self,
275        name: String,
276        version: Version,
277        source: ToolSource,
278        dependencies: HashMap<String, VersionReq>,
279    ) -> Result<()> {
280        let info = ToolInfo {
281            name: name.clone(),
282            version,
283            installed_at: chrono::Utc::now(),
284            source,
285            dependencies,
286        };
287
288        self.tools.insert(name, info);
289        self.save()?;
290
291        Ok(())
292    }
293
294    /// Get tool information
295    pub fn get(&self, name: &str) -> Option<&ToolInfo> {
296        self.tools.get(name)
297    }
298
299    /// Check if a tool is registered
300    pub fn is_registered(&self, name: &str) -> bool {
301        self.tools.contains_key(name)
302    }
303
304    /// Get tool version
305    pub fn version(&self, name: &str) -> Option<&Version> {
306        self.tools.get(name).map(|info| &info.version)
307    }
308
309    /// Check if all dependencies are satisfied
310    pub fn check_dependencies(&self, tool_name: &str) -> Result<Vec<String>> {
311        let mut missing = Vec::new();
312
313        if let Some(info) = self.tools.get(tool_name) {
314            for (dep_name, req) in &info.dependencies {
315                match self.tools.get(dep_name) {
316                    Some(dep_info) => {
317                        if !dep_info.version.satisfies(req) {
318                            missing.push(format!(
319                                "{} requires {} {}, but {} is installed",
320                                tool_name, dep_name, req, dep_info.version
321                            ));
322                        }
323                    }
324                    None => {
325                        missing.push(format!("{} requires {} {}", tool_name, dep_name, req));
326                    }
327                }
328            }
329        }
330
331        Ok(missing)
332    }
333
334    /// List all registered tools
335    pub fn list(&self) -> Vec<&ToolInfo> {
336        self.tools.values().collect()
337    }
338
339    /// Unregister a tool
340    pub fn unregister(&mut self, name: &str) -> Result<()> {
341        self.tools.remove(name);
342        self.save()?;
343        Ok(())
344    }
345
346    /// Check if an update is available
347    pub fn needs_update(&self, name: &str, latest: &Version) -> bool {
348        if let Some(info) = self.tools.get(name) {
349            &info.version < latest
350        } else {
351            false
352        }
353    }
354
355    /// Save registry to disk
356    fn save(&self) -> Result<()> {
357        let content = serde_json::to_string_pretty(&self.tools)?;
358        std::fs::write(&self.registry_path, content)?;
359        Ok(())
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366
367    #[test]
368    fn test_version_parsing() {
369        let v = "1.2.3".parse::<Version>().unwrap();
370        assert_eq!(v.major, 1);
371        assert_eq!(v.minor, 2);
372        assert_eq!(v.patch, 3);
373
374        let v = "v2.0.0-beta.1".parse::<Version>().unwrap();
375        assert_eq!(v.major, 2);
376        assert_eq!(v.pre_release, Some("beta.1".to_string()));
377
378        let v = "1.0.0+build.123".parse::<Version>().unwrap();
379        assert_eq!(v.build, Some("build.123".to_string()));
380    }
381
382    #[test]
383    fn test_version_comparison() {
384        let v1 = Version::new(1, 2, 3);
385        let v2 = Version::new(1, 2, 4);
386        let v3 = Version::new(2, 0, 0);
387
388        assert!(v1 < v2);
389        assert!(v2 < v3);
390        assert!(v1 < v3);
391    }
392
393    #[test]
394    fn test_version_compatibility() {
395        let v1 = Version::new(1, 2, 3);
396        let v2 = Version::new(1, 5, 0);
397        let v3 = Version::new(2, 0, 0);
398
399        assert!(v1.is_compatible_with(&v2));
400        assert!(!v1.is_compatible_with(&v3));
401    }
402
403    #[test]
404    fn test_version_requirements() {
405        let v = Version::new(1, 2, 3);
406
407        let req = "^1.0.0".parse::<VersionReq>().unwrap();
408        assert!(v.satisfies(&req));
409
410        let req = ">=1.2.0".parse::<VersionReq>().unwrap();
411        assert!(v.satisfies(&req));
412
413        let req = ">2.0.0".parse::<VersionReq>().unwrap();
414        assert!(!v.satisfies(&req));
415    }
416
417    #[test]
418    fn test_version_req_parsing() {
419        assert!(matches!(
420            "^1.2.3".parse::<VersionReq>().unwrap(),
421            VersionReq::Compatible(_)
422        ));
423        assert!(matches!(
424            ">=1.2.3".parse::<VersionReq>().unwrap(),
425            VersionReq::GreaterOrEqual(_)
426        ));
427        assert!(matches!(
428            "*".parse::<VersionReq>().unwrap(),
429            VersionReq::Any
430        ));
431    }
432}