use std::path::{Path, PathBuf};
pub const TOOL_VERSION: &str = env!("CARGO_PKG_VERSION");
pub const FIRST_PUBLISHED: (u64, u64, u64) = (0, 13, 0);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Verdict {
Ok,
Degraded { app: String, tool: String },
Refuse { app: String, tool: String },
}
impl Verdict {
pub fn may_answer(&self) -> bool {
!matches!(self, Verdict::Refuse { .. })
}
pub fn note(&self) -> Option<String> {
match self {
Verdict::Ok | Verdict::Refuse { .. } => None,
Verdict::Degraded { app, tool } => Some(degraded_text(app, tool)),
}
}
}
pub fn check(app_version: &str) -> Verdict {
let tool = TOOL_VERSION;
if app_version == tool {
return Verdict::Ok;
}
match (semver_parts(app_version), semver_parts(tool)) {
(Some((a_major, a_minor, _)), Some((t_major, t_minor, _)))
if a_major == t_major && a_minor == t_minor =>
{
Verdict::Degraded {
app: app_version.to_string(),
tool: tool.to_string(),
}
}
_ => Verdict::Refuse {
app: app_version.to_string(),
tool: tool.to_string(),
},
}
}
fn semver_parts(v: &str) -> Option<(u64, u64, u64)> {
let core = v.split(['-', '+']).next()?;
let mut it = core.split('.');
let major = it.next()?.parse().ok()?;
let minor = it.next()?.parse().ok()?;
let patch = it.next().unwrap_or("0").parse().ok()?;
if it.next().is_some() {
return None;
}
Some((major, minor, patch))
}
pub fn readable_sources(resolution: &crate::resolve::Resolution) -> Option<PathBuf> {
if let Some(root) = resolution.checkout_root() {
return Some(root);
}
let widgets = resolution.widgets()?;
widgets.has_src().then(|| widgets.dir.clone())
}
pub fn refusal_text(app: &str, tool: &str, what: &str, sources: Option<&Path>) -> String {
let help = match semver_parts(app) {
Some(parts) if parts < FIRST_PUBLISHED => {
"use the resolved crate sources, or upgrade to Teksilo 0.13.0 or later".to_string()
}
Some(_) => format!("cargo install cargo-teksilo --version {app} --locked"),
None => "install cargo-teksilo from the matching framework checkout".to_string(),
};
let mut message = format!("tool {tool} cannot serve {what} for Teksilo {app}\nhelp: {help}");
if crate::output::verbose() {
if let Some(path) = sources {
message.push_str(&format!("\nSources: {}", path.display()));
}
message.push_str("\nFor unpublished versions: cargo install --path <checkout>/crates/cargo-teksilo --locked");
}
message
}
pub fn degraded_text(app: &str, tool: &str) -> String {
format!("note: app uses Teksilo {app}; tool uses {tool} (compatible patch versions)")
}
#[cfg(test)]
mod tests {
use super::*;
fn refusal(app: &str, tool: &str, what: &str) -> String {
refusal_text(app, tool, what, None)
}
#[test]
fn an_exact_match_answers() {
assert_eq!(check(TOOL_VERSION), Verdict::Ok);
}
#[test]
fn a_patch_difference_degrades_rather_than_refusing() {
let (major, minor, patch) = semver_parts(TOOL_VERSION).unwrap();
let other = format!("{major}.{minor}.{}", patch + 1);
let v = check(&other);
assert!(matches!(v, Verdict::Degraded { .. }), "got {v:?}");
assert!(v.may_answer());
}
#[test]
fn a_minor_difference_refuses() {
let (major, minor, _) = semver_parts(TOOL_VERSION).unwrap();
let other = format!("{major}.{}.0", minor + 1);
let v = check(&other);
assert!(matches!(v, Verdict::Refuse { .. }), "got {v:?}");
assert!(!v.may_answer());
}
#[test]
fn a_major_difference_refuses() {
let (major, _, _) = semver_parts(TOOL_VERSION).unwrap();
let v = check(&format!("{}.0.0", major + 1));
assert!(matches!(v, Verdict::Refuse { .. }), "got {v:?}");
}
#[test]
fn the_0_9_to_0_12_case_refuses() {
assert!(
matches!(check("0.9.2"), Verdict::Refuse { .. }) || TOOL_VERSION.starts_with("0.9")
);
}
#[test]
fn unparseable_versions_refuse_rather_than_guess() {
assert!(matches!(check("not-a-version"), Verdict::Refuse { .. }));
assert!(matches!(check(""), Verdict::Refuse { .. }));
assert!(matches!(check("1.2.3.4"), Verdict::Refuse { .. }));
}
#[test]
fn prerelease_parses_to_its_core() {
assert_eq!(semver_parts("2.0.0-rc.13"), Some((2, 0, 0)));
assert_eq!(semver_parts("1.2.3+build"), Some((1, 2, 3)));
}
#[test]
fn refusals_give_one_usable_remedy() {
let old = refusal("0.12.1", TOOL_VERSION, "search");
assert!(!old.contains("cargo install"));
assert!(old.contains("0.13.0"));
let published = refusal("0.14.0", "0.13.0", "search");
assert!(published.contains("cargo install cargo-teksilo --version 0.14.0 --locked"));
assert_eq!(published.lines().count(), 2);
assert!(!refusal("unknown", TOOL_VERSION, "search").contains("--version"));
}
#[test]
fn the_floor_is_never_above_this_binarys_own_version() {
let tool = semver_parts(TOOL_VERSION).expect("this crate's own version is a semver triple");
assert!(
tool >= FIRST_PUBLISHED,
"TOOL_VERSION {TOOL_VERSION} is below FIRST_PUBLISHED {FIRST_PUBLISHED:?}"
);
}
#[test]
fn the_degraded_note_names_both_versions() {
let t = degraded_text("0.12.0", "0.12.1");
assert!(t.contains("0.12.0") && t.contains("0.12.1"));
}
}