use thiserror::Error;
use crate::artifact_name::{ArtifactName, ArtifactNameError};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ArtifactRef {
Name(ArtifactName),
GitHub {
owner: String,
repo: String,
tag: String,
},
}
impl ArtifactRef {
pub fn parse(input: &str) -> Result<Self, ArtifactRefError> {
let value = input.trim();
if value.is_empty() {
return Err(ArtifactRefError::Empty);
}
if let Some(rest) = value.strip_prefix("github:") {
return parse_github(rest);
}
if value.contains(':') {
let prefix = value.split(':').next().unwrap_or_default();
return Err(ArtifactRefError::UnknownSourcePrefix(prefix.to_string()));
}
ArtifactName::parse(value)
.map(Self::Name)
.map_err(ArtifactRefError::Name)
}
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ArtifactRefError {
#[error("artifact reference cannot be empty")]
Empty,
#[error("invalid github artifact reference '{0}' (expected github:<owner>/<repo>@<tag>)")]
InvalidGitHub(String),
#[error("unsupported artifact source prefix '{0}'")]
UnknownSourcePrefix(String),
#[error(transparent)]
Name(ArtifactNameError),
}
fn parse_github(rest: &str) -> Result<ArtifactRef, ArtifactRefError> {
let Some((repo_ref, tag)) = rest.split_once('@') else {
return Err(ArtifactRefError::InvalidGitHub(format!("github:{rest}")));
};
let Some((owner, repo)) = repo_ref.split_once('/') else {
return Err(ArtifactRefError::InvalidGitHub(format!("github:{rest}")));
};
if owner.trim().is_empty() || repo.trim().is_empty() || tag.trim().is_empty() {
return Err(ArtifactRefError::InvalidGitHub(format!("github:{rest}")));
}
Ok(ArtifactRef::GitHub {
owner: owner.to_string(),
repo: repo.to_string(),
tag: tag.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn artifact_ref_parses_a_full_name() {
assert_eq!(
ArtifactRef::parse("murmur/driver-anthropic").unwrap(),
ArtifactRef::Name(ArtifactName::parse("murmur/driver-anthropic").unwrap())
);
}
#[test]
fn artifact_ref_refuses_a_bare_name_with_the_scoped_hint() {
let error = ArtifactRef::parse("murmur-driver-anthropic").unwrap_err();
let ArtifactRefError::Name(name_error) = error else {
panic!("expected a name error, got {error:?}");
};
assert!(name_error
.hint()
.unwrap()
.contains("murmur/driver-anthropic"));
}
#[test]
fn artifact_ref_parses_github_uri() {
assert_eq!(
ArtifactRef::parse("github:murmur-nexus/default-artifacts@v0.1.1").unwrap(),
ArtifactRef::GitHub {
owner: "murmur-nexus".to_string(),
repo: "default-artifacts".to_string(),
tag: "v0.1.1".to_string(),
}
);
}
#[test]
fn artifact_ref_rejects_malformed_github_uri() {
let error = ArtifactRef::parse("github:murmur-nexus/default-artifacts").unwrap_err();
assert!(matches!(error, ArtifactRefError::InvalidGitHub(_)));
}
}