use crate::proxy::Proxy;
use strum_macros::EnumIter;
#[cfg_attr(feature = "wasm", wasm_bindgen::prelude::wasm_bindgen)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(EnumIter, Debug, PartialEq, Hash, Eq, Clone)]
pub enum Resource {
File {
owner: String,
repo: String,
reference: String,
path: String,
},
Release {
owner: String,
repo: String,
tag: String,
name: String,
},
}
impl Resource {
pub fn file(owner: String, repo: String, reference: String, path: String) -> Self {
Resource::File {
owner,
repo,
reference,
path,
}
}
pub fn release(owner: String, repo: String, tag: String, name: String) -> Self {
Resource::Release {
owner,
repo,
tag,
name,
}
}
pub fn url(&self, proxy_type: &Proxy) -> Option<String> {
match self {
Resource::File {
owner,
repo,
reference,
path,
} => Some(match proxy_type {
Proxy::Github => {
format!(
"https://github.com/{}/{}/raw/{}/{}",
owner, repo, reference, path
)
}
Proxy::Xget => {
format!(
"https://xget.xi-xu.me/gh/{}/{}/raw/{}/{}",
owner, repo, reference, path
)
}
Proxy::GhProxy => {
format!(
"https://gh-proxy.com/https://github.com/{}/{}/raw/{}/{}",
owner, repo, reference, path
)
}
Proxy::Jsdelivr => {
format!(
"https://cdn.jsdelivr.net/gh/{}/{}@{}/{}",
owner, repo, reference, path
)
}
Proxy::Statically => {
format!(
"https://cdn.statically.io/gh/{}/{}/{}/{}",
owner, repo, reference, path
)
}
}),
Resource::Release {
owner,
repo,
tag,
name,
} => match proxy_type {
Proxy::Github => Some(format!(
"https://github.com/{}/{}/releases/download/{}/{}",
owner, repo, tag, name
)),
Proxy::Xget => Some(format!(
"https://xget.xi-xu.me/gh/{}/{}/releases/download/{}/{}",
owner, repo, tag, name
)),
Proxy::GhProxy => Some(format!(
"https://gh-proxy.com/https://github.com/{}/{}/releases/download/{}/{}",
owner, repo, tag, name
)),
Proxy::Jsdelivr => None,
Proxy::Statically => None,
},
}
}
}
use crate::error::ConversionError;
use regex::Regex;
use std::sync::OnceLock;
fn raw_file_regex() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| {
Regex::new(r"^https?://github\.com/(?P<owner>[^/]+)/(?P<repo>[^/]+)/raw/(?P<rest>.+)$")
.unwrap()
})
}
fn blob_file_regex() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| {
Regex::new(r"^https?://github\.com/(?P<owner>[^/]+)/(?P<repo>[^/]+)/blob/(?P<rest>.+)$")
.unwrap()
})
}
fn release_download_regex() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| {
Regex::new(r"^https?://github\.com/(?P<owner>[^/]+)/(?P<repo>[^/]+)/releases/download/(?P<tag>[^/]+)/(?P<filename>.+)$")
.unwrap()
})
}
impl TryFrom<&str> for Resource {
type Error = ConversionError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let value = value.trim();
if let Some(captures) = raw_file_regex().captures(value) {
let owner = captures["owner"].to_string();
let repo = captures["repo"].to_string();
let rest = &captures["rest"];
let (reference, path) = split_reference_and_path(rest)?;
return Ok(Resource::File {
owner,
repo,
reference,
path,
});
}
if let Some(captures) = blob_file_regex().captures(value) {
let owner = captures["owner"].to_string();
let repo = captures["repo"].to_string();
let rest = &captures["rest"];
let (reference, path) = split_reference_and_path(rest)?;
return Ok(Resource::File {
owner,
repo,
reference,
path,
});
}
if let Some(captures) = release_download_regex().captures(value) {
return Ok(Resource::Release {
owner: captures["owner"].to_string(),
repo: captures["repo"].to_string(),
tag: captures["tag"].to_string(),
name: captures["filename"].to_string(),
});
}
Err(ConversionError::InvalidUrl(value.to_string()))
}
}
fn split_reference_and_path(rest: &str) -> Result<(String, String), ConversionError> {
let parts: Vec<&str> = rest.split('/').collect();
if parts.is_empty() {
return Err(ConversionError::ParseError(
"Missing reference and path".to_string(),
));
}
if parts.len() >= 4 && parts[0] == "refs" {
let reference = format!("{}/{}/{}", parts[0], parts[1], parts[2]);
let path = parts[3..].join("/");
if path.is_empty() {
return Err(ConversionError::ParseError("Missing file path".to_string()));
}
Ok((reference, path))
} else if parts.len() >= 2 {
let reference = parts[0].to_string();
let path = parts[1..].join("/");
Ok((reference, path))
} else {
Err(ConversionError::ParseError(
"Invalid reference/path format".to_string(),
))
}
}
impl TryFrom<String> for Resource {
type Error = ConversionError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Resource::try_from(value.as_str())
}
}