mirror of
https://github.com/sergi0g/cup.git
synced 2025-11-08 05:03:49 -05:00
wip
This commit is contained in:
1343
Cargo.lock
generated
1343
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -6,5 +6,6 @@ edition = "2024"
|
||||
[lib]
|
||||
|
||||
[dependencies]
|
||||
bollard = "0.19.2"
|
||||
regex = "1.11.1"
|
||||
rustc-hash = "2.1.1"
|
||||
|
||||
51
cup/src/auth.rs
Normal file
51
cup/src/auth.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
pub struct AuthToken {
|
||||
token_type: TokenType,
|
||||
value: String,
|
||||
}
|
||||
|
||||
enum TokenType {
|
||||
Basic,
|
||||
Bearer,
|
||||
}
|
||||
|
||||
impl Display for TokenType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Basic => "Basic",
|
||||
Self::Bearer => "Bearer",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthToken {
|
||||
pub fn from_basic(token: &str) -> Self {
|
||||
Self {
|
||||
token_type: TokenType::Basic,
|
||||
value: token.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_bearer(token: &str) -> Self {
|
||||
Self {
|
||||
token_type: TokenType::Bearer,
|
||||
value: token.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_type(&self) -> &'static str {
|
||||
match &self.token_type {
|
||||
TokenType::Basic => "Basic",
|
||||
TokenType::Bearer => "Bearer"
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_value(&self) -> &str {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
10
cup/src/docker.rs
Normal file
10
cup/src/docker.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use crate::socket::Socket;
|
||||
|
||||
/// Wraps a Docker client and provides useful functions get data from it.
|
||||
pub struct Docker {
|
||||
socket: Socket,
|
||||
client: bollard::Docker
|
||||
}
|
||||
|
||||
impl Docker {
|
||||
}
|
||||
13
cup/src/image_config.rs
Normal file
13
cup/src/image_config.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use crate::version::{VersionType, standard::StandardUpdateType};
|
||||
|
||||
pub struct ImageConfig {
|
||||
version_type: VersionType,
|
||||
// Will only be read if update type is standard
|
||||
ignored_update_types: Vec<StandardUpdateType>,
|
||||
/// Whether to skip checking the image (default is false)
|
||||
ignore: bool
|
||||
}
|
||||
|
||||
impl ImageConfig {
|
||||
|
||||
}
|
||||
3
cup/src/image_to_check.rs
Normal file
3
cup/src/image_to_check.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub struct ImageToCheck {
|
||||
|
||||
}
|
||||
111
cup/src/lib.rs
111
cup/src/lib.rs
@@ -1 +1,110 @@
|
||||
pub mod version;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::{
|
||||
auth::AuthToken, image_config::ImageConfig, image_to_check::ImageToCheck, reference::Reference,
|
||||
reference_matcher::ReferenceMatcher, registry::Registry, registry_config::RegistryConfig,
|
||||
socket::Socket,
|
||||
};
|
||||
|
||||
pub mod auth;
|
||||
pub mod image_config;
|
||||
pub mod image_to_check;
|
||||
pub mod reference;
|
||||
pub mod reference_matcher;
|
||||
pub mod registry;
|
||||
pub mod registry_config;
|
||||
pub mod socket;
|
||||
pub mod version;
|
||||
pub mod docker;
|
||||
|
||||
/// This struct is the main interface to Cup's functions.
|
||||
/// Any useful user-provided or generated data is stored here for reuse during the lifetime of the struct.
|
||||
#[derive(Default)]
|
||||
pub struct Cup {
|
||||
images: Vec<ImageToCheck>,
|
||||
socket: Socket,
|
||||
}
|
||||
|
||||
impl<'a> Cup {
|
||||
/// Returns a new empty instance with all values initialized to their defaults.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn builder() -> CupBuilder {
|
||||
CupBuilder::default()
|
||||
}
|
||||
|
||||
/// Check for updates to the images provided (and any images configured through options)
|
||||
pub async fn check(images: &[&Reference]) -> Vec<Result<CheckResult, CheckError>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CupBuilder {
|
||||
/// Whether or not to check for updates to local images in addition to the other images.
|
||||
check_local_images: bool,
|
||||
/// Whether or not to check for updates to images used in the swarm in addition to the other images. If the host is not a manager node this will only check the locally running swarm images.
|
||||
check_swarm_images: bool,
|
||||
registry_config: FxHashMap<Registry, RegistryConfig>,
|
||||
overrides: FxHashMap<ReferenceMatcher, ImageConfig>,
|
||||
socket: Socket,
|
||||
}
|
||||
|
||||
impl CupBuilder {
|
||||
pub fn with_local_images(&mut self) {
|
||||
self.check_local_images = true
|
||||
}
|
||||
|
||||
pub fn with_swarm_images(&mut self) {
|
||||
self.check_swarm_images = true
|
||||
}
|
||||
|
||||
pub fn with_registry_auth(&mut self, registry: Registry, token: AuthToken) {
|
||||
match self.registry_config.get_mut(®istry) {
|
||||
Some(registry_config) => registry_config.auth = Some(token),
|
||||
None => {
|
||||
self.registry_config.insert(
|
||||
registry,
|
||||
RegistryConfig {
|
||||
auth: Some(token),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_insecure_registry(&mut self, registry: Registry) {
|
||||
match self.registry_config.get_mut(®istry) {
|
||||
Some(registry_config) => registry_config.insecure = true,
|
||||
None => {
|
||||
self.registry_config.insert(
|
||||
registry,
|
||||
RegistryConfig {
|
||||
insecure: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_image_config(&mut self, reference: ReferenceMatcher, config: ImageConfig) {
|
||||
self.overrides.insert(reference, config);
|
||||
}
|
||||
|
||||
pub fn with_socket(&mut self, socket: Socket) {
|
||||
self.socket = socket
|
||||
}
|
||||
|
||||
pub fn build(&self) -> Cup {
|
||||
|
||||
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CheckResult {}
|
||||
pub struct CheckError {}
|
||||
|
||||
5
cup/src/reference.rs
Normal file
5
cup/src/reference.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub struct Reference(String);
|
||||
|
||||
impl Reference {
|
||||
|
||||
}
|
||||
38
cup/src/reference_matcher.rs
Normal file
38
cup/src/reference_matcher.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use regex::Regex;
|
||||
use std::hash::Hash;
|
||||
|
||||
/// When customizing image configuration, there may be a need to operate on a batch of images. The ReferenceMatcher describes which references the configuration should apply to.
|
||||
pub enum ReferenceMatcher {
|
||||
/// The reference is exactly equal to the string provided
|
||||
ExactMatch(String),
|
||||
/// The reference starts with the string provided, useful for configuring based on a registry or repository
|
||||
PrefixMatch(String),
|
||||
/// The reference ends with the string provided, useful for configuring based on a tag
|
||||
SuffixMatch(String),
|
||||
/// The reference contains the string provided
|
||||
Contains(String),
|
||||
/// For more complicated matching logic a regular expression can be used.
|
||||
Custom(Regex),
|
||||
}
|
||||
|
||||
impl PartialEq for ReferenceMatcher {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(ReferenceMatcher::Custom(a), ReferenceMatcher::Custom(b)) => a.as_str() == b.as_str(),
|
||||
(a, b) => a.eq(b),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for ReferenceMatcher {}
|
||||
|
||||
impl Hash for ReferenceMatcher {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
match self {
|
||||
Self::Custom(r) => r.as_str().hash(state),
|
||||
o => o.hash(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ReferenceMatcher {}
|
||||
49
cup/src/registry.rs
Normal file
49
cup/src/registry.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use std::{error::Error, fmt::Display, str::FromStr, sync::OnceLock};
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
#[derive(PartialEq, Eq, Hash)]
|
||||
pub struct Registry {
|
||||
value: String,
|
||||
}
|
||||
|
||||
/// Regex representing the value of `domainAndPort` from https://github.com/distribution/reference/blob/727f80d42224f6696b8e1ad16b06aadf2c6b833b/regexp.go#L108
|
||||
/// Used to verify whether a string is a registry hostname
|
||||
const REGISTRY_REGEX: &str = r"^(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*|\[(?:[a-fA-F0-9:]+)\])(?::[0-9]+)?$";
|
||||
static REGISTRY: OnceLock<Regex> = OnceLock::new();
|
||||
|
||||
impl FromStr for Registry {
|
||||
type Err = ParseRegistryError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let regex = REGISTRY.get_or_init(|| Regex::new(REGISTRY_REGEX).unwrap());
|
||||
if s.is_empty() {
|
||||
Ok(Self {
|
||||
value: String::from("registry-1.docker.io"),
|
||||
})
|
||||
} else if regex.is_match(s) {
|
||||
Ok(Self {
|
||||
value: s.to_string(),
|
||||
})
|
||||
} else {
|
||||
Err(ParseRegistryError {
|
||||
registry: s.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParseRegistryError {
|
||||
pub registry: String,
|
||||
}
|
||||
|
||||
impl Display for ParseRegistryError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "`{}` is not a valid registry hostname", self.registry)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ParseRegistryError {}
|
||||
|
||||
// TODO: Add tests
|
||||
9
cup/src/registry_config.rs
Normal file
9
cup/src/registry_config.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use crate::auth::AuthToken;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RegistryConfig {
|
||||
/// An optional authentication token to use when making requests to this registry
|
||||
pub(super) auth: Option<AuthToken>,
|
||||
/// Whether or not to connect to the registry over unencrypted HTTP
|
||||
pub(super) insecure: bool
|
||||
}
|
||||
7
cup/src/socket.rs
Normal file
7
cup/src/socket.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
#[derive(Default)]
|
||||
pub enum Socket {
|
||||
#[default]
|
||||
Default,
|
||||
Unix(String),
|
||||
Tcp(String)
|
||||
}
|
||||
1
cup/src/version/digest.rs
Normal file
1
cup/src/version/digest.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub struct DigestVersion {}
|
||||
@@ -15,10 +15,10 @@ pub struct ExtendedVersion {
|
||||
}
|
||||
|
||||
impl ExtendedVersion {
|
||||
pub fn parse(version_string: &str, regex: &str) -> Result<Self, VersionParseError> {
|
||||
let regex = Regex::new(regex).map_err(|e| VersionParseError {
|
||||
pub fn parse(version_string: &str, regex: &str) -> Result<Self, ParseVersionError> {
|
||||
let regex = Regex::new(regex).map_err(|e| ParseVersionError {
|
||||
version_string: version_string.to_string(),
|
||||
kind: VersionParseErrorKind::InvalidRegex(e),
|
||||
kind: ParseVersionErrorKind::InvalidRegex(e),
|
||||
})?;
|
||||
|
||||
// Check if all capture groups are named or anonymous
|
||||
@@ -29,9 +29,9 @@ impl ExtendedVersion {
|
||||
match prev_state {
|
||||
None => Ok(Some(name)), // First iteration
|
||||
Some(prev_state) => match (prev_state, name) {
|
||||
(Some(_), None) | (None, Some(_)) => Err(VersionParseError {
|
||||
(Some(_), None) | (None, Some(_)) => Err(ParseVersionError {
|
||||
version_string: version_string.to_string(),
|
||||
kind: VersionParseErrorKind::NonUniformCaptureGroups,
|
||||
kind: ParseVersionErrorKind::NonUniformCaptureGroups,
|
||||
}),
|
||||
_ => Ok(Some(name)),
|
||||
},
|
||||
@@ -51,22 +51,22 @@ impl ExtendedVersion {
|
||||
.capture_names()
|
||||
.flatten()
|
||||
.map(|name| {
|
||||
let capture = captures.name(name).ok_or_else(|| VersionParseError {
|
||||
let capture = captures.name(name).ok_or_else(|| ParseVersionError {
|
||||
version_string: version_string.to_string(),
|
||||
kind: VersionParseErrorKind::GroupDidNotMatch(name.to_string()),
|
||||
kind: ParseVersionErrorKind::GroupDidNotMatch(name.to_string()),
|
||||
})?;
|
||||
let component =
|
||||
VersionComponent::from_str(capture.as_str()).map_err(|_| {
|
||||
VersionParseError {
|
||||
ParseVersionError {
|
||||
version_string: version_string.to_string(),
|
||||
kind: VersionParseErrorKind::GroupDidNotMatch(
|
||||
kind: ParseVersionErrorKind::GroupDidNotMatch(
|
||||
name.to_string(),
|
||||
),
|
||||
}
|
||||
})?;
|
||||
Ok((name.to_string(), component))
|
||||
})
|
||||
.collect::<Result<FxHashMap<String, VersionComponent>, VersionParseError>>(
|
||||
.collect::<Result<FxHashMap<String, VersionComponent>, ParseVersionError>>(
|
||||
)?
|
||||
} else {
|
||||
captures
|
||||
@@ -77,20 +77,20 @@ impl ExtendedVersion {
|
||||
.map(|(i, m)| {
|
||||
VersionComponent::from_str(m.as_str())
|
||||
.map(|comp| (i, comp))
|
||||
.map_err(|e| VersionParseError {
|
||||
.map_err(|e| ParseVersionError {
|
||||
version_string: version_string.to_string(),
|
||||
kind: VersionParseErrorKind::ParseComponent(e),
|
||||
kind: ParseVersionErrorKind::ParseComponent(e),
|
||||
})
|
||||
})
|
||||
.collect::<Result<FxHashMap<String, VersionComponent>, VersionParseError>>(
|
||||
.collect::<Result<FxHashMap<String, VersionComponent>, ParseVersionError>>(
|
||||
)?
|
||||
};
|
||||
Self { components }
|
||||
}
|
||||
None => {
|
||||
return Err(VersionParseError {
|
||||
return Err(ParseVersionError {
|
||||
version_string: version_string.to_string(),
|
||||
kind: VersionParseErrorKind::NoMatch,
|
||||
kind: ParseVersionErrorKind::NoMatch,
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -100,12 +100,12 @@ impl ExtendedVersion {
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[non_exhaustive]
|
||||
pub struct VersionParseError {
|
||||
pub struct ParseVersionError {
|
||||
version_string: String,
|
||||
kind: VersionParseErrorKind,
|
||||
kind: ParseVersionErrorKind,
|
||||
}
|
||||
|
||||
impl Display for VersionParseError {
|
||||
impl Display for ParseVersionError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
@@ -115,7 +115,7 @@ impl Display for VersionParseError {
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for VersionParseError {
|
||||
impl Error for ParseVersionError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
Some(&self.kind)
|
||||
}
|
||||
@@ -123,7 +123,7 @@ impl Error for VersionParseError {
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub enum VersionParseErrorKind {
|
||||
pub enum ParseVersionErrorKind {
|
||||
/// The regex supplied could not be compiled
|
||||
InvalidRegex(regex::Error),
|
||||
/// The regex supplied has both named and anonymous capture groups
|
||||
@@ -136,7 +136,7 @@ pub enum VersionParseErrorKind {
|
||||
ParseComponent(ParseIntError),
|
||||
}
|
||||
|
||||
impl Display for VersionParseErrorKind {
|
||||
impl Display for ParseVersionErrorKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::InvalidRegex(_) => write!(f, "invalid regex"),
|
||||
@@ -152,7 +152,7 @@ impl Display for VersionParseErrorKind {
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for VersionParseErrorKind {
|
||||
impl Error for ParseVersionErrorKind {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
match self {
|
||||
Self::InvalidRegex(e) => Some(e),
|
||||
@@ -172,8 +172,8 @@ mod tests {
|
||||
use crate::version::version_component::VersionComponent;
|
||||
|
||||
use super::ExtendedVersion;
|
||||
use super::VersionParseError;
|
||||
use super::VersionParseErrorKind;
|
||||
use super::ParseVersionError;
|
||||
use super::ParseVersionErrorKind;
|
||||
|
||||
#[test]
|
||||
fn parse_with_anonymous() {
|
||||
@@ -274,9 +274,9 @@ mod tests {
|
||||
"1.0.0-test2",
|
||||
r"(\d+)\.(\d+))\.(\d+)-test(\d+)" // Whoops, someone left an extra ) somewhere...
|
||||
),
|
||||
Err(VersionParseError {
|
||||
Err(ParseVersionError {
|
||||
version_string: String::from("1.0.0-test2"),
|
||||
kind: VersionParseErrorKind::InvalidRegex(
|
||||
kind: ParseVersionErrorKind::InvalidRegex(
|
||||
Regex::new(r"(\d+)\.(\d+))\.(\d+)-test(\d+)").unwrap_err()
|
||||
)
|
||||
})
|
||||
@@ -287,9 +287,9 @@ mod tests {
|
||||
fn invalid_component() {
|
||||
assert_eq!(
|
||||
ExtendedVersion::parse("50h+2-3_40", r"([a-z0-9]+)\+(\d+)-(\d+)_(\d+)"),
|
||||
Err(VersionParseError {
|
||||
Err(ParseVersionError {
|
||||
version_string: String::from("50h+2-3_40"),
|
||||
kind: VersionParseErrorKind::ParseComponent("50h".parse::<usize>().unwrap_err())
|
||||
kind: ParseVersionErrorKind::ParseComponent("50h".parse::<usize>().unwrap_err())
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -301,9 +301,9 @@ mod tests {
|
||||
"4.1.2.5",
|
||||
r"(?<Major>\d+)\.(?<Minor>\d+)\.(?<Patch>\d+)\.(\d+)"
|
||||
),
|
||||
Err(VersionParseError {
|
||||
Err(ParseVersionError {
|
||||
version_string: String::from("4.1.2.5"),
|
||||
kind: VersionParseErrorKind::NonUniformCaptureGroups
|
||||
kind: ParseVersionErrorKind::NonUniformCaptureGroups
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -315,9 +315,9 @@ mod tests {
|
||||
"1-sometool-0.2.5-alpine",
|
||||
r"(?:\d+)-somet0ol-(\d+)\.(\d+)\.(\d+)-alpine"
|
||||
),
|
||||
Err(VersionParseError {
|
||||
Err(ParseVersionError {
|
||||
version_string: String::from("1-sometool-0.2.5-alpine"),
|
||||
kind: VersionParseErrorKind::NoMatch
|
||||
kind: ParseVersionErrorKind::NoMatch
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -329,9 +329,9 @@ mod tests {
|
||||
"2.0.5-alpine",
|
||||
r"(?<Eenie>\d+)\.(?<Meenie>\d+)\.(?<Miney>\d+)|(?<Moe>moe)-alpine"
|
||||
),
|
||||
Err(VersionParseError {
|
||||
Err(ParseVersionError {
|
||||
version_string: String::from("2.0.5-alpine"),
|
||||
kind: VersionParseErrorKind::GroupDidNotMatch(String::from("Moe"))
|
||||
kind: ParseVersionErrorKind::GroupDidNotMatch(String::from("Moe"))
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use crate::version::date::DateVersion;
|
||||
use crate::version::digest::DigestVersion;
|
||||
use crate::version::extended::ExtendedVersion;
|
||||
use crate::version::standard::StandardVersion;
|
||||
|
||||
pub mod date;
|
||||
pub mod digest;
|
||||
pub mod extended;
|
||||
pub mod standard;
|
||||
|
||||
@@ -17,6 +19,17 @@ pub enum Version {
|
||||
Extended(ExtendedVersion),
|
||||
/// The "Date" versioning schema is for images which are versioned based on date and or time and come with their own quirky rules to compare them. Refer to `DateVersion` for more info.
|
||||
Date(DateVersion),
|
||||
/// Version checking based on local and remote image digests
|
||||
Digest(DigestVersion)
|
||||
}
|
||||
|
||||
pub enum VersionType {
|
||||
/// Tries to automatically infer the version type.
|
||||
Auto,
|
||||
Standard,
|
||||
Extended,
|
||||
Date,
|
||||
Digest
|
||||
}
|
||||
|
||||
mod version_component {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::{error::Error, fmt::Display, num::ParseIntError, str::FromStr};
|
||||
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use super::version_component::VersionComponent;
|
||||
|
||||
/// A versioning scheme I'd call SemVer-inspired. The main difference from [SemVer](https://semver.org) is that the minor and patch versions are optional.
|
||||
@@ -13,28 +15,28 @@ pub struct StandardVersion {
|
||||
}
|
||||
|
||||
impl FromStr for StandardVersion {
|
||||
type Err = VersionParseError;
|
||||
type Err = ParseVersionError;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let splits = s.split('.');
|
||||
if splits.clone().any(|split| split.is_empty()) {
|
||||
return Err(VersionParseError {
|
||||
return Err(ParseVersionError {
|
||||
version_string: s.to_string(),
|
||||
kind: VersionParseErrorKind::IncorrectFormat,
|
||||
kind: ParseVersionErrorKind::IncorrectFormat,
|
||||
});
|
||||
}
|
||||
let mut component_iter = splits.map(|component| {
|
||||
VersionComponent::from_str(component).map_err(|e| VersionParseError {
|
||||
VersionComponent::from_str(component).map_err(|e| ParseVersionError {
|
||||
version_string: s.to_string(),
|
||||
kind: VersionParseErrorKind::ParseComponent(e),
|
||||
kind: ParseVersionErrorKind::ParseComponent(e),
|
||||
})
|
||||
});
|
||||
let major = component_iter.next().transpose()?.unwrap();
|
||||
let minor = component_iter.next().transpose()?;
|
||||
let patch = component_iter.next().transpose()?;
|
||||
if component_iter.next().is_some() {
|
||||
return Err(VersionParseError {
|
||||
return Err(ParseVersionError {
|
||||
version_string: s.to_string(),
|
||||
kind: VersionParseErrorKind::TooManyComponents(4 + component_iter.count()),
|
||||
kind: ParseVersionErrorKind::TooManyComponents(4 + component_iter.count()),
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
@@ -45,15 +47,22 @@ impl FromStr for StandardVersion {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash)]
|
||||
pub enum StandardUpdateType {
|
||||
Major,
|
||||
Minor,
|
||||
Patch
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[non_exhaustive]
|
||||
pub struct VersionParseError {
|
||||
pub struct ParseVersionError {
|
||||
pub version_string: String,
|
||||
pub kind: VersionParseErrorKind,
|
||||
pub kind: ParseVersionErrorKind,
|
||||
}
|
||||
|
||||
impl Display for VersionParseError {
|
||||
impl Display for ParseVersionError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
@@ -63,7 +72,7 @@ impl Display for VersionParseError {
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for VersionParseError {
|
||||
impl Error for ParseVersionError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
Some(&self.kind)
|
||||
}
|
||||
@@ -71,7 +80,7 @@ impl Error for VersionParseError {
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub enum VersionParseErrorKind {
|
||||
pub enum ParseVersionErrorKind {
|
||||
/// A version component could not be parsed
|
||||
ParseComponent(ParseIntError),
|
||||
/// The version string is not in the format expected by `StandardVersion`
|
||||
@@ -80,7 +89,7 @@ pub enum VersionParseErrorKind {
|
||||
TooManyComponents(usize),
|
||||
}
|
||||
|
||||
impl Display for VersionParseErrorKind {
|
||||
impl Display for ParseVersionErrorKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match &self {
|
||||
Self::IncorrectFormat => write!(f, "version string is incorrectly formatted"),
|
||||
@@ -94,7 +103,7 @@ impl Display for VersionParseErrorKind {
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for VersionParseErrorKind {
|
||||
impl Error for ParseVersionErrorKind {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
match &self {
|
||||
Self::ParseComponent(e) => Some(e),
|
||||
@@ -108,7 +117,7 @@ impl Error for VersionParseErrorKind {
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::{StandardVersion, VersionParseError, VersionParseErrorKind};
|
||||
use super::{StandardVersion, ParseVersionError, ParseVersionErrorKind};
|
||||
use super::super::version_component::VersionComponent;
|
||||
|
||||
#[test]
|
||||
@@ -190,9 +199,9 @@ mod tests {
|
||||
fn parse_invalid_string() {
|
||||
assert_eq!(
|
||||
StandardVersion::from_str(".1.0"),
|
||||
Err(VersionParseError {
|
||||
Err(ParseVersionError {
|
||||
version_string: String::from(".1.0"),
|
||||
kind: VersionParseErrorKind::IncorrectFormat
|
||||
kind: ParseVersionErrorKind::IncorrectFormat
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -201,9 +210,9 @@ mod tests {
|
||||
fn parse_invalid_component() {
|
||||
assert_eq!(
|
||||
StandardVersion::from_str("0.1.O"),
|
||||
Err(VersionParseError {
|
||||
Err(ParseVersionError {
|
||||
version_string: String::from("0.1.O"),
|
||||
kind: VersionParseErrorKind::ParseComponent(
|
||||
kind: ParseVersionErrorKind::ParseComponent(
|
||||
"O".parse::<u32>().unwrap_err()
|
||||
)
|
||||
})
|
||||
@@ -214,9 +223,9 @@ mod tests {
|
||||
fn parse_four_components() {
|
||||
assert_eq!(
|
||||
StandardVersion::from_str("1.2.4.0"),
|
||||
Err(VersionParseError {
|
||||
Err(ParseVersionError {
|
||||
version_string: String::from("1.2.4.0"),
|
||||
kind: VersionParseErrorKind::TooManyComponents(4)
|
||||
kind: ParseVersionErrorKind::TooManyComponents(4)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user