use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
pub enum SourceType {
CratesIo,
GitHub {
url: String,
repo_path: Option<String>,
reference: GitReference,
},
Local {
path: String,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", content = "value")]
pub enum GitReference {
Branch(String),
Tag(String),
Default,
}
pub struct SourceDetector;
impl SourceDetector {
pub fn detect(source: Option<&str>) -> SourceType {
match source {
None => SourceType::CratesIo,
Some(s) => {
if s.starts_with("http://") || s.starts_with("https://") {
Self::parse_url(s)
} else if Self::is_local_path(s) {
SourceType::Local {
path: s.to_string(),
}
} else {
SourceType::CratesIo
}
}
}
}
fn is_local_path(s: &str) -> bool {
s.starts_with('/')
|| s.starts_with("~/")
|| s.starts_with("../")
|| s.starts_with("./")
|| s.contains('/')
|| s.contains('\\')
}
fn parse_url(url: &str) -> SourceType {
let (base_url, reference) = if let Some(pos) = url.find("#branch:") {
let (base, branch_part) = url.split_at(pos);
let branch = branch_part.trim_start_matches("#branch:");
(
base.to_string(),
Some(GitReference::Branch(branch.to_string())),
)
} else if let Some(pos) = url.find("#tag:") {
let (base, tag_part) = url.split_at(pos);
let tag = tag_part.trim_start_matches("#tag:");
(base.to_string(), Some(GitReference::Tag(tag.to_string())))
} else {
(url.to_string(), None)
};
let normalized_url = if base_url.starts_with("http://github.com/") {
base_url.replace("http://", "https://")
} else {
base_url
};
if let Some(github_part) = normalized_url.strip_prefix("https://github.com/") {
Self::parse_github_url(github_part, reference)
} else {
SourceType::Local {
path: url.to_string(),
}
}
}
fn parse_github_url(github_part: &str, explicit_reference: Option<GitReference>) -> SourceType {
let parts: Vec<&str> = github_part.split('/').collect();
if parts.len() >= 2 {
let base_url = format!("https://github.com/{}/{}", parts[0], parts[1]);
if parts.len() > 4 && parts[2] == "tree" {
let branch = parts[3];
let repo_path = parts[4..].join("/");
SourceType::GitHub {
url: base_url,
repo_path: Some(repo_path),
reference: explicit_reference
.unwrap_or_else(|| GitReference::Branch(branch.to_string())),
}
} else {
SourceType::GitHub {
url: base_url,
repo_path: None,
reference: explicit_reference.unwrap_or(GitReference::Default),
}
}
} else {
SourceType::Local {
path: format!("https://github.com/{github_part}"),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_crates_io() {
assert_eq!(SourceDetector::detect(None), SourceType::CratesIo);
assert_eq!(SourceDetector::detect(Some("serde")), SourceType::CratesIo);
}
#[test]
fn test_detect_local_paths() {
assert!(matches!(
SourceDetector::detect(Some("/absolute/path")),
SourceType::Local { .. }
));
assert!(matches!(
SourceDetector::detect(Some("~/home/path")),
SourceType::Local { .. }
));
assert!(matches!(
SourceDetector::detect(Some("./relative/path")),
SourceType::Local { .. }
));
assert!(matches!(
SourceDetector::detect(Some("../parent/path")),
SourceType::Local { .. }
));
}
#[test]
fn test_detect_github_urls() {
match SourceDetector::detect(Some("https://github.com/rust-lang/rust")) {
SourceType::GitHub {
url,
repo_path,
reference,
} => {
assert_eq!(url, "https://github.com/rust-lang/rust");
assert_eq!(repo_path, None);
assert_eq!(reference, GitReference::Default);
}
_ => panic!("Expected GitHub source"),
}
match SourceDetector::detect(Some(
"https://github.com/rust-lang/rust/tree/master/src/libstd",
)) {
SourceType::GitHub {
url,
repo_path,
reference,
} => {
assert_eq!(url, "https://github.com/rust-lang/rust");
assert_eq!(repo_path, Some("src/libstd".to_string()));
assert!(matches!(reference, GitReference::Branch(b) if b == "master"));
}
_ => panic!("Expected GitHub source with path"),
}
}
#[test]
fn test_detect_github_with_tag() {
match SourceDetector::detect(Some("https://github.com/serde-rs/serde#tag:v1.0.136")) {
SourceType::GitHub {
url,
repo_path,
reference,
} => {
assert_eq!(url, "https://github.com/serde-rs/serde");
assert_eq!(repo_path, None);
assert!(matches!(reference, GitReference::Tag(t) if t == "v1.0.136"));
}
_ => panic!("Expected GitHub source with tag"),
}
}
#[test]
fn test_detect_github_with_branch() {
match SourceDetector::detect(Some(
"https://github.com/rust-lang/rust-clippy#branch:master",
)) {
SourceType::GitHub {
url,
repo_path,
reference,
} => {
assert_eq!(url, "https://github.com/rust-lang/rust-clippy");
assert_eq!(repo_path, None);
assert!(matches!(reference, GitReference::Branch(b) if b == "master"));
}
_ => panic!("Expected GitHub source with branch"),
}
}
}