use std::{env, path::PathBuf};
use thiserror::Error;
use url::Url;
#[must_use]
pub fn metalink(url: &Url) -> Option<Box<dyn Metalink>> {
if let Ok(github) = GitHubLink::from_url(url) {
return Some(Box::new(github));
}
if let Ok(gitlab) = GitLabLink::from_url(url) {
return Some(Box::new(gitlab));
}
if let Ok(pathbuf) = PathBuf::from_url(url) {
return Some(Box::new(pathbuf));
}
None
}
pub trait Metalink {
fn from_url(url: &Url) -> Result<Self, MetaLinkError>
where
Self: Sized;
fn platform(&self) -> &str;
fn user_repo(&self) -> String {
match self.branch() {
Some("master" | "main") | None => format!("{}/{}", self.user(), self.repo()),
Some(branch) => format!("{}/{}#{}", self.user(), self.repo(), branch),
}
}
fn user(&self) -> &str;
fn repo(&self) -> &str;
fn branch(&self) -> Option<&str>;
fn pretty(&self) -> String {
format!("{}:{}", self.platform(), self.user_repo())
}
}
impl Metalink for PathBuf {
fn from_url(url: &Url) -> Result<Self, MetaLinkError>
where
Self: Sized,
{
match url.to_file_path() {
Ok(o) => Ok(o),
Err(()) => Err(MetaLinkError::Empty),
}
}
fn pretty(&self) -> String {
#[allow(deprecated)]
if let Some(home) = env::home_dir() {
if let Ok(stripped) = self.strip_prefix(&home) {
return format!("~/{}", stripped.display());
}
}
self.display().to_string()
}
fn branch(&self) -> Option<&str> {
None
}
fn user_repo(&self) -> String {
String::new()
}
fn user(&self) -> &'static str {
""
}
fn repo(&self) -> &str {
match self.file_name() {
Some(val) => val.to_str().expect("Valid unicode"),
None => "/",
}
}
fn platform(&self) -> &'static str {
"file"
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct GitHubLink {
url: Url,
repo: String,
user: String,
branch: String,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct GitLabLink {
url: Url,
repo: String,
user: String,
branch: String,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct SourceHutLink {
url: Url,
repo: String,
user: String,
branch: String,
}
#[derive(Debug, Error)]
pub enum MetaLinkError {
#[error("empty path segments")]
Empty,
#[error("invalid size, expected `{expected}`, got `{got}`")]
Size { expected: usize, got: usize },
#[error("mismatched platform, expected `{expected}`, got `{got}`")]
MismatchedPlatform { expected: String, got: String },
#[error("missing domain")]
MissingDomain,
}
impl Metalink for GitHubLink {
fn from_url(url: &Url) -> Result<Self, MetaLinkError>
where
Self: Sized,
{
if let Some(domain) = url.domain() {
if domain != "raw.githubusercontent.com" {
return Err(MetaLinkError::MismatchedPlatform {
expected: String::from("raw.githubusercontent.com"),
got: domain.to_string(),
});
}
} else {
return Err(MetaLinkError::MissingDomain);
}
let path_segments = url
.path_segments()
.ok_or(MetaLinkError::Empty)?
.collect::<Vec<_>>();
if path_segments.len() != 3 {
return Err(MetaLinkError::Size {
expected: 3,
got: path_segments.len(),
});
}
Ok(Self {
url: url.clone(),
user: path_segments[0].to_string(),
repo: path_segments[1].to_string(),
branch: path_segments[2].to_string(),
})
}
fn platform(&self) -> &'static str {
"github"
}
fn user(&self) -> &str {
&self.user
}
fn repo(&self) -> &str {
&self.repo
}
fn branch(&self) -> Option<&str> {
Some(&self.branch)
}
}
impl Metalink for GitLabLink {
fn from_url(url: &Url) -> Result<Self, MetaLinkError>
where
Self: Sized,
{
if let Some(domain) = url.domain() {
if domain != "gitlab.com" {
return Err(MetaLinkError::MismatchedPlatform {
expected: String::from("gitlab.com"),
got: domain.to_string(),
});
}
} else {
return Err(MetaLinkError::MissingDomain);
}
let path_segments = url
.path_segments()
.ok_or(MetaLinkError::Empty)?
.collect::<Vec<_>>();
if path_segments.len() != 5 {
return Err(MetaLinkError::Size {
expected: 5,
got: path_segments.len(),
});
}
Ok(Self {
url: url.clone(),
user: path_segments[0].to_string(),
repo: path_segments[1].to_string(),
branch: path_segments[4].to_string(),
})
}
fn platform(&self) -> &'static str {
"gitlab"
}
fn user(&self) -> &str {
&self.user
}
fn repo(&self) -> &str {
&self.repo
}
fn branch(&self) -> Option<&str> {
Some(&self.branch)
}
}
impl Metalink for SourceHutLink {
fn from_url(url: &Url) -> Result<Self, MetaLinkError>
where
Self: Sized,
{
if let Some(domain) = url.domain() {
if domain != "git.sr.ht" {
return Err(MetaLinkError::MismatchedPlatform {
expected: String::from("git.sr.ht"),
got: domain.to_string(),
});
}
} else {
return Err(MetaLinkError::MissingDomain);
}
let path_segments = url
.path_segments()
.ok_or(MetaLinkError::Empty)?
.collect::<Vec<_>>();
if path_segments.len() != 3 {
return Err(MetaLinkError::Size {
expected: 5,
got: path_segments.len(),
});
}
Ok(Self {
url: url.clone(),
user: path_segments[0]
.strip_prefix('~')
.unwrap_or(path_segments[0])
.to_string(),
repo: path_segments[1].to_string(),
branch: path_segments[3].to_string(),
})
}
fn platform(&self) -> &'static str {
"srht"
}
fn user(&self) -> &str {
&self.user
}
fn repo(&self) -> &str {
&self.repo
}
fn branch(&self) -> Option<&str> {
Some(&self.branch)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_github_metalink() {
let url = Url::parse("https://raw.githubusercontent.com/pacstall/pacstall-programs/master")
.unwrap();
let gh = GitHubLink::from_url(&url).unwrap();
assert_eq!(
gh,
GitHubLink {
url,
user: String::from("pacstall"),
repo: String::from("pacstall-programs"),
branch: String::from("master")
}
)
}
}