use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum CoordinateParseError {
#[error("coordinate field `{0}` must not be empty")]
EmptyField(&'static str),
#[error("coordinate field `{field}` contains illegal character `{ch}`")]
IllegalCharacter {
field: &'static str,
ch: char,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Coordinate {
pub group_id: String,
pub artifact_id: String,
pub version: String,
pub classifier: Option<String>,
pub extension: String,
}
impl Coordinate {
pub fn new_jar(
group_id: impl Into<String>,
artifact_id: impl Into<String>,
version: impl Into<String>,
) -> Result<Self, CoordinateParseError> {
Self::new(group_id, artifact_id, version, None::<String>, "jar")
}
pub fn new(
group_id: impl Into<String>,
artifact_id: impl Into<String>,
version: impl Into<String>,
classifier: Option<impl Into<String>>,
extension: impl Into<String>,
) -> Result<Self, CoordinateParseError> {
let group_id = group_id.into();
let artifact_id = artifact_id.into();
let version = version.into();
let classifier = classifier.map(Into::into);
let extension = extension.into();
validate_field("groupId", &group_id)?;
validate_field("artifactId", &artifact_id)?;
validate_field("version", &version)?;
validate_field("extension", &extension)?;
if let Some(ref c) = classifier {
validate_field("classifier", c)?;
}
Ok(Self {
group_id,
artifact_id,
version,
classifier,
extension,
})
}
#[must_use]
pub fn group_path(&self) -> String {
self.group_id.replace('.', "/")
}
#[must_use]
pub fn filename(&self) -> String {
match &self.classifier {
Some(c) => format!(
"{}-{}-{}.{}",
self.artifact_id, self.version, c, self.extension
),
None => format!("{}-{}.{}", self.artifact_id, self.version, self.extension),
}
}
#[must_use]
pub fn repository_path(&self) -> String {
format!(
"{}/{}/{}/{}",
self.group_path(),
self.artifact_id,
self.version,
self.filename()
)
}
}
fn validate_field(name: &'static str, value: &str) -> Result<(), CoordinateParseError> {
if value.is_empty() {
return Err(CoordinateParseError::EmptyField(name));
}
for ch in value.chars() {
if matches!(ch, '/' | '\\' | ':') {
return Err(CoordinateParseError::IllegalCharacter { field: name, ch });
}
}
Ok(())
}
impl fmt::Display for Coordinate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.classifier {
Some(c) => write!(
f,
"{}:{}:{}:{}:{}",
self.group_id, self.artifact_id, self.extension, c, self.version
),
None => write!(
f,
"{}:{}:{}:{}",
self.group_id, self.artifact_id, self.extension, self.version
),
}
}
}
#[cfg(test)]
mod tests {
use super::{Coordinate, CoordinateParseError};
#[test]
fn jar_filename_without_classifier() {
let c = Coordinate::new_jar("com.example", "foo", "1.2.3").expect("ok");
assert_eq!(c.filename(), "foo-1.2.3.jar");
}
#[test]
fn repository_path_dots_become_slashes() {
let c = Coordinate::new_jar("com.example.foo", "bar", "1.0").expect("ok");
assert_eq!(c.repository_path(), "com/example/foo/bar/1.0/bar-1.0.jar");
}
#[test]
fn classifier_is_inserted_before_extension() {
let c = Coordinate::new("com.example", "foo", "1.0", Some("sources"), "jar").expect("ok");
assert_eq!(c.filename(), "foo-1.0-sources.jar");
}
#[test]
fn empty_group_id_rejected() {
let err = Coordinate::new_jar("", "foo", "1.0").expect_err("reject");
assert!(matches!(err, CoordinateParseError::EmptyField("groupId")));
}
#[test]
fn slash_in_artifact_id_rejected() {
let err = Coordinate::new_jar("com.example", "foo/bar", "1.0").expect_err("reject");
assert!(matches!(
err,
CoordinateParseError::IllegalCharacter {
field: "artifactId",
ch: '/'
}
));
}
#[test]
fn display_uses_colon_form() {
let c = Coordinate::new_jar("com.example", "foo", "1.0").expect("ok");
assert_eq!(c.to_string(), "com.example:foo:jar:1.0");
}
}