pub mod cache;
pub mod crates_io;
pub mod dockerhub;
pub mod go;
pub mod manager;
pub mod maven;
pub mod npm;
pub mod nuget;
pub mod packagist;
pub mod pypi;
pub mod rubygems;
mod trait_def;
pub use trait_def::*;
use crate::http::{MAX_RESPONSE_BYTES, bytes_with_limit};
use crate::models::VersionInfo;
pub(crate) async fn bounded_json<T: serde::de::DeserializeOwned>(
resp: reqwest::Response,
) -> RegistryResult<T> {
let bytes = bytes_with_limit(resp, MAX_RESPONSE_BYTES)
.await
.map_err(|e| RegistryError::Parse(e.to_string()))?;
serde_json::from_slice::<T>(&bytes)
.map_err(|e| RegistryError::Parse(format!("json decode: {e}")))
}
pub(crate) async fn bounded_text(resp: reqwest::Response) -> RegistryResult<String> {
let bytes = bytes_with_limit(resp, MAX_RESPONSE_BYTES)
.await
.map_err(|e| RegistryError::Parse(e.to_string()))?;
String::from_utf8(bytes).map_err(|e| RegistryError::Parse(format!("utf-8 decode: {e}")))
}
fn status_error(code: u16) -> Option<RegistryError> {
match code {
200..=299 => None,
429 => Some(RegistryError::RateLimited),
_ => Some(RegistryError::Parse(format!("unexpected status {code}"))),
}
}
pub(crate) fn ensure_status(resp: reqwest::Response) -> RegistryResult<reqwest::Response> {
match status_error(resp.status().as_u16()) {
Some(err) => Err(err),
None => Ok(resp),
}
}
pub fn sort_versions_semver(versions: &mut [VersionInfo]) {
versions.sort_by(|a, b| {
let va = parse_version(&a.version);
let vb = parse_version(&b.version);
match (va, vb) {
(Some(va), Some(vb)) => vb.cmp(&va),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => b.released_at.cmp(&a.released_at),
}
});
}
fn parse_version(v: &str) -> Option<semver::Version> {
let v = v.strip_prefix('v').unwrap_or(v);
semver::Version::parse(v).ok()
}
pub fn is_prerelease(version: &str) -> bool {
let v = version.strip_prefix('v').unwrap_or(version);
match semver::Version::parse(v) {
Ok(parsed) => !parsed.pre.is_empty(),
Err(_) => {
let without_build = v.split('+').next().unwrap_or(v);
without_build.contains('-')
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
#[test]
fn test_sort_semver_descending() {
let mut versions = vec![
VersionInfo {
version: "0.9.8".into(),
released_at: None,
prerelease: false,
},
VersionInfo {
version: "1.0.6".into(),
released_at: None,
prerelease: false,
},
VersionInfo {
version: "0.8.0".into(),
released_at: None,
prerelease: false,
},
VersionInfo {
version: "1.1.0".into(),
released_at: None,
prerelease: false,
},
];
sort_versions_semver(&mut versions);
let vs: Vec<&str> = versions.iter().map(|v| v.version.as_str()).collect();
assert_eq!(vs, vec!["1.1.0", "1.0.6", "0.9.8", "0.8.0"]);
}
#[test]
fn test_sort_semver_with_prerelease() {
let mut versions = vec![
VersionInfo {
version: "1.0.0".into(),
released_at: None,
prerelease: false,
},
VersionInfo {
version: "1.0.1-beta.1".into(),
released_at: None,
prerelease: true,
},
VersionInfo {
version: "1.0.1".into(),
released_at: None,
prerelease: false,
},
];
sort_versions_semver(&mut versions);
let vs: Vec<&str> = versions.iter().map(|v| v.version.as_str()).collect();
assert_eq!(vs, vec!["1.0.1", "1.0.1-beta.1", "1.0.0"]);
}
#[test]
fn test_sort_non_semver_falls_back_to_date() {
let now = Utc::now();
let earlier = now - chrono::Duration::hours(1);
let mut versions = vec![
VersionInfo {
version: "latest".into(),
released_at: Some(earlier),
prerelease: false,
},
VersionInfo {
version: "nightly".into(),
released_at: Some(now),
prerelease: false,
},
];
sort_versions_semver(&mut versions);
assert_eq!(versions[0].version, "nightly");
assert_eq!(versions[1].version, "latest");
}
#[test]
fn test_sort_semver_before_non_semver() {
let mut versions = vec![
VersionInfo {
version: "not-a-version".into(),
released_at: None,
prerelease: false,
},
VersionInfo {
version: "1.0.0".into(),
released_at: None,
prerelease: false,
},
];
sort_versions_semver(&mut versions);
assert_eq!(versions[0].version, "1.0.0");
assert_eq!(versions[1].version, "not-a-version");
}
#[test]
fn test_is_prerelease_with_build_metadata() {
assert!(!is_prerelease("1.0.6+spec-1.1.0"));
assert!(!is_prerelease("0.8.1+commit.abc123"));
}
#[test]
fn test_is_prerelease_true_cases() {
assert!(is_prerelease("1.0.0-beta.1"));
assert!(is_prerelease("2.0.0-rc.1"));
assert!(is_prerelease("v1.0.0-alpha"));
}
#[test]
fn test_is_prerelease_false_cases() {
assert!(!is_prerelease("1.0.0"));
assert!(!is_prerelease("v2.3.4"));
assert!(!is_prerelease("0.9.8"));
}
#[test]
fn test_sort_v_prefixed_versions() {
let mut versions = vec![
VersionInfo {
version: "v0.1.0".into(),
released_at: None,
prerelease: false,
},
VersionInfo {
version: "v1.0.0".into(),
released_at: None,
prerelease: false,
},
VersionInfo {
version: "v0.9.0".into(),
released_at: None,
prerelease: false,
},
];
sort_versions_semver(&mut versions);
let vs: Vec<&str> = versions.iter().map(|v| v.version.as_str()).collect();
assert_eq!(vs, vec!["v1.0.0", "v0.9.0", "v0.1.0"]);
}
#[test]
fn status_error_classifies_codes() {
assert!(status_error(200).is_none());
assert!(status_error(299).is_none());
assert!(matches!(
status_error(429),
Some(RegistryError::RateLimited)
));
assert!(matches!(status_error(503), Some(RegistryError::Parse(_))));
assert!(matches!(status_error(404), Some(RegistryError::Parse(_))));
}
}