use serde::Serialize;
use crate::output::CliReport;
use crate::output::human::Human;
#[derive(Debug, Serialize)]
pub struct UpdateReport {
pub status: &'static str,
pub current_version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub update_available: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub install_method: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub upgrade_command: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub daemon: Option<&'static str>,
}
impl UpdateReport {
pub fn delegated(
current_version: String,
install_method: &'static str,
upgrade_command: String,
) -> Self {
Self {
status: "delegated",
current_version,
update_available: None,
target_version: None,
install_method: Some(install_method),
upgrade_command: Some(upgrade_command),
daemon: None,
}
}
pub fn checked(
current_version: String,
available: bool,
target_version: Option<String>,
) -> Self {
Self {
status: "checked",
current_version,
update_available: Some(available),
target_version,
install_method: None,
upgrade_command: None,
daemon: None,
}
}
pub fn aborted(current_version: String) -> Self {
Self {
status: "aborted",
current_version,
update_available: None,
target_version: None,
install_method: None,
upgrade_command: None,
daemon: None,
}
}
pub fn unchanged(current_version: String) -> Self {
Self {
status: "unchanged",
current_version,
update_available: None,
target_version: None,
install_method: None,
upgrade_command: None,
daemon: None,
}
}
pub fn updated(
current_version: String,
new_version: String,
daemon: Option<&'static str>,
) -> Self {
Self {
status: "updated",
current_version,
update_available: None,
target_version: Some(new_version),
install_method: None,
upgrade_command: None,
daemon,
}
}
}
impl CliReport for UpdateReport {
fn render(&self, h: &mut Human<'_>) -> std::io::Result<()> {
match self.status {
"delegated" => {
let how = match self.install_method {
Some("homebrew") => "Homebrew",
Some("cargo") => "Cargo",
_ => "your package manager",
};
h.line(&format!(
"bitrouter looks installed via {how}. Update with:"
))?;
if let Some(cmd) = &self.upgrade_command {
h.line(&format!(" {cmd}"))?;
}
Ok(())
}
"checked" => {
if self.update_available == Some(true) {
match &self.target_version {
Some(v) => h.line(&format!(
"update available: {} -> {v}",
self.current_version
)),
None => h.line("update available (target version unknown)"),
}
} else {
h.line(&format!("up to date ({})", self.current_version))
}
}
"aborted" => h.line("aborted"),
"unchanged" => h.line(&format!("already up to date ({})", self.current_version)),
"updated" => {
let new = self.target_version.as_deref().unwrap_or("(unknown)");
h.line(&format!("✓ updated {} -> {new}", self.current_version))?;
match self.daemon {
Some("restarted") => {
h.note(&format!("Restarted the running daemon on {new}."))
}
Some("restart_needed") => h.note(&format!(
"A daemon is running the old binary. Run `bitrouter restart` to serve {new}."
)),
_ => Ok(()),
}
}
other => h.line(other),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::output::{Format, Output};
fn json(r: &dyn CliReport) -> serde_json::Value {
serde_json::from_slice(&Output::new(Format::Json).render_to_vec(r)).unwrap()
}
fn human(r: &dyn CliReport) -> String {
String::from_utf8(Output::new(Format::Human).render_to_vec(r)).unwrap()
}
#[test]
fn delegated_carries_method_and_command() {
let r = UpdateReport::delegated(
"1.0.0-alpha.19".into(),
"homebrew",
"brew upgrade bitrouter".into(),
);
assert_eq!(
json(&r),
serde_json::json!({
"status": "delegated",
"current_version": "1.0.0-alpha.19",
"install_method": "homebrew",
"upgrade_command": "brew upgrade bitrouter"
})
);
let h = human(&r);
assert!(h.contains("looks installed via Homebrew"), "{h:?}");
assert!(h.contains(" brew upgrade bitrouter"), "{h:?}");
}
#[test]
fn checked_available_names_target_version() {
let r = UpdateReport::checked("1.0.0-alpha.19".into(), true, Some("1.0.0-alpha.20".into()));
assert_eq!(
json(&r),
serde_json::json!({
"status": "checked",
"current_version": "1.0.0-alpha.19",
"update_available": true,
"target_version": "1.0.0-alpha.20"
})
);
assert_eq!(
human(&r),
"update available: 1.0.0-alpha.19 -> 1.0.0-alpha.20\n"
);
}
#[test]
fn checked_up_to_date_omits_target() {
let r = UpdateReport::checked("1.0.0-alpha.19".into(), false, None);
assert_eq!(
json(&r),
serde_json::json!({
"status": "checked",
"current_version": "1.0.0-alpha.19",
"update_available": false
})
);
assert_eq!(human(&r), "up to date (1.0.0-alpha.19)\n");
}
#[test]
fn checked_available_but_target_unknown() {
let r = UpdateReport::checked("1.0.0-alpha.19".into(), true, None);
assert_eq!(
json(&r),
serde_json::json!({
"status": "checked",
"current_version": "1.0.0-alpha.19",
"update_available": true
})
);
assert_eq!(human(&r), "update available (target version unknown)\n");
}
#[test]
fn updated_without_running_daemon_omits_daemon_key() {
let r = UpdateReport::updated("1.0.0-alpha.19".into(), "1.0.0-alpha.20".into(), None);
assert_eq!(
json(&r),
serde_json::json!({
"status": "updated",
"current_version": "1.0.0-alpha.19",
"target_version": "1.0.0-alpha.20"
})
);
assert_eq!(human(&r), "✓ updated 1.0.0-alpha.19 -> 1.0.0-alpha.20\n");
}
#[test]
fn updated_with_restart_hint_renders_note() {
let r = UpdateReport::updated(
"1.0.0-alpha.19".into(),
"1.0.0-alpha.20".into(),
Some("restart_needed"),
);
assert_eq!(
json(&r),
serde_json::json!({
"status": "updated",
"current_version": "1.0.0-alpha.19",
"target_version": "1.0.0-alpha.20",
"daemon": "restart_needed"
})
);
let h = human(&r);
assert!(
h.contains("✓ updated 1.0.0-alpha.19 -> 1.0.0-alpha.20"),
"{h:?}"
);
assert!(h.contains("Run `bitrouter restart`"), "{h:?}");
}
#[test]
fn updated_restarted_omits_hint() {
let r = UpdateReport::updated(
"1.0.0-alpha.19".into(),
"1.0.0-alpha.20".into(),
Some("restarted"),
);
let h = human(&r);
assert!(
h.contains("Restarted the running daemon on 1.0.0-alpha.20"),
"{h:?}"
);
}
#[test]
fn unchanged_and_aborted_are_one_liners() {
assert_eq!(
human(&UpdateReport::unchanged("1.0.0-alpha.19".into())),
"already up to date (1.0.0-alpha.19)\n"
);
assert_eq!(
human(&UpdateReport::aborted("1.0.0-alpha.19".into())),
"aborted\n"
);
assert_eq!(
json(&UpdateReport::aborted("1.0.0-alpha.19".into())),
serde_json::json!({"status": "aborted", "current_version": "1.0.0-alpha.19"})
);
}
}