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
20pub mod registry;
22
23pub 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 fn check_version(self) -> Result<Option<Version>>
32 where
33 Self: Sized,
34 {
35 Ok(None)
36 }
37}
38
39pub 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
56pub 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), 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 pub fn interval(self, interval: Duration) -> Self {
114 Self { interval, ..self }
115 }
116
117 pub fn timeout(self, timeout: Duration) -> Self {
135 Self { timeout, ..self }
136 }
137
138 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 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 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 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
240pub struct FakeUpdateInformer<V: AsRef<str>> {
242 version: V,
243}
244
245pub 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 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}