use std::time::Duration;
use anyhow::{Context, Result};
pub const REPO: &str = "jhonsferg/gvsn";
pub fn api_base() -> String {
std::env::var("GVSN_TEST_API_BASE").unwrap_or_else(|_| "https://api.github.com".to_owned())
}
#[derive(serde::Deserialize)]
pub struct GithubRelease {
pub tag_name: String,
}
pub fn fetch_latest_version(timeout: Duration) -> Result<String> {
let agent = ureq::Agent::config_builder()
.timeout_global(Some(timeout))
.user_agent(format!("gvsn/{}", env!("CARGO_PKG_VERSION")))
.build()
.new_agent();
let url = format!("{}/repos/{REPO}/releases/latest", api_base());
let mut response = agent
.get(&url)
.call()
.with_context(|| format!("Failed to reach {url}"))?;
let release: GithubRelease = response
.body_mut()
.read_json()
.context("Failed to parse GitHub release response")?;
Ok(release.tag_name.trim_start_matches('v').to_string())
}
pub fn parse_semver(s: &str) -> Option<(u32, u32, u32)> {
let parts: Vec<&str> = s.split('.').collect();
match parts.as_slice() {
[major, minor, patch] => Some((
major.parse().ok()?,
minor.parse().ok()?,
patch.parse().ok()?,
)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn semver_parses_correctly() {
assert_eq!(parse_semver("1.2.3"), Some((1, 2, 3)));
assert_eq!(parse_semver("0.1.0"), Some((0, 1, 0)));
assert_eq!(parse_semver("10.0.0"), Some((10, 0, 0)));
assert_eq!(parse_semver("bad"), None);
assert_eq!(parse_semver("1.2"), None);
}
#[test]
fn semver_rejects_invalid_middle_segment_instead_of_skipping_it() {
assert_eq!(parse_semver("1.2.3-beta.1"), None);
assert_eq!(parse_semver("1.x.2.3"), None);
}
#[test]
fn semver_ordering() {
assert!(parse_semver("0.2.0") > parse_semver("0.1.9"));
assert!(parse_semver("1.0.0") > parse_semver("0.99.99"));
assert_eq!(parse_semver("1.0.0"), parse_semver("1.0.0"));
}
}