1use ansi_term::Color::{Blue, Green, Red, Yellow};
2use chrono::{DateTime, Utc};
3use configstore::{AppUI, Configstore};
4use curl::easy::{Easy, List};
5use serde_derive::{Deserialize, Serialize};
6use std::time::Duration;
7
8const REGISTRY_URL: &str = "https://crates.io";
9
10#[cfg(test)]
11use mockito;
12
13fn get_base_url() -> String {
14 #[cfg(not(test))]
15 let url = format!("{}/api/v1/crates", REGISTRY_URL);
16 #[cfg(test)]
17 let url = format!("{}/api/v1/crates", mockito::server_url());
18 url
19}
20
21#[derive(Debug, thiserror::Error)]
22enum ErrorKind {
23 #[error("Error while parsing json: {0}")]
24 UnableToParseJson(String),
25 #[error("Error received from registry: {0}")]
26 RegistryError(String),
27}
28
29#[derive(Deserialize, Debug, Clone)]
30struct VersionResponse {
31 versions: Option<Vec<Version>>,
32 errors: Option<Vec<JsonError>>,
33}
34
35#[derive(Deserialize, Debug, Clone)]
36struct JsonError {
37 detail: String,
38}
39
40#[derive(Deserialize, Debug, Clone)]
41struct Version {
42 num: String,
43}
44
45fn get_latest_from_json(
46 resp: &VersionResponse,
47) -> std::result::Result<String, Box<dyn std::error::Error>> {
48 if let Some(versions) = &resp.versions {
49 match versions.first() {
50 Some(version) => Ok(version.num.clone()),
51 None => Err(ErrorKind::UnableToParseJson("Versions array is empty".to_string()).into()),
52 }
53 } else if let Some(errors) = &resp.errors {
54 match errors.first() {
55 Some(error) => Err(ErrorKind::RegistryError(error.detail.clone()).into()),
56 None => Err(
57 ErrorKind::UnableToParseJson("No errors in the errors array".to_string()).into(),
58 ),
59 }
60 } else {
61 Err(ErrorKind::UnableToParseJson(
62 "Invalid json response, does not have versions or errors".to_string(),
63 )
64 .into())
65 }
66}
67
68fn get_latest_version(crate_name: &str) -> std::result::Result<String, Box<dyn std::error::Error>> {
69 let mut easy = Easy::new();
72 let base_url = get_base_url();
73 let url = format!("{}/{}/versions", base_url, crate_name);
74 easy.url(&url)?;
75 let mut list = List::new();
76 list.append("USER-AGENT Update-notifier (teshaq@mozilla.com)")?;
77 easy.http_headers(list)?;
78 let mut resp_buf = Vec::new();
79 {
82 let mut transfer = easy.transfer();
83 transfer.write_function(|data| {
84 resp_buf.extend_from_slice(data);
85 Ok(data.len())
86 })?;
87 transfer.perform()?;
88 }
89 let resp = std::str::from_utf8(&resp_buf)?;
90
91 let json_resp: VersionResponse = serde_json::from_str(resp)?;
92 get_latest_from_json(&json_resp)
93}
94
95fn generate_notice(name: &str, current_version: &str, latest_version: &str) -> String {
96 let line_1 = format!(
97 "A new version of {} is available! {} → {}",
98 Green.bold().paint(name),
99 Red.bold().paint(current_version),
100 Green.bold().paint(latest_version)
101 );
102 let line_2 = format!(
103 "Use `{}` to install version {}",
104 Blue.bold().paint(format!("cargo install {}", name)),
105 Green.bold().paint(latest_version)
106 );
107 let line_3 = format!(
108 "Check {} for more details",
109 Yellow.paint(format!("{}/crates/{}", REGISTRY_URL, name))
110 );
111 let mut border_line = String::from("\n───────────────────────────────────────────────────────");
112 let extension = "─";
113 for _ in 0..name.len() {
114 border_line.push_str(extension);
115 }
116 border_line.push('\n');
117 format!(
118 "{}
119 {}
120 {}
121 {}
122 {}",
123 border_line, line_1, line_2, line_3, border_line
124 )
125}
126
127fn print_notice(name: &str, current_version: &str, latest_version: &str) {
128 print!("{}", generate_notice(name, current_version, latest_version));
129}
130
131#[derive(Deserialize, Serialize)]
132struct Config {
133 last_checked: DateTime<Utc>,
134}
135
136fn get_app_name(name: &str) -> String {
137 let mut app_name: String = String::from(name);
138 app_name.push_str("-update-notifier");
139 #[cfg(test)]
140 app_name.push_str("-test");
141 app_name
142}
143
144fn update_time(date_time: DateTime<Utc>, name: &str) -> Result<(), Box<dyn std::error::Error>> {
145 let config = Config {
146 last_checked: date_time,
147 };
148 let config_store = Configstore::new(&get_app_name(name), AppUI::CommandLine)?;
149 config_store.set("config", config)?;
150 Ok(())
151}
152
153fn compare_with_latest(
154 name: &str,
155 current_version: &str,
156) -> Result<(), Box<dyn std::error::Error>> {
157 let latest_version = get_latest_version(name)?;
158 if latest_version != current_version {
159 print_notice(name, current_version, &latest_version);
160 }
161 let date_time = Utc::now();
162 update_time(date_time, name)
163}
164
165pub fn check_version(
190 name: &str,
191 current_version: &str,
192 interval: Duration,
193) -> Result<(), Box<dyn std::error::Error>> {
194 let date_time_now = Utc::now();
195 let config_store = Configstore::new(&get_app_name(name), AppUI::CommandLine)?;
196 match config_store.get::<Config>("config") {
197 Ok(config) => {
198 let prev_time = config.last_checked;
199 let duration_interval = chrono::Duration::from_std(interval)?;
200 if date_time_now.signed_duration_since(prev_time) >= duration_interval {
201 compare_with_latest(name, current_version)
202 } else {
203 Ok(())
204 }
205 }
206 Err(_) => compare_with_latest(name, current_version),
207 }
208}
209
210#[cfg(test)]
211mod tests;