use std::fmt::{Display, Formatter};
use url::Url;
use crate::DIDWebVHError;
#[derive(Debug, PartialEq)]
pub enum URLType {
DIDDoc,
WhoIs,
}
pub struct WebVHURL {
pub type_: URLType,
pub did_url: String,
pub scid: String,
pub domain: String,
pub port: Option<u16>,
pub path: String,
pub fragment: Option<String>,
pub query: Option<String>,
}
impl WebVHURL {
pub fn parse_did_url(url: &str) -> Result<WebVHURL, DIDWebVHError> {
let url = if let Some(prefix) = url.strip_prefix("did:webvh:") {
prefix
} else if url.starts_with("did:") {
return Err(DIDWebVHError::UnsupportedMethod);
} else {
url
};
let (prefix, fragment) = match url.split_once('#') {
Some((prefix, fragment)) => (prefix, Some(fragment.to_string())),
None => (url, None),
};
let (prefix, query) = match prefix.split_once('?') {
Some((prefix, query)) => (prefix, Some(query.to_string())),
None => (url, None),
};
let parts = prefix.split(':').collect::<Vec<_>>();
if parts.len() < 2 {
return Err(DIDWebVHError::InvalidMethodIdentifier(
"Invalid URL: Must contain SCID and domain".to_string(),
));
}
let scid = parts[0].to_string();
let (domain, port) = match parts[1].split_once("%3A") {
Some((domain, port)) => {
let port = match port.parse::<u16>() {
Ok(port) => port,
Err(err) => {
return Err(DIDWebVHError::InvalidMethodIdentifier(format!(
"Invalid URL: Port ({port}) must be a number: {err}",
)));
}
};
(domain.to_string(), Some(port))
}
None => (parts[1].to_string(), None),
};
let mut path = String::new();
for part in parts[2..].iter() {
if part != &"whois" {
path.push('/');
path.push_str(part);
}
}
if path.is_empty() {
path = "/.well-known".to_string();
}
let type_ = if parts.len() > 2 && parts[parts.len() - 1] == "whois" {
path.push_str("/whois.vp");
URLType::WhoIs
} else {
path.push_str("/did.jsonl");
URLType::DIDDoc
};
Ok(WebVHURL {
type_,
did_url: url.to_string(),
scid,
domain,
port,
path,
fragment,
query,
})
}
pub fn parse_url(url: &Url) -> Result<WebVHURL, DIDWebVHError> {
if url.scheme() != "http" && url.scheme() != "https" {
return Err(DIDWebVHError::InvalidMethodIdentifier(
"Invalid URL: Must be http or https".to_string(),
));
}
let fragment = url.fragment();
let query = url.query();
let Some(domain) = url.domain() else {
return Err(DIDWebVHError::InvalidMethodIdentifier(
"Invalid URL: Must contain domain".to_string(),
));
};
let port = url.port();
let (type_, path) = if url.path() == "/" {
(URLType::DIDDoc, "/.well-known/did.jsonl".to_string())
} else if url.path().ends_with("/whois") {
(URLType::WhoIs, "/whois.vp".to_string())
} else if url.path().ends_with("/did.jsonl") {
(URLType::DIDDoc, url.path().to_string())
} else {
(URLType::DIDDoc, [url.path(), "/did.jsonl"].concat())
};
Ok(WebVHURL {
type_,
did_url: url.to_string(),
scid: "{SCID}".to_string(),
domain: domain.to_string(),
port,
path,
fragment: fragment.map(|s| s.to_string()),
query: query.map(|s| s.to_string()),
})
}
pub fn get_http_url(&self) -> Result<Url, DIDWebVHError> {
let mut url_string = String::new();
if self.domain == "localhost" {
url_string.push_str("http://");
} else {
url_string.push_str("https://");
}
url_string.push_str(&self.domain);
if let Some(port) = self.port {
url_string.push_str(&format!(":{port}",));
}
url_string.push_str(&self.path);
if let Some(query) = &self.query {
url_string.push_str(&format!("?{query}",));
}
if let Some(fragment) = &self.fragment {
url_string.push_str(&format!("#{fragment}",));
}
match Url::parse(&url_string) {
Ok(url) => Ok(url),
Err(err) => Err(DIDWebVHError::InvalidMethodIdentifier(format!(
"Invalid URL: {err}",
))),
}
}
}
impl Display for WebVHURL {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut url_string = String::new();
url_string.push_str("did:webvh:");
url_string.push_str(&self.scid);
url_string.push(':');
url_string.push_str(&self.domain);
if let Some(port) = self.port {
url_string.push_str(&format!("%3A{port}",));
}
if !self.path.is_empty() && self.path != "/.well-known/did.jsonl" {
let s = self.path.strip_suffix("/did.jsonl").unwrap();
let s = s.replace("/", ":");
url_string.push_str(&s);
}
if let Some(query) = &self.query {
url_string.push('?');
url_string.push_str(query);
}
if let Some(fragment) = &self.fragment {
url_string.push('#');
url_string.push_str(fragment);
}
write!(f, "{url_string}",)
}
}
#[cfg(test)]
mod tests {
use crate::{
DIDWebVHError,
url::{URLType, WebVHURL},
};
#[test]
fn wrong_method() {
assert!(WebVHURL::parse_did_url("did:wrong:method").is_err())
}
#[test]
fn url_with_fragment() {
let parsed = match WebVHURL::parse_did_url("did:webvh:scid:example.com#key-fragment") {
Ok(parsed) => parsed,
Err(_) => panic!("Failed to parse URL"),
};
assert_eq!(parsed.fragment, Some("key-fragment".to_string()));
}
#[test]
fn url_with_query() {
let parsed = match WebVHURL::parse_did_url("did:webvh:scid:example.com?versionId=1-xyz") {
Ok(parsed) => parsed,
Err(_) => panic!("Failed to parse URL"),
};
assert_eq!(parsed.query, Some("versionId=1-xyz".to_string()));
}
#[test]
fn missing_parts() {
assert!(WebVHURL::parse_did_url("did:webvh:domain").is_err());
assert!(WebVHURL::parse_did_url("did:webvh:domain#test").is_err());
}
#[test]
fn url_with_port() {
assert!(WebVHURL::parse_did_url("did:webvh:scid:domain%3A8000").is_ok());
}
#[test]
fn url_with_bad_port() {
assert!(WebVHURL::parse_did_url("did:webvh:scid:domain%3A8bad").is_err());
assert!(WebVHURL::parse_did_url("did:webvh:scid:domain%3A999999").is_err());
}
#[test]
fn url_with_whois() -> Result<(), DIDWebVHError> {
let result = WebVHURL::parse_did_url("did:webvh:scid:domain%3A8000:whois")?;
assert_eq!(result.type_, URLType::WhoIs);
assert_eq!(result.path, "/.well-known/whois.vp");
Ok(())
}
#[test]
fn url_with_whois_path() -> Result<(), DIDWebVHError> {
let result = WebVHURL::parse_did_url("did:webvh:scid:domain%3A8000:custom:path:whois")?;
assert_eq!(result.type_, URLType::WhoIs);
assert_eq!(result.path, "/custom/path/whois.vp");
Ok(())
}
#[test]
fn url_with_default_path() -> Result<(), DIDWebVHError> {
let result = WebVHURL::parse_did_url("did:webvh:scid:domain%3A8000")?;
assert_eq!(result.type_, URLType::DIDDoc);
assert_eq!(result.path, "/.well-known/did.jsonl");
Ok(())
}
#[test]
fn url_with_custom_path() -> Result<(), DIDWebVHError> {
let result = WebVHURL::parse_did_url("did:webvh:scid:domain%3A8000:custom:path")?;
assert_eq!(result.type_, URLType::DIDDoc);
assert_eq!(result.path, "/custom/path/did.jsonl");
Ok(())
}
#[test]
fn to_url_from_basic() -> Result<(), DIDWebVHError> {
let webvh = WebVHURL::parse_did_url("did:webvh:scid:example.com")?;
assert_eq!(
webvh.get_http_url()?.to_string().as_str(),
"https://example.com/.well-known/did.jsonl"
);
Ok(())
}
#[test]
fn to_url_from_basic_whois() -> Result<(), DIDWebVHError> {
let webvh = WebVHURL::parse_did_url("did:webvh:scid:example.com:whois")?;
assert_eq!(
webvh.get_http_url()?.to_string().as_str(),
"https://example.com/.well-known/whois.vp"
);
Ok(())
}
#[test]
fn to_url_from_complex() -> Result<(), DIDWebVHError> {
let webvh = WebVHURL::parse_did_url(
"did:webvh:scid:example.com%3A8080:custom:path?versionId=1-xyz#fragment",
)?;
assert_eq!(
webvh.get_http_url()?.to_string().as_str(),
"https://example.com:8080/custom/path/did.jsonl?versionId=1-xyz#fragment"
);
Ok(())
}
}