update_informer/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use crate::{
4    http_client::{DefaultHttpClient, HttpClient},
5    version_file::VersionFile,
6};
7use std::time::Duration;
8
9pub use package::Package;
10pub use registry::Registry;
11pub use version::Version;
12
13mod package;
14mod version;
15mod version_file;
16
17#[cfg(test)]
18mod test_helper;
19
20/// A registry service that stores information about releases.
21pub mod registry;
22
23/// An HTTP client to send requests to the registry.
24pub mod http_client;
25
26type Error = Box<dyn std::error::Error>;
27pub type Result<T> = std::result::Result<T, Error>;
28
29pub trait Check {
30    /// Checks for a new version in the registry.
31    fn check_version(self) -> Result<Option<Version>>
32    where
33        Self: Sized,
34    {
35        Ok(None)
36    }
37}
38
39/// Checks for a new version on Crates.io, GitHub, Npm or PyPi.
40///
41/// A cache file handled by the instance throttles the number of actual update checks, or you can opt in to manage this yourself.
42pub struct UpdateInformer<
43    R: Registry,
44    N: AsRef<str>,
45    V: AsRef<str>,
46    H: HttpClient = DefaultHttpClient,
47> {
48    _registry: R,
49    name: N,
50    version: V,
51    http_client: H,
52    interval: Duration,
53    timeout: Duration,
54}
55
56/// Constructs a new `UpdateInformer`.
57///
58/// # Arguments
59///
60/// * `registry` - A registry service such as Crates.io or GitHub.
61/// * `name` - A project name.
62/// * `version` - Current version of the project.
63///
64/// # Examples
65///
66/// ```rust
67/// use update_informer::{registry, Check};
68///
69/// let name = env!("CARGO_PKG_NAME");
70/// let version = env!("CARGO_PKG_VERSION");
71/// let informer = update_informer::new(registry::Crates, name, version);
72/// ```
73pub fn new<R, N, V>(registry: R, name: N, version: V) -> UpdateInformer<R, N, V>
74where
75    R: Registry,
76    N: AsRef<str>,
77    V: AsRef<str>,
78{
79    UpdateInformer {
80        _registry: registry,
81        name,
82        version,
83        http_client: DefaultHttpClient {},
84        interval: Duration::from_secs(60 * 60 * 24), // Once a day
85        timeout: Duration::from_secs(5),
86    }
87}
88
89impl<R, N, V, H> UpdateInformer<R, N, V, H>
90where
91    R: Registry,
92    N: AsRef<str>,
93    V: AsRef<str>,
94    H: HttpClient,
95{
96    /// Sets the interval of how often to check for a new version.
97    ///
98    /// # Arguments
99    ///
100    /// * `interval` - The `Duration` after an actual update check during which a subsequent check will be skipped. 24 hours by default. This is implemented using a cache file. Specify `Duration::ZERO` to work without a cache file and unconditionally perform the update check.
101    ///
102    /// # Examples
103    ///
104    /// ```rust
105    /// use std::time::Duration;
106    /// use update_informer::{registry, Check};
107    ///
108    /// const EVERY_HOUR: Duration = Duration::from_secs(60 * 60);
109    ///
110    /// let informer = update_informer::new(registry::Crates, "crate_name", "0.1.0").interval(EVERY_HOUR);
111    /// let _ = informer.check_version(); // The check will start only after an hour
112    /// ```
113    pub fn interval(self, interval: Duration) -> Self {
114        Self { interval, ..self }
115    }
116
117    /// Sets a request timeout.
118    ///
119    /// # Arguments
120    ///
121    /// * `timeout` - A request timeout. By default, it is 5 seconds.
122    ///
123    /// # Examples
124    ///
125    /// ```rust
126    /// use std::time::Duration;
127    /// use update_informer::{registry, Check};
128    ///
129    /// const THIRTY_SECONDS: Duration = Duration::from_secs(30);
130    ///
131    /// let informer = update_informer::new(registry::Crates, "crate_name", "0.1.0").timeout(THIRTY_SECONDS);
132    /// let _ = informer.check_version();
133    /// ```
134    pub fn timeout(self, timeout: Duration) -> Self {
135        Self { timeout, ..self }
136    }
137
138    /// Sets an HTTP client to send request to the registry.
139    ///
140    /// # Arguments
141    ///
142    /// * `http_client` - A type that implements the `HttpClient` trait.
143    ///
144    /// # Examples
145    ///
146    /// ```rust
147    /// use isahc::ReadResponseExt;
148    /// use std::time::Duration;
149    /// use serde::de::DeserializeOwned;
150    /// use update_informer::{http_client::{HeaderMap, HttpClient}, registry, Check};
151    ///
152    /// struct YourOwnHttpClient;
153    ///
154    /// impl HttpClient for YourOwnHttpClient {
155    ///     fn get<T: DeserializeOwned>(
156    ///         url: &str,
157    ///         _timeout: Duration,
158    ///         _headers: HeaderMap,
159    ///     ) -> update_informer::Result<T> {
160    ///         let json = isahc::get(url)?.json()?;
161    ///         Ok(json)
162    ///     }
163    /// }
164    ///
165    /// let informer = update_informer::new(registry::Crates, "crate_name", "0.1.0").http_client(YourOwnHttpClient);
166    /// let _ = informer.check_version();
167    /// ```
168    pub fn http_client<C: HttpClient>(self, http_client: C) -> UpdateInformer<R, N, V, C> {
169        UpdateInformer {
170            _registry: self._registry,
171            name: self.name,
172            version: self.version,
173            interval: self.interval,
174            timeout: self.timeout,
175            http_client,
176        }
177    }
178}
179
180impl<R, N, V, H> Check for UpdateInformer<R, N, V, H>
181where
182    R: Registry,
183    N: AsRef<str>,
184    V: AsRef<str>,
185    H: HttpClient,
186{
187    /// Checks for a new version in the registry.
188    ///
189    /// In case of a non-zero [`interval()`](Self::interval), this will create or access a cache file.
190    ///
191    /// # Examples
192    ///
193    /// To check for a new version on Crates.io:
194    ///
195    /// ```rust
196    /// use update_informer::{registry, Check};
197    ///
198    /// let informer = update_informer::new(registry::Crates, "crate_name", "0.1.0");
199    /// let _ = informer.check_version();
200    /// ```
201    fn check_version(self) -> Result<Option<Version>> {
202        let pkg = Package::new(self.name.as_ref(), self.version.as_ref())?;
203        let client = http_client::new(self.http_client, self.timeout);
204
205        // If the interval is zero, don't use the cache file
206        let latest_version = if self.interval.is_zero() {
207            match R::get_latest_version(client, &pkg)? {
208                Some(v) => v,
209                None => return Ok(None),
210            }
211        } else {
212            let latest_version_file = VersionFile::new(R::NAME, &pkg, self.version.as_ref())?;
213            let last_modified = latest_version_file.last_modified()?;
214
215            if last_modified >= self.interval {
216                // This is needed to update mtime of the file
217                latest_version_file.recreate_file()?;
218
219                match R::get_latest_version(client, &pkg)? {
220                    Some(v) => {
221                        latest_version_file.write_version(&v)?;
222                        v
223                    }
224                    None => return Ok(None),
225                }
226            } else {
227                latest_version_file.get_version()?
228            }
229        };
230
231        let latest_version = Version::parse(latest_version)?;
232        if &latest_version > pkg.version() {
233            return Ok(Some(latest_version));
234        }
235
236        Ok(None)
237    }
238}
239
240/// Fake `UpdateInformer`. Used only for tests.
241pub struct FakeUpdateInformer<V: AsRef<str>> {
242    version: V,
243}
244
245/// Constructs a new `FakeUpdateInformer`.
246///
247/// # Arguments
248///
249/// * `registry` - A registry service such as Crates.io or GitHub (not used).
250/// * `name` - A project name (not used).
251/// * `version` - Current version of the project (not used).
252/// * `interval` - An interval how often to check for a new version (not used).
253/// * `new_version` - The desired version.
254///
255/// # Examples
256///
257/// ```rust
258/// use update_informer::{registry, Check};
259///
260/// let informer = update_informer::fake(registry::Crates, "repo", "0.1.0", "1.0.0");
261/// ```
262pub fn fake<R, N, V>(_registry: R, _name: N, _version: V, new_version: V) -> FakeUpdateInformer<V>
263where
264    R: Registry,
265    N: AsRef<str>,
266    V: AsRef<str>,
267{
268    FakeUpdateInformer {
269        version: new_version,
270    }
271}
272
273impl<V: AsRef<str>> FakeUpdateInformer<V> {
274    pub fn interval(self, _interval: Duration) -> Self {
275        self
276    }
277
278    pub fn timeout(self, _timeout: Duration) -> Self {
279        self
280    }
281
282    pub fn http_client<C: HttpClient>(self, _http_client: C) -> Self {
283        self
284    }
285}
286
287impl<V: AsRef<str>> Check for FakeUpdateInformer<V> {
288    /// Returns the desired version as a new version.
289    ///
290    /// # Examples
291    ///
292    /// ```rust
293    /// use update_informer::{registry, Check};
294    ///
295    /// let informer = update_informer::fake(registry::Crates, "crate_name", "0.1.0", "1.0.0");
296    /// let result = informer.check_version();
297    /// assert!(result.is_ok());
298    ///
299    /// let version = result.unwrap();
300    /// assert!(version.is_some());
301    /// assert_eq!(version.unwrap().to_string(), "v1.0.0");
302    /// ```
303    fn check_version(self) -> Result<Option<Version>> {
304        let version = Version::parse(self.version.as_ref())?;
305
306        Ok(Some(version))
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313    use crate::{registry::Crates, test_helper::within_test_dir};
314    use mockito::Mock;
315    use std::fs;
316
317    const PKG_NAME: &str = "repo";
318    const CURRENT_VERSION: &str = "3.1.0";
319    const LATEST_VERSION: &str = "3.1.1";
320
321    fn mock_crates(pkg: &str) -> Mock {
322        let pkg = Package::new(pkg, CURRENT_VERSION).unwrap();
323        let (mock, _) = crate::test_helper::mock_crates(
324            &pkg,
325            200,
326            "tests/fixtures/registry/crates/versions.json",
327        );
328
329        mock
330    }
331
332    #[test]
333    fn no_new_version_with_interval_test() {
334        within_test_dir(|_| {
335            let informer = new(Crates, PKG_NAME, CURRENT_VERSION);
336            let result = informer.check_version();
337
338            assert!(result.is_ok());
339            assert_eq!(result.unwrap(), None);
340        });
341    }
342
343    #[test]
344    fn no_new_version_on_registry_test() {
345        within_test_dir(|_| {
346            let _mock = mock_crates(PKG_NAME);
347            let informer = new(Crates, PKG_NAME, LATEST_VERSION).interval(Duration::ZERO);
348            let result = informer.check_version();
349
350            assert!(result.is_ok());
351            assert_eq!(result.unwrap(), None);
352        });
353    }
354
355    #[test]
356    fn check_version_on_crates_test() {
357        within_test_dir(|_| {
358            let _mock = mock_crates(PKG_NAME);
359            let informer = new(Crates, PKG_NAME, CURRENT_VERSION).interval(Duration::ZERO);
360            let result = informer.check_version();
361            let version = Version::parse(LATEST_VERSION).expect("parse version");
362
363            assert!(result.is_ok());
364            assert_eq!(result.unwrap(), Some(version));
365        });
366    }
367
368    #[test]
369    fn return_version_from_file_test() {
370        within_test_dir(|version_file| {
371            fs::write(version_file, "4.0.0").expect("create file");
372
373            let informer = new(Crates, PKG_NAME, CURRENT_VERSION);
374            let result = informer.check_version();
375            let version = Version::parse("4.0.0").expect("parse version");
376
377            assert!(result.is_ok());
378            assert_eq!(result.unwrap(), Some(version));
379        });
380    }
381
382    #[test]
383    fn create_version_file_test() {
384        within_test_dir(|version_file| {
385            assert!(!version_file.exists());
386
387            let informer = new(Crates, PKG_NAME, CURRENT_VERSION);
388            let result = informer.check_version();
389            assert!(result.is_ok());
390            assert!(version_file.exists());
391
392            let version = fs::read_to_string(version_file).expect("read file");
393            assert_eq!(version, CURRENT_VERSION);
394        });
395    }
396
397    #[test]
398    fn do_not_create_version_file_test() {
399        within_test_dir(|version_file| {
400            assert!(!version_file.exists());
401
402            let _mock = mock_crates(PKG_NAME);
403            let informer = new(Crates, PKG_NAME, CURRENT_VERSION).interval(Duration::ZERO);
404            let result = informer.check_version();
405
406            assert!(result.is_ok());
407            assert!(!version_file.exists());
408        });
409    }
410
411    #[test]
412    fn check_version_with_string_name_test() {
413        within_test_dir(|_| {
414            let pkg_name = format!("{}/{}", "owner", PKG_NAME);
415            let informer = new(Crates, pkg_name, CURRENT_VERSION);
416            let result = informer.check_version();
417
418            assert!(result.is_ok());
419        });
420    }
421
422    #[test]
423    fn check_version_with_string_version_test() {
424        within_test_dir(|_| {
425            let version = String::from(CURRENT_VERSION);
426            let informer = new(Crates, PKG_NAME, version);
427            let result = informer.check_version();
428
429            assert!(result.is_ok());
430        });
431    }
432
433    #[test]
434    fn check_version_with_amp_string_test() {
435        within_test_dir(|_| {
436            let pkg_name = format!("{}/{}", "owner", PKG_NAME);
437            let version = String::from(CURRENT_VERSION);
438            let informer = new(Crates, &pkg_name, &version);
439            let result = informer.check_version();
440
441            assert!(result.is_ok());
442        });
443    }
444
445    #[test]
446    fn fake_check_version_test() {
447        let version = "1.0.0";
448        let informer = fake(Crates, PKG_NAME, CURRENT_VERSION, version)
449            .interval(Duration::ZERO)
450            .timeout(Duration::ZERO);
451        let result = informer.check_version();
452        let version = Version::parse(version).expect("parse version");
453
454        assert!(result.is_ok());
455        assert_eq!(result.unwrap(), Some(version));
456    }
457}