use log;
use std::str::FromStr;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Version {
pub major: u64,
pub minor: u64,
pub patch: u64,
}
#[derive(Debug, Clone, Copy)]
pub enum Bump {
Major(u64),
Minor(u64),
Patch(u64),
}
impl Version {
const CORE_VERSION_SEPARATOR: char = '.';
pub fn bump(&self, bumpspec: Bump) -> Self {
match bumpspec {
Bump::Major(diff) => {
Self { major: self.major + diff, minor: 0, patch: 0 }
},
Bump::Minor(diff) => {
Self { minor: self.minor + diff, patch: 0, ..*self }
},
Bump::Patch(diff) => Self { patch: self.patch + diff, ..*self },
}
}
}
#[derive(Error, Debug)]
pub enum ParseError {
#[error("separator '{separator}' could not be found")]
NoSeparatorFound { separator: char },
#[error("could not parse number (non-digit character found)")]
InvalidNumber,
}
enum ParseState<'parser> {
Trim { string: &'parser str },
Major { string: &'parser str },
Minor { string: &'parser str, major: u64 },
Patch { string: &'parser str, major: u64, minor: u64 },
Finished { major: u64, minor: u64, patch: u64 },
}
pub fn parse(string: &str) -> Result<Version, ParseError> {
log::trace!("Parsing semantic version from {string:?}.");
let mut parse_state = ParseState::Trim { string };
loop {
match parse_state {
ParseState::Trim { string } => {
parse_state = ParseState::Major { string: string.trim() }
},
ParseState::Major { string } => {
let (major, rest) = parse_number_to_separator(
string,
Version::CORE_VERSION_SEPARATOR,
)?;
parse_state = ParseState::Minor { string: rest, major };
},
ParseState::Minor { string, major } => {
let (minor, rest) = parse_number_to_separator(
string,
Version::CORE_VERSION_SEPARATOR,
)?;
parse_state = ParseState::Patch { string: rest, major, minor };
},
ParseState::Patch { string, major, minor } => {
let patch = parse_number(string)?;
parse_state = ParseState::Finished { major, minor, patch };
},
ParseState::Finished { major, minor, patch } => {
return Ok(Version { major, minor, patch });
},
}
}
}
fn parse_number<T>(string: &str) -> Result<T, ParseError>
where
T: FromStr,
{
log::trace!("Parsing number from {string:?}.");
if string.len() > 1 && string.starts_with('0') {
return Err(ParseError::InvalidNumber);
}
let Ok(number) = string.parse::<T>() else {
log::error!("Could not parse number from {string:?}.");
return Err(ParseError::InvalidNumber);
};
Ok(number)
}
fn parse_number_to_separator<T>(
string: &str,
separator: char,
) -> Result<(T, &str), ParseError>
where
T: FromStr,
{
log::trace!("Parsing number from {string:?} until dot.");
let Some((maybe_number, rest)) = string.split_once(separator) else {
log::error!("Could not find dot separator between version parts.");
return Err(ParseError::NoSeparatorFound { separator });
};
log::trace!(
"Parsed number into ({maybe_number:?}, {rest:?}) at dot separator."
);
let number: T = parse_number(maybe_number)?;
Ok((number, rest))
}