use std::fmt;
use anyhow::{bail, Context, Result};
use crate::net::Http;
pub const DEFAULT_REPO: &str = "AccentCMS/accent";
pub const OLDEST_PUBLISHED: &str = "v0.22.0";
pub const OLDEST_MUSL: &str = "v0.26.0";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Version(String);
impl Version {
pub fn parse(raw: &str) -> Result<Self> {
let bare = raw.trim().trim_start_matches('v');
if bare.is_empty() {
bail!("empty version");
}
if bare.contains('/') || bare.contains(char::is_whitespace) {
bail!("not a release version: {raw}");
}
Ok(Self(bare.to_string()))
}
pub fn tag(&self) -> String {
format!("v{}", self.0)
}
pub fn number(&self) -> &str {
&self.0
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "v{}", self.0)
}
}
#[derive(Debug, Clone)]
pub struct Repo(String);
impl Repo {
pub fn from_env() -> Self {
match std::env::var("ACCENT_REPO") {
Ok(slug) if !slug.trim().is_empty() => Self(slug.trim().to_string()),
_ => Self(DEFAULT_REPO.to_string()),
}
}
pub fn slug(&self) -> &str {
&self.0
}
pub fn releases_page(&self) -> String {
format!("https://github.com/{}/releases", self.0)
}
pub fn latest_release_url(&self) -> String {
format!("https://github.com/{}/releases/latest", self.0)
}
pub fn download_url(&self, version: &Version, asset: &str) -> String {
format!(
"https://github.com/{}/releases/download/{}/{asset}",
self.0,
version.tag()
)
}
pub fn checksums_url(&self, version: &Version) -> String {
self.download_url(version, &checksums_name(version))
}
pub fn signature_url(&self, version: &Version) -> String {
self.download_url(version, &format!("{}.asc", checksums_name(version)))
}
}
pub fn checksums_name(version: &Version) -> String {
format!("checksums-{}.txt", version.tag())
}
pub fn resolve_latest(http: &Http, repo: &Repo) -> Result<Version> {
let url = repo.latest_release_url();
let location = http
.redirect_location(&url)
.with_context(|| format!("asking {url} for the latest release"))?;
let version = location
.as_deref()
.and_then(tag_from_location)
.map(|tag| Version::parse(&tag))
.transpose()?;
version.with_context(|| {
format!(
"could not determine the latest version — there may be no published release yet.\n\
Check {} or pass --version.",
repo.releases_page()
)
})
}
pub fn tag_from_location(location: &str) -> Option<String> {
let tag = location.rsplit_once("/tag/")?.1;
let tag = tag.split(['?', '#']).next().unwrap_or_default().trim();
if tag.is_empty() || tag.contains('/') {
return None;
}
Some(tag.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_a_version_with_or_without_the_v() {
assert_eq!(Version::parse("0.25.0").unwrap().tag(), "v0.25.0");
assert_eq!(Version::parse("v0.25.0").unwrap().tag(), "v0.25.0");
assert_eq!(Version::parse(" v0.25.0 ").unwrap().number(), "0.25.0");
assert_eq!(
Version::parse("0.25.0").unwrap(),
Version::parse("v0.25.0").unwrap()
);
}
#[test]
fn rejects_junk_versions() {
assert!(Version::parse("").is_err());
assert!(Version::parse("v").is_err());
assert!(Version::parse("0.25.0/../etc").is_err());
assert!(Version::parse("0.25.0 rc1").is_err());
}
#[test]
fn builds_the_published_urls() {
let repo = Repo(DEFAULT_REPO.to_string());
let v = Version::parse("0.25.0").unwrap();
assert_eq!(
repo.download_url(&v, "accent-v0.25.0-aarch64-apple-darwin.tar.gz"),
"https://github.com/AccentCMS/accent/releases/download/v0.25.0/accent-v0.25.0-aarch64-apple-darwin.tar.gz"
);
assert_eq!(
repo.checksums_url(&v),
"https://github.com/AccentCMS/accent/releases/download/v0.25.0/checksums-v0.25.0.txt"
);
assert_eq!(
repo.signature_url(&v),
"https://github.com/AccentCMS/accent/releases/download/v0.25.0/checksums-v0.25.0.txt.asc"
);
}
#[test]
fn reads_the_tag_out_of_a_redirect() {
assert_eq!(
tag_from_location("https://github.com/AccentCMS/accent/releases/tag/v0.25.0")
.as_deref(),
Some("v0.25.0")
);
assert_eq!(
tag_from_location("https://github.com/AccentCMS/accent/releases"),
None
);
assert_eq!(tag_from_location("").as_deref(), None);
}
}