use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug, schemars::JsonSchema, Hash)]
pub struct WebcIdent {
#[serde(skip_serializing_if = "Option::is_none")]
pub repository: Option<url::Url>,
pub namespace: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tag: Option<String>,
}
impl WebcIdent {
pub fn build_identifier(&self) -> String {
let mut ident = format!("{}/{}", self.namespace, self.name);
if let Some(tag) = &self.tag {
ident.push('@');
ident.push_str(tag);
}
ident
}
pub fn build_download_url(&self) -> Option<url::Url> {
let mut url = self.repository.as_ref()?.clone();
let ident = self.build_identifier();
let original_path = url.path().strip_suffix('/').unwrap_or(url.path());
let final_path = format!("{original_path}/{ident}");
url.set_path(&final_path);
Some(url)
}
pub fn build_download_url_with_default_registry(&self, default_reg: &url::Url) -> url::Url {
let mut url = self
.repository
.as_ref()
.cloned()
.unwrap_or_else(|| default_reg.clone());
let ident = self.build_identifier();
let original_path = url.path().strip_suffix('/').unwrap_or(url.path());
let final_path = format!("{original_path}/{ident}");
url.set_path(&final_path);
url
}
pub fn parse(value: &str) -> Result<Self, WebcParseError> {
let (rest, tag_opt) = value
.trim()
.rsplit_once('@')
.map(|(x, y)| (x, if y.is_empty() { None } else { Some(y) }))
.unwrap_or((value, None));
let mut parts = rest.rsplit('/');
let name = parts
.next()
.map(|x| x.trim())
.filter(|x| !x.is_empty())
.ok_or_else(|| WebcParseError::new(value, "package name is required"))?;
let namespace = parts
.next()
.map(|x| x.trim())
.filter(|x| !x.is_empty())
.ok_or_else(|| WebcParseError::new(value, "package namespace is required"))?;
let rest = parts.rev().collect::<Vec<_>>().join("/");
let repository = if rest.is_empty() {
None
} else {
let registry = rest.trim();
let full_registry =
if registry.starts_with("http://") || registry.starts_with("https://") {
registry.to_string()
} else {
format!("https://{}", registry)
};
let registry_url = url::Url::parse(&full_registry)
.map_err(|e| WebcParseError::new(value, format!("invalid registry url: {}", e)))?;
Some(registry_url)
};
Ok(Self {
repository,
namespace: namespace.to_string(),
name: name.to_string(),
tag: tag_opt.map(|x| x.to_string()),
})
}
}
impl std::fmt::Display for WebcIdent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(url) = self.build_download_url() {
write!(f, "{}", url)
} else {
write!(f, "{}", self.build_identifier())
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StringWebcIdent(pub WebcIdent);
impl StringWebcIdent {
pub fn parse(value: &str) -> Result<Self, WebcParseError> {
Ok(Self(WebcIdent::parse(value)?))
}
}
impl std::fmt::Display for StringWebcIdent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl schemars::JsonSchema for StringWebcIdent {
fn schema_name() -> String {
"StringWebcPackageIdent".to_string()
}
fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::Schema::Object(schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
..Default::default()
})
}
}
impl From<StringWebcIdent> for WebcIdent {
fn from(x: StringWebcIdent) -> Self {
x.0
}
}
impl From<WebcIdent> for StringWebcIdent {
fn from(x: WebcIdent) -> Self {
Self(x)
}
}
impl serde::Serialize for StringWebcIdent {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let val = self.0.to_string();
serializer.serialize_str(&val)
}
}
impl<'de> serde::Deserialize<'de> for StringWebcIdent {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let ident = WebcIdent::parse(&s).map_err(|e| serde::de::Error::custom(e.to_string()))?;
Ok(Self(ident))
}
}
impl std::str::FromStr for StringWebcIdent {
type Err = WebcParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
#[derive(PartialEq, Eq, Debug)]
pub struct WebcParseError {
value: String,
message: String,
}
impl WebcParseError {
fn new(value: impl Into<String>, message: impl Into<String>) -> Self {
Self {
value: value.into(),
message: message.into(),
}
}
}
impl std::fmt::Display for WebcParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"could not parse webc package specifier '{}': {}",
self.value, self.message
)
}
}
impl std::error::Error for WebcParseError {}
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug, schemars::JsonSchema)]
pub struct WebcPackagePathV1 {
#[serde(skip_serializing_if = "Option::is_none")]
pub volume: Option<String>,
pub path: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_webc_ident() {
assert_eq!(
WebcIdent::parse("ns/name").unwrap(),
WebcIdent {
repository: None,
namespace: "ns".to_string(),
name: "name".to_string(),
tag: None,
}
);
assert_eq!(
WebcIdent::parse("ns/name@").unwrap(),
WebcIdent {
repository: None,
namespace: "ns".to_string(),
name: "name".to_string(),
tag: None,
},
"empty tag should be parsed as None"
);
assert_eq!(
WebcIdent::parse("ns/name@tag").unwrap(),
WebcIdent {
repository: None,
namespace: "ns".to_string(),
name: "name".to_string(),
tag: Some("tag".to_string()),
}
);
assert_eq!(
WebcIdent::parse("reg.com/ns/name").unwrap(),
WebcIdent {
repository: Some(url::Url::parse("https://reg.com").unwrap()),
namespace: "ns".to_string(),
name: "name".to_string(),
tag: None,
}
);
assert_eq!(
WebcIdent::parse("reg.com/ns/name@tag").unwrap(),
WebcIdent {
repository: Some(url::Url::parse("https://reg.com").unwrap()),
namespace: "ns".to_string(),
name: "name".to_string(),
tag: Some("tag".to_string()),
}
);
assert_eq!(
WebcIdent::parse("https://reg.com/ns/name").unwrap(),
WebcIdent {
repository: Some(url::Url::parse("https://reg.com").unwrap()),
namespace: "ns".to_string(),
name: "name".to_string(),
tag: None,
}
);
assert_eq!(
WebcIdent::parse("https://reg.com/ns/name@tag").unwrap(),
WebcIdent {
repository: Some(url::Url::parse("https://reg.com").unwrap()),
namespace: "ns".to_string(),
name: "name".to_string(),
tag: Some("tag".to_string()),
}
);
assert_eq!(
WebcIdent::parse("http://reg.com/ns/name").unwrap(),
WebcIdent {
repository: Some(url::Url::parse("http://reg.com").unwrap()),
namespace: "ns".to_string(),
name: "name".to_string(),
tag: None,
}
);
assert_eq!(
WebcIdent::parse("http://reg.com/ns/name@tag").unwrap(),
WebcIdent {
repository: Some(url::Url::parse("http://reg.com").unwrap()),
namespace: "ns".to_string(),
name: "name".to_string(),
tag: Some("tag".to_string()),
}
);
assert_eq!(
WebcIdent::parse("alpha"),
Err(WebcParseError::new(
"alpha",
"package namespace is required"
))
);
assert_eq!(
WebcIdent::parse(""),
Err(WebcParseError::new("", "package name is required"))
);
}
#[test]
fn test_serde_serialize_webc_str_ident_with_repo() {
let ident = StringWebcIdent(WebcIdent {
repository: Some(url::Url::parse("https://wapm.io").unwrap()),
namespace: "ns".to_string(),
name: "name".to_string(),
tag: None,
});
let raw = serde_json::to_string(&ident).unwrap();
assert_eq!(raw, "\"https://wapm.io/ns/name\"");
let ident2 = serde_json::from_str::<StringWebcIdent>(&raw).unwrap();
assert_eq!(ident, ident2);
}
#[test]
fn test_serde_serialize_webc_str_ident_without_repo() {
let ident = StringWebcIdent(WebcIdent {
repository: None,
namespace: "ns".to_string(),
name: "name".to_string(),
tag: None,
});
let raw = serde_json::to_string(&ident).unwrap();
assert_eq!(raw, "\"ns/name\"");
let ident2 = serde_json::from_str::<StringWebcIdent>(&raw).unwrap();
assert_eq!(ident, ident2);
}
}