mirror of
https://github.com/MalteT/mensa.git
synced 2024-10-22 21:59:17 +02:00
Cleanup, add command tests
This commit is contained in:
parent
15e5133f8c
commit
d1b0dc36fe
80
Cargo.lock
generated
80
Cargo.lock
generated
|
@ -48,6 +48,20 @@ dependencies = [
|
|||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "assert_cmd"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e996dc7940838b7ef1096b882e29ec30a3149a3a443cdc8dba19ed382eca1fe2"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"doc-comment",
|
||||
"predicates",
|
||||
"predicates-core",
|
||||
"predicates-tree",
|
||||
"wait-timeout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "1.6.1"
|
||||
|
@ -311,6 +325,17 @@ dependencies = [
|
|||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.7.1"
|
||||
|
@ -535,6 +560,12 @@ version = "2.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
|
||||
|
||||
[[package]]
|
||||
name = "difflib"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.8.1"
|
||||
|
@ -584,6 +615,12 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "doc-comment"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.6.1"
|
||||
|
@ -1200,6 +1237,7 @@ dependencies = [
|
|||
name = "mensa"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"assert_cmd",
|
||||
"cacache",
|
||||
"chrono",
|
||||
"date_time_parser",
|
||||
|
@ -1534,6 +1572,33 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||
|
||||
[[package]]
|
||||
name = "predicates"
|
||||
version = "2.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c6ce811d0b2e103743eec01db1c50612221f173084ce2f7941053e94b6bb474"
|
||||
dependencies = [
|
||||
"difflib",
|
||||
"itertools",
|
||||
"predicates-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "predicates-core"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57e35a3326b75e49aa85f5dc6ec15b41108cf5aee58eabb1f274dd18b73c2451"
|
||||
|
||||
[[package]]
|
||||
name = "predicates-tree"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "338c7be2905b732ae3984a2f40032b5e94fd8f52505b186c7d4d68d193445df7"
|
||||
dependencies = [
|
||||
"predicates-core",
|
||||
"termtree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pretty_assertions"
|
||||
version = "1.0.0"
|
||||
|
@ -2155,6 +2220,12 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termtree"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78fbf2dd23e79c28ccfa2472d3e6b3b189866ffef1aeb91f17c2d968b6586378"
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.11.0"
|
||||
|
@ -2488,6 +2559,15 @@ version = "0.9.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
|
||||
|
||||
[[package]]
|
||||
name = "wait-timeout"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "waker-fn"
|
||||
version = "1.1.0"
|
||||
|
|
|
@ -40,3 +40,4 @@ itertools = "0.10"
|
|||
temp-dir = "0.1"
|
||||
httpmock = "0.6"
|
||||
pretty_assertions = "1.0"
|
||||
assert_cmd = "2.0"
|
||||
|
|
20
src/cache.rs
20
src/cache.rs
|
@ -32,10 +32,7 @@ use cacache::Metadata;
|
|||
use chrono::{Duration, TimeZone};
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use reqwest::{
|
||||
blocking::{Client, Response},
|
||||
IntoUrl, StatusCode,
|
||||
};
|
||||
use reqwest::{blocking::Response, IntoUrl, StatusCode};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use tracing::{info, warn};
|
||||
|
||||
|
@ -89,14 +86,14 @@ where
|
|||
U: IntoUrl,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
fetch(&CONF.client, url, local_ttl, |text, _| {
|
||||
fetch(url, local_ttl, |text, _| {
|
||||
// TODO: Check content header?
|
||||
serde_json::from_str(&text).map_err(|why| Error::Deserializing(why, "fetching json"))
|
||||
})
|
||||
}
|
||||
|
||||
/// Generic method for fetching remote url-based resources that may be cached.
|
||||
pub fn fetch<Map, U, T>(client: &Client, url: U, local_ttl: Duration, map: Map) -> Result<T>
|
||||
pub fn fetch<Map, U, T>(url: U, local_ttl: Duration, map: Map) -> Result<T>
|
||||
where
|
||||
U: IntoUrl,
|
||||
Map: FnOnce(String, Headers) -> Result<T>,
|
||||
|
@ -114,20 +111,20 @@ where
|
|||
}
|
||||
Ok(CacheResult::Miss) => {
|
||||
info!("Missed cache on {:?}", url);
|
||||
get_and_update_cache(client, url, None, None)?
|
||||
get_and_update_cache(url, None, None)?
|
||||
}
|
||||
Ok(CacheResult::Stale(old_headers, meta)) => {
|
||||
info!("Stale cache on {:?}", url);
|
||||
// The cache is stale but may still be valid
|
||||
// Request the resource with set IF_NONE_MATCH tag and update
|
||||
// the caches metadata or value
|
||||
match get_and_update_cache(client, url, old_headers.etag, Some(meta)) {
|
||||
match get_and_update_cache(url, old_headers.etag, Some(meta)) {
|
||||
Ok(tah) => tah,
|
||||
Err(why) => {
|
||||
warn!("{}", why);
|
||||
// Fetching and updating failed for some reason, retry
|
||||
// without the IF_NONE_MATCH tag and fail if unsuccessful
|
||||
get_and_update_cache(client, url, None, None)?
|
||||
get_and_update_cache(url, None, None)?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -135,7 +132,7 @@ where
|
|||
// Fetching from the cache failed for some reason, just
|
||||
// request the resource and update the cache
|
||||
warn!("{}", why);
|
||||
get_and_update_cache(client, url, None, None)?
|
||||
get_and_update_cache(url, None, None)?
|
||||
}
|
||||
};
|
||||
// Apply the map and return the result
|
||||
|
@ -174,13 +171,12 @@ fn try_load_cache(url: &str, local_ttl: Duration) -> Result<CacheResult<TextAndH
|
|||
/// If an optional `etag` is provided, add the If-None-Match header, and thus
|
||||
/// only get an update if the new ETAG differs from the given `etag`.
|
||||
fn get_and_update_cache(
|
||||
client: &Client,
|
||||
url: &str,
|
||||
etag: Option<String>,
|
||||
meta: Option<Metadata>,
|
||||
) -> Result<TextAndHeaders> {
|
||||
// Construct the request
|
||||
let mut builder = client.get(url);
|
||||
let mut builder = CONF.client.get(url);
|
||||
// Add If-None-Match header, if etag is present
|
||||
if let Some(etag) = etag {
|
||||
let etag_key = reqwest::header::IF_NONE_MATCH;
|
||||
|
|
4
src/cache/fetchable.rs
vendored
4
src/cache/fetchable.rs
vendored
|
@ -12,10 +12,6 @@ pub enum Fetchable<T> {
|
|||
}
|
||||
|
||||
impl<T> Fetchable<T> {
|
||||
pub fn is_fetched(&self) -> bool {
|
||||
matches!(self, Self::Fetched(_))
|
||||
}
|
||||
|
||||
pub fn fetch<F>(&mut self, f: F) -> Result<&T>
|
||||
where
|
||||
F: FnOnce() -> Result<T>,
|
||||
|
|
3
src/cache/tests.rs
vendored
3
src/cache/tests.rs
vendored
|
@ -6,7 +6,6 @@ use pretty_assertions::assert_eq;
|
|||
use super::*;
|
||||
|
||||
lazy_static! {
|
||||
static ref CLIENT: Client = Client::new();
|
||||
static ref TTL: Duration = Duration::minutes(1);
|
||||
}
|
||||
|
||||
|
@ -48,7 +47,7 @@ fn basic_caching() {
|
|||
print_cache_list("After first read");
|
||||
assert_eq!(val, CacheResult::Miss);
|
||||
// Populate the cache with the first request
|
||||
let val = fetch(&*CLIENT, server.url("/test"), *TTL, |txt, _| Ok(txt)).unwrap();
|
||||
let val = fetch(server.url("/test"), *TTL, |txt, _| Ok(txt)).unwrap();
|
||||
assert_eq!(val, "This page works!",);
|
||||
// The cache should now be hit
|
||||
let val = try_load_cache(&server.url("/test"), Duration::max_value()).unwrap();
|
||||
|
|
|
@ -56,11 +56,12 @@ pub struct Meta {
|
|||
#[serde(try_from = "de::DayDeserialized")]
|
||||
pub struct Day {
|
||||
date: NaiveDate,
|
||||
closed: bool,
|
||||
#[serde(rename = "closed")]
|
||||
_closed: bool,
|
||||
}
|
||||
|
||||
impl Meta {
|
||||
pub fn fetch(id: CanteenId) -> Result<Self> {
|
||||
pub fn fetch(_id: CanteenId) -> Result<Self> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
@ -108,10 +109,6 @@ impl Canteen {
|
|||
self.id
|
||||
}
|
||||
|
||||
pub fn name(&mut self) -> Result<&String> {
|
||||
Ok(&self.meta()?.address)
|
||||
}
|
||||
|
||||
pub fn address(&mut self) -> Result<&String> {
|
||||
Ok(&self.meta()?.address)
|
||||
}
|
||||
|
@ -174,7 +171,7 @@ impl Canteen {
|
|||
ENDPOINT, lat, long, geo.radius,
|
||||
)
|
||||
};
|
||||
PaginatedList::from(&CONF.client, url, *TTL_CANTEENS)?.try_flatten_and_collect()
|
||||
PaginatedList::new(url, *TTL_CANTEENS)?.consume()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ impl TryFrom<DayDeserialized> for super::Day {
|
|||
fn try_from(raw: DayDeserialized) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
date: NaiveDate::parse_from_str(&raw.date, "%Y-%m-%d").map_err(Error::InvalidDate)?,
|
||||
closed: raw.closed,
|
||||
_closed: raw.closed,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
10
src/error.rs
10
src/error.rs
|
@ -1,7 +1,5 @@
|
|||
use core::fmt;
|
||||
|
||||
use thiserror::Error;
|
||||
use tracing::{error, info, warn};
|
||||
use tracing::{error, warn};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
|
@ -80,9 +78,3 @@ where
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Debug print the given value using [`info`].
|
||||
pub fn pass_info<T: fmt::Debug>(t: T) -> T {
|
||||
info!("{:#?}", &t);
|
||||
t
|
||||
}
|
||||
|
|
18
src/geoip.rs
18
src/geoip.rs
|
@ -40,15 +40,15 @@ pub fn infer() -> Result<(f32, f32)> {
|
|||
},
|
||||
Command::Tags => (None, None),
|
||||
};
|
||||
let (lat, long) = if lat.is_none() || long.is_none() {
|
||||
let guessed = fetch_geoip()?;
|
||||
(
|
||||
lat.unwrap_or(guessed.latitude),
|
||||
long.unwrap_or(guessed.longitude),
|
||||
)
|
||||
} else {
|
||||
// Cannot panic, due to above if
|
||||
(lat.unwrap(), long.unwrap())
|
||||
let (lat, long) = match (lat, long) {
|
||||
(Some(lat), Some(long)) => (lat, long),
|
||||
(lat, long) => {
|
||||
let guessed = fetch_geoip()?;
|
||||
(
|
||||
lat.unwrap_or(guessed.latitude),
|
||||
long.unwrap_or(guessed.longitude),
|
||||
)
|
||||
}
|
||||
};
|
||||
Ok((lat, long))
|
||||
}
|
||||
|
|
|
@ -109,6 +109,8 @@ mod geoip;
|
|||
mod meal;
|
||||
mod pagination;
|
||||
mod tag;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use crate::{
|
||||
canteen::Canteen,
|
||||
|
|
|
@ -53,18 +53,12 @@ enum Note {
|
|||
}
|
||||
|
||||
impl Meta {
|
||||
fn fetch(id: MealId) -> Result<Meta> {
|
||||
fn fetch(_id: MealId) -> Result<Meta> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Meal {
|
||||
/// Print this [`Meal`] to the terminal.
|
||||
pub fn print(&mut self, highlight: bool) -> Result<()> {
|
||||
self.complete()?.print(highlight);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn meta(&mut self) -> Result<&Meta> {
|
||||
self.meta.fetch(|| Meta::fetch(self.id))
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
//! Wrapper around [`reqwest`] for multi-page requests.
|
||||
//!
|
||||
//! Only relevant/tested on the OpenMensa API.
|
||||
|
||||
use chrono::Duration;
|
||||
use reqwest::blocking::Client;
|
||||
use itertools::Itertools;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use std::marker::PhantomData;
|
||||
|
@ -9,23 +13,42 @@ use crate::{
|
|||
error::{Error, Result},
|
||||
};
|
||||
|
||||
pub struct PaginatedList<'client, T>
|
||||
/// An iterator over json pages containing lists.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ## Page 1
|
||||
///
|
||||
/// ```json
|
||||
/// [ { "id": 1 },
|
||||
/// { "id": 2 } ]
|
||||
/// ```
|
||||
///
|
||||
/// ## Page 2
|
||||
///
|
||||
/// ```json
|
||||
/// [ { "id": 3 },
|
||||
/// { "id": 4 } ]
|
||||
/// ```
|
||||
pub struct PaginatedList<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
client: &'client Client,
|
||||
next_page: Option<String>,
|
||||
ttl: Duration,
|
||||
__item: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<'client, T> PaginatedList<'client, T>
|
||||
impl<T> PaginatedList<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
pub fn from<S: AsRef<str>>(client: &'client Client, url: S, ttl: Duration) -> Result<Self> {
|
||||
/// Create a new page iterator
|
||||
///
|
||||
/// Takes the `url` for the first page and a
|
||||
/// `local_ttl` for the cached values.
|
||||
pub fn new<S: AsRef<str>>(url: S, ttl: Duration) -> Result<Self> {
|
||||
Ok(PaginatedList {
|
||||
client,
|
||||
ttl,
|
||||
next_page: Some(url.as_ref().into()),
|
||||
__item: PhantomData,
|
||||
|
@ -33,21 +56,17 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'client, T> PaginatedList<'client, T>
|
||||
impl<T> PaginatedList<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
pub fn try_flatten_and_collect(self) -> Result<Vec<T>> {
|
||||
let mut ret = vec![];
|
||||
for value in self {
|
||||
let value = value?;
|
||||
ret.extend(value);
|
||||
}
|
||||
Ok(ret)
|
||||
/// Consumes this iterator, flattening the collected pages.
|
||||
pub fn consume(self) -> Result<Vec<T>> {
|
||||
self.flatten_ok().try_collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'client, T> Iterator for PaginatedList<'client, T>
|
||||
impl<T> Iterator for PaginatedList<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
|
@ -56,7 +75,7 @@ where
|
|||
fn next(&mut self) -> Option<Self::Item> {
|
||||
// This will yield until no next_page is available
|
||||
let curr_page = self.next_page.take()?;
|
||||
let res = cache::fetch(self.client, &curr_page, self.ttl, |text, headers| {
|
||||
let res = cache::fetch(curr_page, self.ttl, |text, headers| {
|
||||
let val = serde_json::from_str::<Vec<_>>(&text)
|
||||
.map_err(|why| Error::Deserializing(why, "fetching json in pagination iterator"))?;
|
||||
Ok((val, headers.this_page, headers.next_page, headers.last_page))
|
||||
|
|
88
src/tests.rs
Normal file
88
src/tests.rs
Normal file
|
@ -0,0 +1,88 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use assert_cmd::Command;
|
||||
|
||||
#[test]
|
||||
pub fn cmd_mensa_meals() {
|
||||
Command::cargo_bin("mensa")
|
||||
.unwrap()
|
||||
// Prevent loading the config
|
||||
.args(&["--config", "/does/not/exist"])
|
||||
// Show meals
|
||||
.arg("meals")
|
||||
// Use canteen id 1
|
||||
.args(&["--id", "1"])
|
||||
.timeout(Duration::from_secs(10))
|
||||
.assert()
|
||||
.success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn cmd_mensa_meals_json() {
|
||||
Command::cargo_bin("mensa")
|
||||
.unwrap()
|
||||
// Prevent loading the config
|
||||
.args(&["--config", "/does/not/exist"])
|
||||
// Show meals
|
||||
.arg("meals")
|
||||
// Use canteen id 1
|
||||
.args(&["--id", "1"])
|
||||
.arg("--json")
|
||||
.timeout(Duration::from_secs(10))
|
||||
.assert()
|
||||
.success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn cmd_mensa_canteens() {
|
||||
Command::cargo_bin("mensa")
|
||||
.unwrap()
|
||||
// Prevent loading the config
|
||||
.args(&["--config", "/does/not/exist"])
|
||||
// Show meals
|
||||
.arg("canteens")
|
||||
.timeout(Duration::from_secs(10))
|
||||
.assert()
|
||||
.success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn cmd_mensa_canteens_json() {
|
||||
Command::cargo_bin("mensa")
|
||||
.unwrap()
|
||||
// Prevent loading the config
|
||||
.args(&["--config", "/does/not/exist"])
|
||||
// Show meals
|
||||
.arg("canteens")
|
||||
.arg("--json")
|
||||
.timeout(Duration::from_secs(10))
|
||||
.assert()
|
||||
.success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn cmd_mensa_tags() {
|
||||
Command::cargo_bin("mensa")
|
||||
.unwrap()
|
||||
// Prevent loading the config
|
||||
.args(&["--config", "/does/not/exist"])
|
||||
// Show tags
|
||||
.arg("tags")
|
||||
.timeout(Duration::from_secs(10))
|
||||
.assert()
|
||||
.success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn cmd_mensa_tags_json() {
|
||||
Command::cargo_bin("mensa")
|
||||
.unwrap()
|
||||
// Prevent loading the config
|
||||
.args(&["--config", "/does/not/exist"])
|
||||
// Show tags
|
||||
.arg("tags")
|
||||
.arg("--json")
|
||||
.timeout(Duration::from_secs(10))
|
||||
.assert()
|
||||
.success();
|
||||
}
|
Loading…
Reference in a new issue