1use std::time::Duration;
15
16const CRATE_NAME: &str = "koda-cli";
17const CRATES_IO_URL: &str = "https://crates.io/api/v1/crates/koda-cli";
18const CHECK_TIMEOUT: Duration = Duration::from_secs(3);
19
20pub fn spawn_version_check() -> tokio::task::JoinHandle<Option<String>> {
22 tokio::spawn(async move { check_latest_version().await })
23}
24
25pub fn update_available(latest: &str) -> Option<(&'static str, String)> {
28 let current = env!("CARGO_PKG_VERSION");
29 if latest != current && is_newer(latest, current) {
30 Some((current, latest.to_string()))
31 } else {
32 None
33 }
34}
35
36pub fn crate_name() -> &'static str {
38 CRATE_NAME
39}
40
41async fn check_latest_version() -> Option<String> {
43 let client = reqwest::Client::builder()
44 .timeout(CHECK_TIMEOUT)
45 .build()
46 .ok()?;
47
48 let resp = client
49 .get(CRATES_IO_URL)
50 .header(
51 "User-Agent",
52 format!("Koda/{} (version-check)", env!("CARGO_PKG_VERSION")),
53 )
54 .send()
55 .await
56 .ok()?;
57
58 if !resp.status().is_success() {
59 return None;
60 }
61
62 let body: serde_json::Value = resp.json().await.ok()?;
63 body.get("crate")?
64 .get("max_version")?
65 .as_str()
66 .map(|s| s.to_string())
67}
68
69fn is_newer(a: &str, b: &str) -> bool {
71 let parse = |s: &str| -> Vec<u64> { s.split('.').filter_map(|p| p.parse().ok()).collect() };
72 let va = parse(a);
73 let vb = parse(b);
74 va > vb
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn test_is_newer() {
83 assert!(is_newer("0.2.0", "0.1.0"));
84 assert!(is_newer("1.0.0", "0.9.9"));
85 assert!(is_newer("0.1.1", "0.1.0"));
86 assert!(!is_newer("0.1.0", "0.1.0"));
87 assert!(!is_newer("0.1.0", "0.2.0"));
88 }
89
90 #[test]
91 fn test_is_newer_same_version() {
92 assert!(!is_newer("0.1.0", "0.1.0"));
93 }
94
95 #[test]
96 fn test_is_newer_single_component() {
97 assert!(is_newer("2", "1"));
98 assert!(!is_newer("1", "2"));
99 }
100
101 #[test]
102 fn test_is_newer_patch_only() {
103 assert!(is_newer("0.1.10", "0.1.9"));
104 assert!(!is_newer("0.1.9", "0.1.10"));
105 }
106
107 #[test]
108 fn test_crate_name() {
109 assert_eq!(crate_name(), "koda-cli");
110 }
111
112 #[test]
113 fn test_update_available_same_version_returns_none() {
114 let current = env!("CARGO_PKG_VERSION");
115 assert!(update_available(current).is_none());
117 }
118
119 #[test]
120 fn test_update_available_older_version_returns_none() {
121 assert!(update_available("0.0.1").is_none());
123 }
124
125 #[test]
126 fn test_update_available_future_version_returns_some() {
127 let result = update_available("999.0.0");
129 assert!(result.is_some());
130 let (current, latest) = result.unwrap();
131 assert_eq!(latest, "999.0.0");
132 assert_eq!(current, env!("CARGO_PKG_VERSION"));
133 }
134}