use std::cmp::Ordering;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Warning {
pub kind: WarningKind,
pub name: String,
pub message: Option<String>,
pub warn_at: Option<String>,
pub remove_at: Option<String>,
pub replacement: Option<String>,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum WarningKind {
DeprecatedFlag,
DeprecatedCommand,
DeprecatedEnv,
#[default]
Other,
}
impl Warning {
pub fn flag(
name: impl Into<String>,
message: Option<String>,
warn_at: Option<String>,
remove_at: Option<String>,
) -> Self {
Self {
kind: WarningKind::DeprecatedFlag,
name: name.into(),
message,
warn_at,
remove_at,
replacement: None,
}
}
pub fn command(
name: impl Into<String>,
message: Option<String>,
warn_at: Option<String>,
remove_at: Option<String>,
) -> Self {
Self {
kind: WarningKind::DeprecatedCommand,
name: name.into(),
message,
warn_at,
remove_at,
replacement: None,
}
}
pub fn env(name: impl Into<String>, replacement: Option<String>) -> Self {
Self {
kind: WarningKind::DeprecatedEnv,
name: name.into(),
message: None,
warn_at: None,
remove_at: None,
replacement,
}
}
pub fn render(&self) -> String {
let subject = match self.kind {
WarningKind::DeprecatedCommand => format!("the {} command", self.name),
_ => self.name.clone(),
};
let mut out = format!("warning: {subject} is deprecated");
if let Some(at) = &self.remove_at {
out.push_str(&format!(", removed at {at}"));
}
match (&self.message, &self.replacement) {
(Some(message), _) => out.push_str(&format!(": {message}")),
(None, Some(replacement)) => out.push_str(&format!(": use {replacement}")),
(None, None) => {}
}
out.push('\n');
out
}
}
pub fn version_reaches(current: Option<&str>, warn_at: Option<&str>) -> bool {
let (Some(warn_at), Some(current)) = (warn_at, current) else {
return true;
};
!matches!(compare(current, warn_at), Some(Ordering::Less))
}
pub fn compare(a: &str, b: &str) -> Option<Ordering> {
let (a_core, a_pre) = split(a);
let (b_core, b_pre) = split(b);
let mut a_segments = a_core.split('.');
let mut b_segments = b_core.split('.');
loop {
let (a_next, b_next) = (a_segments.next(), b_segments.next());
if a_next.is_none() && b_next.is_none() {
break;
}
match segment(a_next)?.cmp(&segment(b_next)?) {
Ordering::Equal => continue,
ordering => return Some(ordering),
}
}
Some(match (a_pre, b_pre) {
(None, None) => Ordering::Equal,
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(Some(a), Some(b)) => a.cmp(b),
})
}
fn split(version: &str) -> (&str, Option<&str>) {
let version = version.split('+').next().unwrap_or(version);
match version.split_once('-') {
Some((core, pre)) => (core, Some(pre)),
None => (version, None),
}
}
fn segment(segment: Option<&str>) -> Option<u64> {
match segment {
None => Some(0),
Some(text) => text.parse().ok(),
}
}
pub fn retain_reached(warnings: &mut Vec<Warning>, version: Option<&str>) {
warnings.retain(|warning| version_reaches(version, warning.warn_at.as_deref()));
}
pub fn render(warnings: &[Warning]) -> String {
warnings.iter().map(Warning::render).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_gate_matches_the_rule() {
assert!(version_reaches(Some("1.0.0"), None));
assert!(version_reaches(None, Some("2.0.0")));
assert!(!version_reaches(Some("1.0.0"), Some("2.0.0")));
assert!(version_reaches(Some("2.0.0"), Some("2.0.0")));
assert!(!version_reaches(Some("2.0.0-rc.1"), Some("2.0.0")));
assert!(version_reaches(Some("nightly"), Some("2.0.0")));
assert_eq!(compare("2026.12", "2026.12.0"), Some(Ordering::Equal));
assert_eq!(compare("1.0.0+abc", "1.0.0+def"), Some(Ordering::Equal));
assert_eq!(compare("nightly", "1.0.0"), None);
}
#[test]
fn a_warning_says_what_to_do_about_it() {
assert_eq!(
Warning::flag(
"--old",
Some("use --new".into()),
None,
Some("2.0.0".into())
)
.render(),
"warning: --old is deprecated, removed at 2.0.0: use --new\n",
);
assert_eq!(
Warning::command("old", None, None, None).render(),
"warning: the old command is deprecated\n",
);
assert_eq!(
Warning::env("OLD_TOKEN", Some("APP_TOKEN".into())).render(),
"warning: OLD_TOKEN is deprecated: use APP_TOKEN\n",
);
}
}