Skip to main content

vanta_core/
version.rs

1//! Version requests and tool requests (the `name@version` surface).
2//!
3//! See `docs/06-resolution.md` for the request grammar. Parsing lives here;
4//! version ordering and constraint satisfaction live in `vanta-resolve`.
5
6use crate::error::{Area, VtaError, VtaResult};
7use crate::types::ToolName;
8use std::fmt;
9
10/// A version request as written in `vanta.toml` or on the CLI.
11#[derive(Debug, Clone, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum VersionReq {
14    /// A fully-specified version, e.g. `24.3.0`.
15    Exact(String),
16    /// A prefix match, e.g. `24` or `24.3`.
17    Prefix(String),
18    /// The newest stable version.
19    Latest,
20    /// The newest long-term-support version (provider-defined).
21    Lts,
22    /// A named channel, e.g. `stable` or `nightly`.
23    Channel(String),
24    /// A SemVer range, e.g. `^24` or `>=20 <24`.
25    Range(String),
26    /// Use a system-provided tool.
27    System,
28}
29
30impl VersionReq {
31    /// Parse the version portion of a request. This never fails; an unrecognized
32    /// token is treated as a channel name (resolution decides if it exists).
33    pub fn parse(s: &str) -> VersionReq {
34        match s {
35            "latest" | "" => VersionReq::Latest,
36            "lts" => VersionReq::Lts,
37            "system" => VersionReq::System,
38            _ if s.starts_with(['^', '~', '>', '<', '=', '*']) => VersionReq::Range(s.to_string()),
39            _ if s.chars().next().is_some_and(|c| c.is_ascii_digit()) => {
40                if s.matches('.').count() >= 2 {
41                    VersionReq::Exact(s.to_string())
42                } else {
43                    VersionReq::Prefix(s.to_string())
44                }
45            }
46            _ => VersionReq::Channel(s.to_string()),
47        }
48    }
49}
50
51impl fmt::Display for VersionReq {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        match self {
54            VersionReq::Exact(s)
55            | VersionReq::Prefix(s)
56            | VersionReq::Channel(s)
57            | VersionReq::Range(s) => f.write_str(s),
58            VersionReq::Latest => f.write_str("latest"),
59            VersionReq::Lts => f.write_str("lts"),
60            VersionReq::System => f.write_str("system"),
61        }
62    }
63}
64
65/// A tool request: a tool name plus a version request.
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct Request {
68    pub tool: ToolName,
69    pub version: VersionReq,
70}
71
72impl Request {
73    /// Parse a `name[@version]` request. A missing version means [`VersionReq::Latest`].
74    pub fn parse(s: &str) -> VtaResult<Request> {
75        let (tool, version) = match s.split_once('@') {
76            Some((t, v)) => (t, VersionReq::parse(v)),
77            None => (s, VersionReq::Latest),
78        };
79        if tool.is_empty() {
80            return Err(VtaError::new(
81                Area::Cfg,
82                4,
83                format!("empty tool name in request `{s}`"),
84            ));
85        }
86        Ok(Request {
87            tool: tool.to_string(),
88            version,
89        })
90    }
91}
92
93impl fmt::Display for Request {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        write!(f, "{}@{}", self.tool, self.version)
96    }
97}
98
99#[cfg(test)]
100mod fuzz {
101    use super::*;
102    proptest::proptest! {
103        #[test]
104        fn version_req_parse_never_panics(s in ".*") { let _ = VersionReq::parse(&s); }
105        #[test]
106        fn request_parse_never_panics(s in ".*") { let _ = Request::parse(&s); }
107    }
108}