use std::future::Future;
use std::pin::Pin;
use anyhow::Result;
use crate::constants::{self, NPM_REGISTRY_URL};
use crate::dependency::{Dependency, DependencyKind, Ecosystem, VersionResult};
use crate::error::DepupError;
use crate::maven::discovery::VersionProperty;
use crate::maven::tool::ToolVersionResolver;
use crate::version;
macro_rules! pm_tools {
( $( ($pattern:expr, $package:expr) ),* $(,)? ) => {
const PM_TOOLS: &[(&str, &str)] = &[ $( ($pattern, $package), )* ];
const PM_PATTERN_NAMES: &[&str] = &[ $( $pattern, )* ];
};
}
pm_tools![
("version.npm", "npm"),
("npm.version", "npm"),
("version.pnpm", "pnpm"),
("pnpm.version", "pnpm"),
("version.yarn", "yarn"),
("yarn.version", "yarn"),
];
pub struct PmVersionsResolver {
client: reqwest::Client,
}
impl PmVersionsResolver {
pub fn new() -> Self {
Self {
client: constants::http_client(),
}
}
fn resolve_package(property_name: &str) -> Option<&'static str> {
PM_TOOLS
.iter()
.find(|(pattern, _)| *pattern == property_name)
.map(|(_, package)| *package)
}
async fn fetch_and_check(
&self,
property: &VersionProperty,
source: &str,
) -> Result<VersionResult> {
let Some(package) = Self::resolve_package(&property.name) else {
let id = Dependency::new(
Ecosystem::Maven,
DependencyKind::ToolVersion,
property.name.clone(),
None,
source.to_string(),
);
return Ok(VersionResult::error(
id,
property.current_value.clone(),
format!("Unknown tool property: {}", property.name),
));
};
let id = Dependency::new(
Ecosystem::Maven,
DependencyKind::ToolVersion,
package.to_string(),
None,
source.to_string(),
);
let current = property.current_value.clone();
let url = format!("{NPM_REGISTRY_URL}/{package}");
let resp = self
.client
.get(&url)
.send()
.await
.map_err(|e| DepupError::http_request_failed(&url, &e.to_string()))?;
if !resp.status().is_success() {
return Err(
DepupError::http_request_failed(&url, &format!("HTTP {}", resp.status())).into(),
);
}
let body: serde_json::Value = resp
.json()
.await
.map_err(|e| DepupError::http_request_failed(&url, &e.to_string()))?;
let latest = body["dist-tags"]["latest"]
.as_str()
.map(ToString::to_string);
match latest {
Some(latest) => {
let is_outdated = version::is_newer(¤t, &latest);
Ok(VersionResult::checked(id, current, latest, is_outdated))
}
None => Ok(VersionResult::error(
id,
current,
format!("No latest version found for {package}"),
)),
}
}
}
impl ToolVersionResolver for PmVersionsResolver {
fn patterns(&self) -> &[&str] {
PM_PATTERN_NAMES
}
fn label(&self, property: &VersionProperty) -> String {
Self::resolve_package(&property.name)
.unwrap_or("unknown")
.to_string()
}
fn resolve<'a>(
&'a self,
property: &'a VersionProperty,
source: &'a str,
) -> Pin<Box<dyn Future<Output = Result<VersionResult>> + Send + 'a>> {
Box::pin(self.fetch_and_check(property, source))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn patterns_match_pm_tools() {
let resolver = PmVersionsResolver::new();
let patterns = resolver.patterns();
assert!(patterns.contains(&"version.npm"));
assert!(patterns.contains(&"npm.version"));
assert!(patterns.contains(&"version.pnpm"));
assert!(patterns.contains(&"pnpm.version"));
assert!(patterns.contains(&"version.yarn"));
assert!(patterns.contains(&"yarn.version"));
}
#[test]
fn resolve_package_names() {
assert_eq!(
PmVersionsResolver::resolve_package("version.npm"),
Some("npm")
);
assert_eq!(
PmVersionsResolver::resolve_package("npm.version"),
Some("npm")
);
assert_eq!(
PmVersionsResolver::resolve_package("version.pnpm"),
Some("pnpm")
);
assert_eq!(
PmVersionsResolver::resolve_package("pnpm.version"),
Some("pnpm")
);
assert_eq!(
PmVersionsResolver::resolve_package("version.yarn"),
Some("yarn")
);
assert_eq!(
PmVersionsResolver::resolve_package("yarn.version"),
Some("yarn")
);
}
#[test]
fn does_not_match_unrelated() {
assert_eq!(PmVersionsResolver::resolve_package("version.junit"), None);
assert_eq!(PmVersionsResolver::resolve_package("npm"), None);
assert_eq!(PmVersionsResolver::resolve_package("version.node"), None);
}
#[test]
fn label_resolves_from_property() {
let resolver = PmVersionsResolver::new();
let prop = VersionProperty {
name: "version.pnpm".to_string(),
current_value: "9.0.0".to_string(),
};
assert_eq!(resolver.label(&prop), "pnpm");
}
#[test]
fn label_returns_unknown_for_unrecognized_property() {
let resolver = PmVersionsResolver::new();
let prop = VersionProperty {
name: "version.unknown-tool".to_string(),
current_value: "1.0.0".to_string(),
};
assert_eq!(resolver.label(&prop), "unknown");
}
#[test]
fn resolve_package_returns_none_for_node() {
assert_eq!(PmVersionsResolver::resolve_package("version.node"), None);
}
}