use serde::Serialize;
use crate::cli::versions::VersionsArgs;
use crate::error::RkError;
use crate::output::Output;
use crate::{embedded, registry};
#[derive(Debug, Serialize)]
struct PinResult {
tool: String,
pinned: String,
result: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
available: Option<String>,
}
#[derive(Debug, Serialize)]
struct Report {
schema: &'static str,
pins: Vec<PinResult>,
}
pub fn run(args: &VersionsArgs) -> Result<(), RkError> {
if !args.check {
Output::human().result_raw(embedded::VERSIONS);
return Ok(());
}
let out = Output::new(args.json);
let mut results = Vec::new();
for pin in registry::pins() {
let result = pin.check.as_deref().map_or_else(
|| PinResult {
tool: pin.name.clone(),
pinned: pin.version.clone(),
result: "source-unreachable",
available: None,
},
|url| check_one(&pin.name, &pin.version, url),
);
out.result_line(match (&result.result, &result.available) {
(&"update-available", Some(available)) => format!(
"update-available {} {} pinned, {available} at the source",
result.tool, result.pinned
),
_ => format!("{} {} {}", result.result, result.tool, result.pinned),
});
results.push(result);
}
out.next(&[
"a pin update is a reviewed change to versions.toml, with its checked date".to_owned(),
]);
out.emit(&Report {
schema: "rk.versions-check/1",
pins: results,
})
}
fn check_one(tool: &str, pinned: &str, url: &str) -> PinResult {
let result = |result, available| PinResult {
tool: tool.to_owned(),
pinned: pinned.to_owned(),
result,
available,
};
let curl = std::env::var_os("RK_CURL_BIN").unwrap_or_else(|| "curl".into());
let fetched = std::process::Command::new(curl)
.args(["-fsSL", "--max-time", "10", url])
.output();
let body = match fetched {
Ok(output) if output.status.success() => output.stdout,
_ => return result("source-unreachable", None),
};
let Some(available) = latest_version(&body) else {
return result("source-unparsable", None);
};
if is_current(pinned, &available) {
result("current", Some(available))
} else {
result("update-available", Some(available))
}
}
fn latest_version(body: &[u8]) -> Option<String> {
let value: serde_json::Value = serde_json::from_slice(body).ok()?;
let raw = value
.get("crate")
.and_then(|krate| krate.get("max_stable_version"))
.or_else(|| value.get("tag_name"))
.and_then(serde_json::Value::as_str)?;
let start = raw.find(|c: char| c.is_ascii_digit())?;
Some(raw[start..].to_owned())
}
fn is_current(pinned: &str, available: &str) -> bool {
if pinned == available {
return true;
}
!pinned.contains('.') && available.split('.').next() == Some(pinned)
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use super::{PinResult, Report, is_current, latest_version};
#[test]
fn a_source_version_is_read_from_both_answer_shapes() {
assert_eq!(
latest_version(br#"{"crate":{"max_stable_version":"0.3.170"}}"#),
Some("0.3.170".to_owned())
);
assert_eq!(
latest_version(br#"{"tag_name":"v2.13.1"}"#),
Some("2.13.1".to_owned())
);
assert_eq!(
latest_version(br#"{"tag_name":"release-plz-v0.3.160"}"#),
Some("0.3.160".to_owned())
);
assert_eq!(latest_version(b"not json"), None);
assert_eq!(latest_version(br#"{"unrelated":true}"#), None);
}
#[test]
fn a_major_only_pin_is_current_within_its_major() {
assert!(is_current("0.3.160", "0.3.160"));
assert!(!is_current("0.3.160", "0.3.170"));
assert!(is_current("4", "4.3.1"));
assert!(!is_current("4", "5.0.0"));
}
#[test]
fn the_versions_check_schema_snapshot_holds() {
let report = Report {
schema: "rk.versions-check/1",
pins: vec![PinResult {
tool: "release-plz".into(),
pinned: "0.3.160".into(),
result: "update-available",
available: Some("0.3.170".into()),
}],
};
assert_eq!(
serde_json::to_string(&report).expect("a report serializes"),
r#"{"schema":"rk.versions-check/1","pins":[{"tool":"release-plz","pinned":"0.3.160","result":"update-available","available":"0.3.170"}]}"#
);
}
}