use crate::error::{Area, VtaError, VtaResult};
use crate::types::ToolName;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum VersionReq {
Exact(String),
Prefix(String),
Latest,
Lts,
Channel(String),
Range(String),
System,
}
impl VersionReq {
pub fn parse(s: &str) -> VersionReq {
match s {
"latest" | "" => VersionReq::Latest,
"lts" => VersionReq::Lts,
"system" => VersionReq::System,
_ if s.starts_with(['^', '~', '>', '<', '=', '*']) => VersionReq::Range(s.to_string()),
_ if s.chars().next().is_some_and(|c| c.is_ascii_digit()) => {
if s.matches('.').count() >= 2 {
VersionReq::Exact(s.to_string())
} else {
VersionReq::Prefix(s.to_string())
}
}
_ => VersionReq::Channel(s.to_string()),
}
}
}
impl fmt::Display for VersionReq {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
VersionReq::Exact(s)
| VersionReq::Prefix(s)
| VersionReq::Channel(s)
| VersionReq::Range(s) => f.write_str(s),
VersionReq::Latest => f.write_str("latest"),
VersionReq::Lts => f.write_str("lts"),
VersionReq::System => f.write_str("system"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Request {
pub tool: ToolName,
pub version: VersionReq,
}
impl Request {
pub fn parse(s: &str) -> VtaResult<Request> {
let (tool, version) = match s.split_once('@') {
Some((t, v)) => (t, VersionReq::parse(v)),
None => (s, VersionReq::Latest),
};
if tool.is_empty() {
return Err(VtaError::new(
Area::Cfg,
4,
format!("empty tool name in request `{s}`"),
));
}
Ok(Request {
tool: tool.to_string(),
version,
})
}
}
impl fmt::Display for Request {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}@{}", self.tool, self.version)
}
}
#[cfg(test)]
mod fuzz {
use super::*;
proptest::proptest! {
#[test]
fn version_req_parse_never_panics(s in ".*") { let _ = VersionReq::parse(&s); }
#[test]
fn request_parse_never_panics(s in ".*") { let _ = Request::parse(&s); }
}
}