mirror of
https://github.com/sergi0g/cup.git
synced 2025-11-17 01:23:39 -05:00
feat: implement extended version parsing
This commit is contained in:
49
Cargo.lock
generated
49
Cargo.lock
generated
@@ -3,10 +3,20 @@
|
|||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cup"
|
name = "cup"
|
||||||
version = "4.0.0"
|
version = "4.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"regex",
|
||||||
|
"rustc-hash",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -18,3 +28,42 @@ name = "cup_server"
|
|||||||
version = "4.0.0"
|
version = "4.0.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hash"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||||
|
|||||||
@@ -6,3 +6,5 @@ edition = "2024"
|
|||||||
[lib]
|
[lib]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
regex = "1.11.1"
|
||||||
|
rustc-hash = "2.1.1"
|
||||||
|
|||||||
@@ -1 +1,340 @@
|
|||||||
pub struct ExtendedVersion {}
|
use std::num::ParseIntError;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::{error::Error, fmt::Display};
|
||||||
|
|
||||||
|
use regex::Regex;
|
||||||
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
|
use crate::version::version_component::VersionComponent;
|
||||||
|
|
||||||
|
/// This doesn't describe a specific versioning scheme, but instead aims to provide the utilites for parsing a version string not covered by any of the other parsers in the crate.
|
||||||
|
/// Takes a regex with a capture group for each component (either _all_ named or anonymous)
|
||||||
|
#[cfg_attr(test, derive(Debug, PartialEq))]
|
||||||
|
pub struct ExtendedVersion {
|
||||||
|
components: FxHashMap<String, VersionComponent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtendedVersion {
|
||||||
|
pub fn parse(version_string: &str, regex: &str) -> Result<Self, VersionParseError> {
|
||||||
|
let regex = Regex::new(regex).map_err(|e| VersionParseError {
|
||||||
|
version_string: version_string.to_string(),
|
||||||
|
kind: VersionParseErrorKind::InvalidRegex(e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Check if all capture groups are named or anonymous
|
||||||
|
let is_named = match regex
|
||||||
|
.capture_names()
|
||||||
|
.skip(1) // The first group will always be the implicit anonymous group for the whole regex
|
||||||
|
.try_fold(None, |prev_state, name| {
|
||||||
|
match prev_state {
|
||||||
|
None => Ok(Some(name)), // First iteration
|
||||||
|
Some(prev_state) => match (prev_state, name) {
|
||||||
|
(Some(_), None) | (None, Some(_)) => Err(VersionParseError {
|
||||||
|
version_string: version_string.to_string(),
|
||||||
|
kind: VersionParseErrorKind::NonUniformCaptureGroups,
|
||||||
|
}),
|
||||||
|
_ => Ok(Some(name)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Ok(Some(Some(_))) => true,
|
||||||
|
Ok(Some(None)) => false,
|
||||||
|
Ok(None) => false,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse the version string
|
||||||
|
Ok(match regex.captures(version_string) {
|
||||||
|
Some(captures) => {
|
||||||
|
let components = if is_named {
|
||||||
|
regex
|
||||||
|
.capture_names()
|
||||||
|
.flatten()
|
||||||
|
.map(|name| {
|
||||||
|
let capture = captures.name(name).ok_or_else(|| VersionParseError {
|
||||||
|
version_string: version_string.to_string(),
|
||||||
|
kind: VersionParseErrorKind::GroupDidNotMatch(name.to_string()),
|
||||||
|
})?;
|
||||||
|
let component =
|
||||||
|
VersionComponent::from_str(capture.as_str()).map_err(|_| {
|
||||||
|
VersionParseError {
|
||||||
|
version_string: version_string.to_string(),
|
||||||
|
kind: VersionParseErrorKind::GroupDidNotMatch(
|
||||||
|
name.to_string(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
Ok((name.to_string(), component))
|
||||||
|
})
|
||||||
|
.collect::<Result<FxHashMap<String, VersionComponent>, VersionParseError>>(
|
||||||
|
)?
|
||||||
|
} else {
|
||||||
|
captures
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.skip(1) // skip group 0 (whole match)
|
||||||
|
.filter_map(|(i, m)| m.map(|mat| (i.to_string(), mat)))
|
||||||
|
.map(|(i, m)| {
|
||||||
|
VersionComponent::from_str(m.as_str())
|
||||||
|
.map(|comp| (i, comp))
|
||||||
|
.map_err(|e| VersionParseError {
|
||||||
|
version_string: version_string.to_string(),
|
||||||
|
kind: VersionParseErrorKind::ParseComponent(e),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<FxHashMap<String, VersionComponent>, VersionParseError>>(
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
Self { components }
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
return Err(VersionParseError {
|
||||||
|
version_string: version_string.to_string(),
|
||||||
|
kind: VersionParseErrorKind::NoMatch,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[cfg_attr(test, derive(PartialEq))]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct VersionParseError {
|
||||||
|
version_string: String,
|
||||||
|
kind: VersionParseErrorKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for VersionParseError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"failed to parse `{}` as extended version",
|
||||||
|
self.version_string
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for VersionParseError {
|
||||||
|
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||||
|
Some(&self.kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[cfg_attr(test, derive(PartialEq))]
|
||||||
|
pub enum VersionParseErrorKind {
|
||||||
|
/// The regex supplied could not be compiled
|
||||||
|
InvalidRegex(regex::Error),
|
||||||
|
/// The regex supplied has both named and anonymous capture groups
|
||||||
|
NonUniformCaptureGroups,
|
||||||
|
/// The version string did not match the regex supplied
|
||||||
|
NoMatch,
|
||||||
|
/// A named group had no matches
|
||||||
|
GroupDidNotMatch(String),
|
||||||
|
/// A version component could not be parsed
|
||||||
|
ParseComponent(ParseIntError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for VersionParseErrorKind {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::InvalidRegex(_) => write!(f, "invalid regex"),
|
||||||
|
Self::NonUniformCaptureGroups => {
|
||||||
|
write!(f, "regex has both named and anonymous capture groups")
|
||||||
|
}
|
||||||
|
Self::NoMatch => write!(f, "version string did not match the regex"),
|
||||||
|
Self::GroupDidNotMatch(name) => {
|
||||||
|
write!(f, "named group `{}` did not match", name)
|
||||||
|
}
|
||||||
|
Self::ParseComponent(_) => write!(f, "version component is not a valid integer"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for VersionParseErrorKind {
|
||||||
|
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
Self::InvalidRegex(e) => Some(e),
|
||||||
|
Self::ParseComponent(e) => Some(e),
|
||||||
|
Self::GroupDidNotMatch(_) => None,
|
||||||
|
Self::NoMatch => None,
|
||||||
|
Self::NonUniformCaptureGroups => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use regex::Regex;
|
||||||
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
|
use crate::version::version_component::VersionComponent;
|
||||||
|
|
||||||
|
use super::ExtendedVersion;
|
||||||
|
use super::VersionParseError;
|
||||||
|
use super::VersionParseErrorKind;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_with_anonymous() {
|
||||||
|
assert_eq!(
|
||||||
|
ExtendedVersion::parse("24.04.11.2.1", r"(\d+)\.(\d+)\.(\d+)\.(\d+)\.(\d+)"),
|
||||||
|
Ok(ExtendedVersion {
|
||||||
|
components: {
|
||||||
|
let mut map = FxHashMap::default();
|
||||||
|
map.insert(
|
||||||
|
"1".to_string(),
|
||||||
|
VersionComponent {
|
||||||
|
value: 24,
|
||||||
|
length: 2,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
map.insert(
|
||||||
|
"2".to_string(),
|
||||||
|
VersionComponent {
|
||||||
|
value: 4,
|
||||||
|
length: 2,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
map.insert(
|
||||||
|
"3".to_string(),
|
||||||
|
VersionComponent {
|
||||||
|
value: 11,
|
||||||
|
length: 2,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
map.insert(
|
||||||
|
"4".to_string(),
|
||||||
|
VersionComponent {
|
||||||
|
value: 2,
|
||||||
|
length: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
map.insert(
|
||||||
|
"5".to_string(),
|
||||||
|
VersionComponent {
|
||||||
|
value: 1,
|
||||||
|
length: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
map
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_with_named() {
|
||||||
|
assert_eq!(
|
||||||
|
ExtendedVersion::parse(
|
||||||
|
"v0.7.1-ls84",
|
||||||
|
r"v(?<Alpha>\d+)\.(?<Beta>\d+)\.(?<Gamma>\d+)-ls(?<Delta>\d+)"
|
||||||
|
),
|
||||||
|
Ok(ExtendedVersion {
|
||||||
|
components: {
|
||||||
|
let mut map = FxHashMap::default();
|
||||||
|
map.insert(
|
||||||
|
"Alpha".to_string(),
|
||||||
|
VersionComponent {
|
||||||
|
value: 0,
|
||||||
|
length: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
map.insert(
|
||||||
|
"Beta".to_string(),
|
||||||
|
VersionComponent {
|
||||||
|
value: 7,
|
||||||
|
length: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
map.insert(
|
||||||
|
"Gamma".to_string(),
|
||||||
|
VersionComponent {
|
||||||
|
value: 1,
|
||||||
|
length: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
map.insert(
|
||||||
|
"Delta".to_string(),
|
||||||
|
VersionComponent {
|
||||||
|
value: 84,
|
||||||
|
length: 2,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
map
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_regex() {
|
||||||
|
assert_eq!(
|
||||||
|
ExtendedVersion::parse(
|
||||||
|
"1.0.0-test2",
|
||||||
|
r"(\d+)\.(\d+))\.(\d+)-test(\d+)" // Whoops, someone left an extra ) somewhere...
|
||||||
|
),
|
||||||
|
Err(VersionParseError {
|
||||||
|
version_string: String::from("1.0.0-test2"),
|
||||||
|
kind: VersionParseErrorKind::InvalidRegex(
|
||||||
|
Regex::new(r"(\d+)\.(\d+))\.(\d+)-test(\d+)").unwrap_err()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_component() {
|
||||||
|
assert_eq!(
|
||||||
|
ExtendedVersion::parse("50h+2-3_40", r"([a-z0-9]+)\+(\d+)-(\d+)_(\d+)"),
|
||||||
|
Err(VersionParseError {
|
||||||
|
version_string: String::from("50h+2-3_40"),
|
||||||
|
kind: VersionParseErrorKind::ParseComponent("50h".parse::<usize>().unwrap_err())
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_uniform_groups() {
|
||||||
|
assert_eq!(
|
||||||
|
ExtendedVersion::parse(
|
||||||
|
"4.1.2.5",
|
||||||
|
r"(?<Major>\d+)\.(?<Minor>\d+)\.(?<Patch>\d+)\.(\d+)"
|
||||||
|
),
|
||||||
|
Err(VersionParseError {
|
||||||
|
version_string: String::from("4.1.2.5"),
|
||||||
|
kind: VersionParseErrorKind::NonUniformCaptureGroups
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_match() {
|
||||||
|
assert_eq!(
|
||||||
|
ExtendedVersion::parse(
|
||||||
|
"1-sometool-0.2.5-alpine",
|
||||||
|
r"(?:\d+)-somet0ol-(\d+)\.(\d+)\.(\d+)-alpine"
|
||||||
|
),
|
||||||
|
Err(VersionParseError {
|
||||||
|
version_string: String::from("1-sometool-0.2.5-alpine"),
|
||||||
|
kind: VersionParseErrorKind::NoMatch
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_group_match() {
|
||||||
|
assert_eq!(
|
||||||
|
ExtendedVersion::parse(
|
||||||
|
"2.0.5-alpine",
|
||||||
|
r"(?<Eenie>\d+)\.(?<Meenie>\d+)\.(?<Miney>\d+)|(?<Moe>moe)-alpine"
|
||||||
|
),
|
||||||
|
Err(VersionParseError {
|
||||||
|
version_string: String::from("2.0.5-alpine"),
|
||||||
|
kind: VersionParseErrorKind::GroupDidNotMatch(String::from("Moe"))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Maybe check that if an anonymous group does not match everything is fine
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user