use super::{CratesIoClient, Error};
pub enum ChangelogResult {
Found { filename: String, content: String },
NoRepository,
NotGitHub { url: String },
NotFound,
}
const CHANGELOG_FILENAMES: &[&str] = &[
"CHANGELOG.md",
"CHANGES.md",
"HISTORY.md",
"RELEASES.md",
"changelog.md",
"changes.md",
"history.md",
"releases.md",
];
impl CratesIoClient {
pub async fn fetch_changelog(&self, name: &str) -> Result<ChangelogResult, Error> {
let crate_resp = self.get_crate(name).await?;
let repo_url = match crate_resp.crate_data.repository {
Some(url) if !url.is_empty() => url,
_ => return Ok(ChangelogResult::NoRepository),
};
let (owner, repo) = match parse_github_repo(&repo_url) {
Some(pair) => pair,
None => return Ok(ChangelogResult::NotGitHub { url: repo_url }),
};
for filename in CHANGELOG_FILENAMES {
let url = format!(
"{}/{}/{}/HEAD/{}",
self.github_raw_base_url, owner, repo, filename
);
let resp = self.http.get(&url).send().await?;
if resp.status().is_success() {
let content = resp.text().await?;
return Ok(ChangelogResult::Found {
filename: filename.to_string(),
content,
});
}
}
Ok(ChangelogResult::NotFound)
}
}
fn parse_github_repo(url: &str) -> Option<(String, String)> {
let rest = url
.trim_end_matches('/')
.trim_end_matches(".git")
.strip_prefix("https://github.com/")
.or_else(|| url.strip_prefix("http://github.com/"))?;
let mut parts = rest.splitn(3, '/');
let owner = parts.next()?.to_string();
let repo = parts.next()?.to_string();
if owner.is_empty() || repo.is_empty() {
return None;
}
Some((owner, repo))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_github_repo_standard() {
let (owner, repo) = parse_github_repo("https://github.com/serde-rs/serde").unwrap();
assert_eq!(owner, "serde-rs");
assert_eq!(repo, "serde");
}
#[test]
fn parse_github_repo_trailing_slash() {
let (owner, repo) = parse_github_repo("https://github.com/tokio-rs/tokio/").unwrap();
assert_eq!(owner, "tokio-rs");
assert_eq!(repo, "tokio");
}
#[test]
fn parse_github_repo_git_suffix() {
let (owner, repo) = parse_github_repo("https://github.com/dtolnay/anyhow.git").unwrap();
assert_eq!(owner, "dtolnay");
assert_eq!(repo, "anyhow");
}
#[test]
fn parse_github_repo_with_subpath() {
let (owner, repo) =
parse_github_repo("https://github.com/rust-lang/rust/tree/master/library/std").unwrap();
assert_eq!(owner, "rust-lang");
assert_eq!(repo, "rust");
}
#[test]
fn parse_github_repo_non_github() {
assert!(parse_github_repo("https://gitlab.com/owner/repo").is_none());
assert!(parse_github_repo("https://bitbucket.org/owner/repo").is_none());
assert!(parse_github_repo("").is_none());
}
#[test]
fn parse_github_repo_incomplete() {
assert!(parse_github_repo("https://github.com/").is_none());
assert!(parse_github_repo("https://github.com/owner").is_none());
}
}