use std::io::Write;
use std::path::PathBuf;
use anyhow::{Context, Result};
use semver::Version;
use serde::Serialize;
use crate::manifest::Manifest;
use crate::report::Report;
pub const SCHEMA: u32 = 1;
#[derive(Serialize)]
pub struct ListOutput {
pub schema: u32,
pub prefix: PathBuf,
pub checked_at: Option<u64>,
pub crates: Vec<ListCrate>,
}
#[derive(Serialize)]
pub struct ListCrate {
pub name: String,
pub version: String,
pub bins: Vec<String>,
pub locked: bool,
pub pinned: bool,
pub status: ListStatus,
pub latest: Option<Version>,
}
#[derive(Serialize, Clone, Copy, PartialEq, Eq, Debug)]
#[serde(rename_all = "snake_case")]
pub enum ListStatus {
UpToDate,
Outdated,
Unknown,
}
#[derive(Serialize)]
pub struct CheckOutput {
pub schema: u32,
pub prefix: PathBuf,
pub checked_at: u64,
pub crates: Vec<CheckCrate>,
}
#[derive(Serialize)]
pub struct CheckCrate {
pub name: String,
pub current: Version,
pub latest: Version,
pub outdated: bool,
}
impl ListOutput {
pub fn build(prefix: PathBuf, manifest: &Manifest, report: Option<&Report>) -> Self {
let crates = manifest
.crates
.iter()
.map(|(name, entry)| {
let (status, latest) = Version::parse(&entry.version)
.ok()
.and_then(|current| report?.checked_for(name, ¤t))
.map_or((ListStatus::Unknown, None), |c| {
let status = if c.is_outdated() {
ListStatus::Outdated
} else {
ListStatus::UpToDate
};
(status, Some(c.latest.clone()))
});
ListCrate {
name: name.clone(),
version: entry.version.clone(),
bins: entry.bins.clone(),
locked: entry.locked,
pinned: entry.pinned,
status,
latest,
}
})
.collect();
Self {
schema: SCHEMA,
prefix,
checked_at: report.map(|r| r.checked_at),
crates,
}
}
}
impl CheckOutput {
pub fn from_report(report: &Report) -> Self {
Self {
schema: SCHEMA,
prefix: report.prefix.clone(),
checked_at: report.checked_at,
crates: report
.crates
.iter()
.map(|c| CheckCrate {
name: c.name.clone(),
current: c.current.clone(),
latest: c.latest.clone(),
outdated: c.is_outdated(),
})
.collect(),
}
}
}
pub fn print<T: Serialize>(value: &T) -> Result<()> {
let mut out = serde_json::to_string_pretty(value).context("serializing JSON output")?;
out.push('\n');
std::io::stdout()
.write_all(out.as_bytes())
.context("writing JSON output")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::manifest::Entry;
use crate::report::Checked;
use std::path::Path;
fn v(s: &str) -> Version {
Version::parse(s).unwrap()
}
fn manifest() -> Manifest {
let mut m = Manifest::default();
for (name, version, bins, locked, pinned) in [
("bat", "0.26.0", vec!["bat"], false, true),
("fd", "10.3.0", vec!["fd"], true, false),
("ripgrep", "14.1.1", vec!["rg"], false, false),
] {
m.crates.insert(
name.to_owned(),
Entry {
version: version.to_owned(),
bins: bins.into_iter().map(str::to_owned).collect(),
locked,
pinned,
},
);
}
m
}
fn report() -> Report {
Report {
checked_at: 1_756_761_600,
prefix: PathBuf::from("/usr/local"),
crates: vec![
Checked {
name: "bat".to_owned(),
current: v("0.26.0"),
latest: v("0.26.1"),
},
Checked {
name: "fd".to_owned(),
current: v("10.2.0"),
latest: v("10.3.0"),
},
Checked {
name: "ripgrep".to_owned(),
current: v("14.1.1"),
latest: v("14.1.0"),
},
],
}
}
#[test]
fn list_output_golden() {
let out = ListOutput::build(PathBuf::from("/usr/local"), &manifest(), Some(&report()));
let json = serde_json::to_string_pretty(&out).unwrap();
let expected = r#"{
"schema": 1,
"prefix": "/usr/local",
"checked_at": 1756761600,
"crates": [
{
"name": "bat",
"version": "0.26.0",
"bins": [
"bat"
],
"locked": false,
"pinned": true,
"status": "outdated",
"latest": "0.26.1"
},
{
"name": "fd",
"version": "10.3.0",
"bins": [
"fd"
],
"locked": true,
"pinned": false,
"status": "unknown",
"latest": null
},
{
"name": "ripgrep",
"version": "14.1.1",
"bins": [
"rg"
],
"locked": false,
"pinned": false,
"status": "up_to_date",
"latest": "14.1.0"
}
]
}"#;
assert_eq!(json, expected);
}
#[test]
fn list_output_without_report_is_all_unknown() {
let out = ListOutput::build(PathBuf::from("/p"), &manifest(), None);
let value = serde_json::to_value(&out).unwrap();
assert_eq!(value["checked_at"], serde_json::Value::Null);
for c in value["crates"].as_array().unwrap() {
assert_eq!(c["status"], "unknown");
assert_eq!(c["latest"], serde_json::Value::Null);
}
let out = ListOutput::build(PathBuf::from("/p"), &Manifest::default(), None);
let value = serde_json::to_value(&out).unwrap();
assert_eq!(value["crates"], serde_json::json!([]));
assert_eq!(value["schema"], SCHEMA);
}
#[test]
fn check_output_golden() {
let out = CheckOutput::from_report(&report());
let json = serde_json::to_string_pretty(&out).unwrap();
let expected = r#"{
"schema": 1,
"prefix": "/usr/local",
"checked_at": 1756761600,
"crates": [
{
"name": "bat",
"current": "0.26.0",
"latest": "0.26.1",
"outdated": true
},
{
"name": "fd",
"current": "10.2.0",
"latest": "10.3.0",
"outdated": true
},
{
"name": "ripgrep",
"current": "14.1.1",
"latest": "14.1.0",
"outdated": false
}
]
}"#;
assert_eq!(json, expected);
}
#[test]
fn identity_is_what_prefix_reports() {
let a = crate::report::identity(Path::new("/usr/local/")).unwrap();
let b = crate::report::identity(Path::new("/usr/./local")).unwrap();
assert_eq!(a, b);
}
}