use std::convert::TryFrom;
use std::fmt;
use std::str::FromStr;
use sha2::{Digest, Sha256};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ParseError {
#[error("Invalid bindle ID '{0}'. A bindle ID should be NAME/VERSION")]
InvalidId(String),
#[error("Version {0} is not a valid semantic version (e.g. 1.2.3)")]
InvalidSemver(String),
}
type Result<T> = std::result::Result<T, ParseError>;
const PATH_SEPARATOR: char = '/';
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Hash, PartialEq, Eq)]
pub struct Id {
name: String,
version: semver::Version,
}
impl Id {
pub fn name(&self) -> &str {
&self.name
}
pub fn version(&self) -> &semver::Version {
&self.version
}
pub fn version_string(&self) -> String {
self.version.to_string()
}
pub fn sha(&self) -> String {
let mut hasher = Sha256::new();
hasher.update(&self.name);
hasher.update("/");
hasher.update(self.version_string());
let result = hasher.finalize();
format!("{:x}", result)
}
}
impl fmt::Display for Id {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
format_args!("{}{}{}", self.name, PATH_SEPARATOR, self.version).fmt(f)
}
}
impl FromStr for Id {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self> {
let last_separator_index = match s.rfind(PATH_SEPARATOR) {
Some(i) => i,
None => return Err(ParseError::InvalidId(s.to_owned())),
};
let (name_part, version_part) = s.split_at(last_separator_index);
let version_part = version_part.trim_start_matches(PATH_SEPARATOR);
if name_part.is_empty() || version_part.is_empty() {
let msg = format!("name: '{}', version: '{}'", name_part, version_part);
return Err(ParseError::InvalidId(msg));
}
let version = version_part
.parse()
.map_err(|_| ParseError::InvalidSemver(version_part.to_owned()))?;
Ok(Id {
name: name_part.to_owned(),
version,
})
}
}
impl From<&Id> for Id {
fn from(id: &Id) -> Self {
id.to_owned()
}
}
impl TryFrom<String> for Id {
type Error = ParseError;
fn try_from(value: String) -> Result<Self> {
value.parse()
}
}
impl TryFrom<&String> for Id {
type Error = ParseError;
fn try_from(value: &String) -> Result<Self> {
value.parse()
}
}
impl TryFrom<&str> for Id {
type Error = ParseError;
fn try_from(value: &str) -> Result<Self> {
value.parse()
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_id_parsing() {
Id::from_str("foo/1.0.0").expect("Should parse simple ID");
Id::from_str("example.com/foo/1.0.0").expect("Should parse namespaced ID");
Id::from_str("example.com/a/long/path/foo/1.0.0").expect("Should parse long ID");
Id::from_str("example.com/foo/1.0.0-rc.1").expect("Should parse RC version ID");
assert!(
Id::from_str("foo/").is_err(),
"Missing version should fail parsing"
);
assert!(
Id::from_str("1.0.0").is_err(),
"Missing name should fail parsing"
);
}
}