diff --git a/Cargo.lock b/Cargo.lock index 0992542..f74463b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,6 +126,7 @@ dependencies = [ "reqwest", "rodio", "rpassword", + "semver", "serde", "serde_json", "tokio", @@ -1394,6 +1395,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" diff --git a/Cargo.toml b/Cargo.toml index bbb8cec..de70406 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ rpassword = "7.4.0" reqwest = { version = "0.12.20", default-features = false, features = ["json", "rustls-tls"] } serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" +semver = "1.0.26" tokio = { version = "1.45.1", features = ["macros", "rt-multi-thread"] } toml = "0.8.23" url = "2.5.4" diff --git a/src/api.rs b/src/api.rs index fe0bc2c..8aa5fd1 100644 --- a/src/api.rs +++ b/src/api.rs @@ -282,6 +282,8 @@ pub struct UserWithToken { pub struct SystemInfo { pub version: VersionInfo, pub min_app_version: VersionInfo, + #[serde(default)] + pub min_cli_version: Option, } #[derive(Debug, Deserialize, Serialize)] @@ -302,6 +304,11 @@ impl VersionInfo { _ => format!("{}.{}.{}", self.major, self.minor, self.patch), } } + + pub fn semver(&self) -> Result { + semver::Version::parse(&self.display()) + .with_context(|| format!("invalid version returned by server: {}", self.display())) + } } #[derive(Debug, Default)] @@ -435,4 +442,16 @@ mod tests { "http://localhost:3000/" ); } + + #[test] + fn parses_prerelease_versions() { + let version = VersionInfo { + major: 1, + minor: 2, + patch: 3, + pre: Some("rc.1".to_owned()), + }; + + assert_eq!(version.semver().unwrap().to_string(), "1.2.3-rc.1"); + } } diff --git a/src/main.rs b/src/main.rs index 55ddf60..3327177 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ mod player; use anyhow::{Context, Result, bail}; use clap::{Args, Parser, Subcommand}; +use semver::Version; use std::io::{self, Write}; use crate::{ @@ -215,6 +216,7 @@ async fn login(args: LoginArgs) -> Result<()> { }; let client = ApiClient::new(&server, None)?; + ensure_supported_cli_version(&client).await?; let response = client.login(&email, &password).await?; let config = Config { @@ -237,6 +239,25 @@ async fn login(args: LoginArgs) -> Result<()> { Ok(()) } +async fn ensure_supported_cli_version(client: &ApiClient) -> Result<()> { + let Some(minimum) = client.system().await?.min_cli_version else { + return Ok(()); + }; + let current = Version::parse(env!("CARGO_PKG_VERSION")) + .expect("CARGO_PKG_VERSION must be a valid semantic version"); + let minimum_version = minimum.semver()?; + + if current < minimum_version { + bail!( + "this server requires Black Candy CLI {} or later; you are running {}. Please upgrade and try again.", + minimum.display(), + current + ); + } + + Ok(()) +} + fn prompt_line(prompt: &str) -> Result { print!("{prompt}"); io::stdout().flush()?; @@ -274,6 +295,9 @@ async fn run_authenticated(command: Command, client: ApiClient) -> Result<()> { } else { println!("Server: {}", system.version.display()); println!("Minimum app version: {}", system.min_app_version.display()); + if let Some(minimum) = system.min_cli_version { + println!("Minimum CLI version: {}", minimum.display()); + } } } Command::Search { query, json } => {