use std::cmp::Ordering;
use std::path::Path;
pub const STAMP_FILE: &str = ".version";
fn parse(v: &str) -> Vec<u64> {
v.trim()
.trim_start_matches('v')
.split(['.', '-', '+'])
.map(|s| s.parse::<u64>().ok())
.take_while(Option::is_some)
.flatten()
.collect()
}
pub fn compare_versions(a: &str, b: &str) -> Ordering {
let (mut pa, mut pb) = (parse(a), parse(b));
let n = pa.len().max(pb.len());
pa.resize(n, 0);
pb.resize(n, 0);
pa.cmp(&pb)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SkillState {
NotInstalled,
Missing,
Unstamped,
Current { version: String },
Stale { installed: String, package: String },
Newer { installed: String, package: String },
}
impl SkillState {
pub fn is_downgrade(&self) -> bool {
matches!(self, SkillState::Newer { .. })
}
}
pub fn read_stamp(skill_dir: &Path) -> Option<String> {
std::fs::read_to_string(skill_dir.join(STAMP_FILE))
.ok()
.map(|s| s.trim().to_string())
}
pub fn write_stamp(skill_dir: &Path, version: &str) -> std::io::Result<()> {
std::fs::write(skill_dir.join(STAMP_FILE), format!("{}\n", version.trim()))
}
pub fn skill_state(skill_dir: &Path, package_version: &str) -> SkillState {
if !skill_dir.join("SKILL.md").exists() {
return if skill_dir.exists() {
SkillState::Missing
} else {
SkillState::NotInstalled
};
}
match read_stamp(skill_dir) {
None => SkillState::Unstamped,
Some(installed) => match compare_versions(&installed, package_version) {
Ordering::Less => SkillState::Stale {
installed,
package: package_version.to_string(),
},
Ordering::Greater => SkillState::Newer {
installed,
package: package_version.to_string(),
},
Ordering::Equal => SkillState::Current { version: installed },
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn version_ordering() {
assert_eq!(compare_versions("1.1.0", "1.1"), Ordering::Equal);
assert_eq!(compare_versions("1.1.23", "1.1.24"), Ordering::Less);
assert_eq!(compare_versions("2.0.0", "1.9.9"), Ordering::Greater);
assert_eq!(compare_versions("v1.2.3", "1.2.3"), Ordering::Equal);
assert_eq!(compare_versions("1.2.3-beta", "1.2.3"), Ordering::Equal);
}
#[test]
fn states() {
let tmp = std::env::temp_dir().join(format!("skgen-state-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
assert_eq!(skill_state(&tmp, "1.0.0"), SkillState::NotInstalled);
std::fs::create_dir_all(&tmp).unwrap();
assert_eq!(skill_state(&tmp, "1.0.0"), SkillState::Missing);
std::fs::write(tmp.join("SKILL.md"), "x").unwrap();
assert_eq!(skill_state(&tmp, "1.0.0"), SkillState::Unstamped);
write_stamp(&tmp, "1.0.0").unwrap();
assert!(matches!(
skill_state(&tmp, "1.0.0"),
SkillState::Current { .. }
));
assert!(matches!(
skill_state(&tmp, "1.1.0"),
SkillState::Stale { .. }
));
let newer = skill_state(&tmp, "0.9.0");
assert!(newer.is_downgrade(), "got {newer:?}");
let _ = std::fs::remove_dir_all(&tmp);
}
}