1use crate::error::{Area, VtaError, VtaResult};
7use crate::types::ToolName;
8use std::fmt;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum VersionReq {
14 Exact(String),
16 Prefix(String),
18 Latest,
20 Lts,
22 Channel(String),
24 Range(String),
26 System,
28}
29
30impl VersionReq {
31 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#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct Request {
68 pub tool: ToolName,
69 pub version: VersionReq,
70}
71
72impl Request {
73 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}