#![doc = include_str!("../README.md")]
use crate::{
http_client::{DefaultHttpClient, HttpClient},
version_file::VersionFile,
};
use std::time::Duration;
pub use package::Package;
pub use registry::Registry;
pub use version::Version;
mod package;
mod version;
mod version_file;
#[cfg(test)]
mod test_helper;
pub mod registry;
pub mod http_client;
type Error = Box<dyn std::error::Error>;
pub type Result<T> = std::result::Result<T, Error>;
pub trait Check {
fn check_version(self) -> Result<Option<Version>>
where
Self: Sized,
{
Ok(None)
}
}
pub struct UpdateInformer<
R: Registry,
N: AsRef<str>,
V: AsRef<str>,
H: HttpClient = DefaultHttpClient,
> {
_registry: R,
name: N,
version: V,
http_client: H,
interval: Duration,
timeout: Duration,
}
pub fn new<R, N, V>(registry: R, name: N, version: V) -> UpdateInformer<R, N, V>
where
R: Registry,
N: AsRef<str>,
V: AsRef<str>,
{
UpdateInformer {
_registry: registry,
name,
version,
http_client: DefaultHttpClient {},
interval: Duration::from_secs(60 * 60 * 24), timeout: Duration::from_secs(5),
}
}
impl<R, N, V, H> UpdateInformer<R, N, V, H>
where
R: Registry,
N: AsRef<str>,
V: AsRef<str>,
H: HttpClient,
{
pub fn interval(self, interval: Duration) -> Self {
Self { interval, ..self }
}
pub fn timeout(self, timeout: Duration) -> Self {
Self { timeout, ..self }
}
pub fn http_client<C: HttpClient>(self, http_client: C) -> UpdateInformer<R, N, V, C> {
UpdateInformer {
_registry: self._registry,
name: self.name,
version: self.version,
interval: self.interval,
timeout: self.timeout,
http_client,
}
}
}
impl<R, N, V, H> Check for UpdateInformer<R, N, V, H>
where
R: Registry,
N: AsRef<str>,
V: AsRef<str>,
H: HttpClient,
{
fn check_version(self) -> Result<Option<Version>> {
let pkg = Package::new(self.name.as_ref(), self.version.as_ref())?;
let client = http_client::new(self.http_client, self.timeout);
let latest_version = if self.interval.is_zero() {
match R::get_latest_version(client, &pkg)? {
Some(v) => v,
None => return Ok(None),
}
} else {
let latest_version_file = VersionFile::new(R::NAME, &pkg, self.version.as_ref())?;
let last_modified = latest_version_file.last_modified()?;
if last_modified >= self.interval {
latest_version_file.recreate_file()?;
match R::get_latest_version(client, &pkg)? {
Some(v) => {
latest_version_file.write_version(&v)?;
v
}
None => return Ok(None),
}
} else {
latest_version_file.get_version()?
}
};
let latest_version = Version::parse(latest_version)?;
if &latest_version > pkg.version() {
return Ok(Some(latest_version));
}
Ok(None)
}
}
pub struct FakeUpdateInformer<V: AsRef<str>> {
version: V,
}
pub fn fake<R, N, V>(_registry: R, _name: N, _version: V, new_version: V) -> FakeUpdateInformer<V>
where
R: Registry,
N: AsRef<str>,
V: AsRef<str>,
{
FakeUpdateInformer {
version: new_version,
}
}
impl<V: AsRef<str>> FakeUpdateInformer<V> {
pub fn interval(self, _interval: Duration) -> Self {
self
}
pub fn timeout(self, _timeout: Duration) -> Self {
self
}
pub fn http_client<C: HttpClient>(self, _http_client: C) -> Self {
self
}
}
impl<V: AsRef<str>> Check for FakeUpdateInformer<V> {
fn check_version(self) -> Result<Option<Version>> {
let version = Version::parse(self.version.as_ref())?;
Ok(Some(version))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{registry::Crates, test_helper::within_test_dir};
use mockito::Mock;
use std::fs;
const PKG_NAME: &str = "repo";
const CURRENT_VERSION: &str = "3.1.0";
const LATEST_VERSION: &str = "3.1.1";
fn mock_crates(pkg: &str) -> Mock {
let pkg = Package::new(pkg, CURRENT_VERSION).unwrap();
let (mock, _) = crate::test_helper::mock_crates(
&pkg,
200,
"tests/fixtures/registry/crates/versions.json",
);
mock
}
#[test]
fn no_new_version_with_interval_test() {
within_test_dir(|_| {
let informer = new(Crates, PKG_NAME, CURRENT_VERSION);
let result = informer.check_version();
assert!(result.is_ok());
assert_eq!(result.unwrap(), None);
});
}
#[test]
fn no_new_version_on_registry_test() {
within_test_dir(|_| {
let _mock = mock_crates(PKG_NAME);
let informer = new(Crates, PKG_NAME, LATEST_VERSION).interval(Duration::ZERO);
let result = informer.check_version();
assert!(result.is_ok());
assert_eq!(result.unwrap(), None);
});
}
#[test]
fn check_version_on_crates_test() {
within_test_dir(|_| {
let _mock = mock_crates(PKG_NAME);
let informer = new(Crates, PKG_NAME, CURRENT_VERSION).interval(Duration::ZERO);
let result = informer.check_version();
let version = Version::parse(LATEST_VERSION).expect("parse version");
assert!(result.is_ok());
assert_eq!(result.unwrap(), Some(version));
});
}
#[test]
fn return_version_from_file_test() {
within_test_dir(|version_file| {
fs::write(version_file, "4.0.0").expect("create file");
let informer = new(Crates, PKG_NAME, CURRENT_VERSION);
let result = informer.check_version();
let version = Version::parse("4.0.0").expect("parse version");
assert!(result.is_ok());
assert_eq!(result.unwrap(), Some(version));
});
}
#[test]
fn create_version_file_test() {
within_test_dir(|version_file| {
assert!(!version_file.exists());
let informer = new(Crates, PKG_NAME, CURRENT_VERSION);
let result = informer.check_version();
assert!(result.is_ok());
assert!(version_file.exists());
let version = fs::read_to_string(version_file).expect("read file");
assert_eq!(version, CURRENT_VERSION);
});
}
#[test]
fn do_not_create_version_file_test() {
within_test_dir(|version_file| {
assert!(!version_file.exists());
let _mock = mock_crates(PKG_NAME);
let informer = new(Crates, PKG_NAME, CURRENT_VERSION).interval(Duration::ZERO);
let result = informer.check_version();
assert!(result.is_ok());
assert!(!version_file.exists());
});
}
#[test]
fn check_version_with_string_name_test() {
within_test_dir(|_| {
let pkg_name = format!("{}/{}", "owner", PKG_NAME);
let informer = new(Crates, pkg_name, CURRENT_VERSION);
let result = informer.check_version();
assert!(result.is_ok());
});
}
#[test]
fn check_version_with_string_version_test() {
within_test_dir(|_| {
let version = String::from(CURRENT_VERSION);
let informer = new(Crates, PKG_NAME, version);
let result = informer.check_version();
assert!(result.is_ok());
});
}
#[test]
fn check_version_with_amp_string_test() {
within_test_dir(|_| {
let pkg_name = format!("{}/{}", "owner", PKG_NAME);
let version = String::from(CURRENT_VERSION);
let informer = new(Crates, &pkg_name, &version);
let result = informer.check_version();
assert!(result.is_ok());
});
}
#[test]
fn fake_check_version_test() {
let version = "1.0.0";
let informer = fake(Crates, PKG_NAME, CURRENT_VERSION, version)
.interval(Duration::ZERO)
.timeout(Duration::ZERO);
let result = informer.check_version();
let version = Version::parse(version).expect("parse version");
assert!(result.is_ok());
assert_eq!(result.unwrap(), Some(version));
}
}