use std::fmt;
use std::str::FromStr;
use hiero_sdk_proto::services;
use crate::protobuf::{
FromProtobuf,
ToProtobuf,
};
use crate::Error;
#[cfg(test)]
mod tests;
#[derive(Debug, Clone)]
pub struct SemanticVersion {
pub major: u32,
pub minor: u32,
pub patch: u32,
pub prerelease: String,
pub build: String,
}
impl fmt::Display for SemanticVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
if !self.prerelease.is_empty() {
write!(f, "-{}", &self.prerelease)?;
}
if !self.build.is_empty() {
write!(f, "+{}", &self.build)?;
}
Ok(())
}
}
fn is_valid_ident_char(ch: char) -> bool {
ch.is_ascii_alphanumeric() || ch == '-'
}
fn is_numeric_with_leading_zero(s: &str) -> bool {
s.strip_prefix('0')
.map_or(false, |rest| !rest.is_empty() && rest.chars().all(|it| it.is_ascii_digit()))
}
fn parse_version(s: &str, name: &str) -> crate::Result<u32> {
if is_numeric_with_leading_zero(s) {
return Err(Error::basic_parse(format!(
"semver section `{name}` starts with leading 0: `{s}`"
)));
}
s.parse().map_err(Error::basic_parse)
}
fn parse_prerelease(s: &str) -> crate::Result<String> {
if s.is_empty() {
return Err(Error::basic_parse("semver with empty rerelease"));
}
for identifier in s.split('.') {
if identifier.is_empty() {
return Err(Error::basic_parse("semver with empty -pre identifier"));
}
if !identifier.chars().all(is_valid_ident_char) {
return Err(Error::basic_parse(
"semver with invalid identifier for the -pre section: `{identifier}`",
));
}
if is_numeric_with_leading_zero(identifier) {
return Err(Error::basic_parse(
"numeric pre-release identifier has leading zero: `{identifier}`",
));
}
}
Ok(s.to_owned())
}
fn parse_build(s: &str) -> crate::Result<String> {
if s.is_empty() {
return Err(Error::basic_parse("semver with empty build"));
}
for identifier in s.split('.') {
if identifier.is_empty() {
return Err(Error::basic_parse("semver with empty build-section identifier"));
}
if !identifier.chars().all(is_valid_ident_char) {
return Err(Error::basic_parse(
"semver with invalid identifier for the build section: `{identifier}",
));
}
}
Ok(s.to_owned())
}
impl FromStr for SemanticVersion {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parts: Vec<&str> = s.splitn(3, '.').collect();
match &*parts {
[major, minor, rest] => {
let (patch, pre, build) = {
let (rest, build) = match rest.split_once('+') {
Some((rest, build)) => (rest, Some(build)),
None => (*rest, None),
};
let (patch, pre) = match rest.split_once('-') {
Some((patch, pre)) => (patch, Some(pre)),
None => (rest, None),
};
(patch, pre, build)
};
let major = parse_version(major, "major")?;
let minor = parse_version(minor, "minor")?;
let patch = parse_version(patch, "patch")?;
let prerelease = match pre {
Some(it) => parse_prerelease(it)?,
None => String::new(),
};
let build = match build {
Some(it) => parse_build(it)?,
None => String::new(),
};
Ok(Self { major, minor, patch, prerelease, build })
}
_ => Err(Error::basic_parse("expected major.minor.patch for semver")),
}
}
}
impl SemanticVersion {
pub fn from_bytes(bytes: &[u8]) -> crate::Result<Self> {
FromProtobuf::from_bytes(bytes)
}
#[must_use]
pub fn to_bytes(&self) -> Vec<u8> {
ToProtobuf::to_bytes(self)
}
}
impl FromProtobuf<services::SemanticVersion> for SemanticVersion {
fn from_protobuf(pb: services::SemanticVersion) -> crate::Result<Self>
where
Self: Sized,
{
Ok(Self {
major: pb.major as u32,
minor: pb.minor as u32,
patch: pb.patch as u32,
prerelease: pb.pre,
build: pb.build,
})
}
}
impl ToProtobuf for SemanticVersion {
type Protobuf = services::SemanticVersion;
fn to_protobuf(&self) -> Self::Protobuf {
Self::Protobuf {
major: self.major as i32,
minor: self.minor as i32,
patch: self.patch as i32,
pre: self.prerelease.clone(),
build: self.build.clone(),
}
}
}