Add Api trait

This should abstract away from the actual implementation and allow
for reproducible, local tests with a DummyApi.
Tests have been rewritten to use the DummyApi.
Prep for #11.
This commit is contained in:
Malte Tammena 2021-10-25 08:44:23 +02:00
parent ca84c76172
commit b21f921b03
10 changed files with 220 additions and 573 deletions

452
Cargo.lock generated
View file

@ -29,25 +29,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "ascii-canvas"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6"
dependencies = [
"term",
]
[[package]]
name = "assert-json-diff"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f1c3703dd33532d7f0ca049168930e9099ecac238e23cf932f3a69c42f06da"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "assert_cmd"
version = "2.0.2"
@ -140,15 +121,6 @@ dependencies = [
"event-listener",
]
[[package]]
name = "async-object-pool"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aeb901c30ebc2fc4ab46395bbfbdba9542c16559d853645d75190c3056caf3bc"
dependencies = [
"async-std",
]
[[package]]
name = "async-process"
version = "1.2.0"
@ -200,17 +172,6 @@ version = "4.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91831deabf0d6d7ec49552e489aed63b7456a7a3c46cff62adad428110b0af0"
[[package]]
name = "async-trait"
version = "0.1.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "atomic-waker"
version = "1.0.0"
@ -249,32 +210,6 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "basic-cookies"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb53b6b315f924c7f113b162e53b3901c05fc9966baf84d201dfcc7432a4bb38"
dependencies = [
"lalrpop",
"lalrpop-util",
"regex",
]
[[package]]
name = "bit-set"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bitflags"
version = "1.3.2"
@ -389,12 +324,6 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba"
[[package]]
name = "castaway"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed247d1586918e46f2bbe0f13b06498db8dab5a8c1093f156652e9f2e0a73fc3"
[[package]]
name = "cc"
version = "1.0.71"
@ -480,12 +409,6 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "crunchy"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "ctor"
version = "0.1.21"
@ -496,37 +419,6 @@ dependencies = [
"syn",
]
[[package]]
name = "curl"
version = "0.4.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aaa3b8db7f3341ddef15786d250106334d4a6c4b0ae4a46cd77082777d9849b9"
dependencies = [
"curl-sys",
"libc",
"openssl-probe",
"openssl-sys",
"schannel",
"socket2",
"winapi",
]
[[package]]
name = "curl-sys"
version = "0.4.49+curl-7.79.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0f44960aea24a786a46907b8824ebc0e66ca06bf4e4978408c7499620343483"
dependencies = [
"cc",
"libc",
"libnghttp2-sys",
"libz-sys",
"openssl-sys",
"pkg-config",
"vcpkg",
"winapi",
]
[[package]]
name = "date_time_parser"
version = "0.1.1"
@ -554,12 +446,6 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
[[package]]
name = "difference"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
[[package]]
name = "difflib"
version = "0.4.0"
@ -594,16 +480,6 @@ dependencies = [
"dirs-sys-next",
]
[[package]]
name = "dirs-next"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if",
"dirs-sys-next",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
@ -627,15 +503,6 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "ena"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7402b94a93c24e742487327a7cd839dc9d36fec9de9fb25b09f2dae459f36c3"
dependencies = [
"log",
]
[[package]]
name = "encoding_rs"
version = "0.8.29"
@ -666,12 +533,6 @@ dependencies = [
"instant",
]
[[package]]
name = "fixedbitset"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d"
[[package]]
name = "fnv"
version = "1.0.7"
@ -944,34 +805,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440"
[[package]]
name = "httpmock"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "218c2c3dabf4497c87d042b43984a5fd03bcd8a6adb0056ade7df20f88df225a"
dependencies = [
"assert-json-diff",
"async-object-pool",
"async-trait",
"base64 0.13.0",
"basic-cookies",
"crossbeam-utils",
"difference",
"form_urlencoded",
"futures-util",
"hyper",
"isahc",
"lazy_static",
"levenshtein",
"log",
"qstring",
"regex",
"serde",
"serde_json",
"serde_regex",
"tokio",
]
[[package]]
name = "hyper"
version = "0.14.14"
@ -1051,33 +884,6 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb"
[[package]]
name = "isahc"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ef5402b1791c9fc479ef9871601a2f10e4cc0f14414a5c9c6e043fb51e5a56"
dependencies = [
"async-channel",
"castaway",
"crossbeam-utils",
"curl",
"curl-sys",
"encoding_rs",
"event-listener",
"futures-lite",
"http",
"log",
"mime",
"once_cell",
"polling",
"slab",
"sluice",
"tracing",
"tracing-futures",
"url",
"waker-fn",
]
[[package]]
name = "itertools"
version = "0.10.1"
@ -1111,87 +917,18 @@ dependencies = [
"log",
]
[[package]]
name = "lalrpop"
version = "0.19.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15174f1c529af5bf1283c3bc0058266b483a67156f79589fab2a25e23cf8988"
dependencies = [
"ascii-canvas",
"atty",
"bit-set",
"diff",
"ena",
"itertools",
"lalrpop-util",
"petgraph",
"pico-args",
"regex",
"regex-syntax",
"string_cache",
"term",
"tiny-keccak",
"unicode-xid",
]
[[package]]
name = "lalrpop-util"
version = "0.19.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3e58cce361efcc90ba8a0a5f982c741ff86b603495bb15a998412e957dcd278"
dependencies = [
"regex",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "levenshtein"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760"
[[package]]
name = "libc"
version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "869d572136620d55835903746bcb5cdc54cb2851fd0aeec53220b4bb65ef3013"
[[package]]
name = "libnghttp2-sys"
version = "0.1.7+1.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57ed28aba195b38d5ff02b9170cbff627e336a20925e43b4945390401c5dc93f"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "libz-sys"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "lock_api"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.14"
@ -1242,7 +979,6 @@ dependencies = [
"chrono",
"date_time_parser",
"directories-next",
"httpmock",
"itertools",
"lazy_static",
"num_enum",
@ -1312,12 +1048,6 @@ dependencies = [
"tempfile",
]
[[package]]
name = "new_debug_unreachable"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
[[package]]
name = "ntapi"
version = "0.3.6"
@ -1453,82 +1183,12 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72"
[[package]]
name = "parking_lot"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [
"instant",
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
dependencies = [
"cfg-if",
"instant",
"libc",
"redox_syscall",
"smallvec",
"winapi",
]
[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "petgraph"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7"
dependencies = [
"fixedbitset",
"indexmap",
]
[[package]]
name = "phf_shared"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
dependencies = [
"siphasher",
]
[[package]]
name = "pico-args"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468"
[[package]]
name = "pin-project"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "576bc800220cc65dac09e99e97b08b358cfab6e17078de8dc5fee223bd2d0c08"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e8fe8163d14ce7f0cdac2e040116f22eac817edabff0be91e8aff7e9accf389"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pin-project-lite"
version = "0.2.7"
@ -1566,12 +1226,6 @@ version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3ca011bd0129ff4ae15cd04c4eef202cadf6c51c21e47aba319b4e0501db741"
[[package]]
name = "precomputed-hash"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "predicates"
version = "2.0.3"
@ -1666,15 +1320,6 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "qstring"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e"
dependencies = [
"percent-encoding",
]
[[package]]
name = "quote"
version = "1.0.10"
@ -1824,12 +1469,6 @@ dependencies = [
"winreg",
]
[[package]]
name = "rustversion"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088"
[[package]]
name = "ryu"
version = "1.0.5"
@ -1855,12 +1494,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "security-framework"
version = "2.4.2"
@ -1924,16 +1557,6 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_regex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf"
dependencies = [
"regex",
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.0"
@ -2024,29 +1647,12 @@ dependencies = [
"libc",
]
[[package]]
name = "siphasher"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b"
[[package]]
name = "slab"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
[[package]]
name = "sluice"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5"
dependencies = [
"async-channel",
"futures-core",
"futures-io",
]
[[package]]
name = "smallvec"
version = "1.7.0"
@ -2084,19 +1690,6 @@ dependencies = [
"thiserror",
]
[[package]]
name = "string_cache"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "923f0f39b6267d37d23ce71ae7235602134b250ace715dd2c90421998ddac0c6"
dependencies = [
"lazy_static",
"new_debug_unreachable",
"parking_lot",
"phf_shared",
"precomputed-hash",
]
[[package]]
name = "strsim"
version = "0.8.0"
@ -2189,17 +1782,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "term"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f"
dependencies = [
"dirs-next",
"rustversion",
"winapi",
]
[[package]]
name = "term_size"
version = "0.3.2"
@ -2286,15 +1868,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
"crunchy",
]
[[package]]
name = "tinyvec"
version = "1.5.0"
@ -2322,24 +1895,10 @@ dependencies = [
"memchr",
"mio",
"num_cpus",
"once_cell",
"pin-project-lite",
"signal-hook-registry",
"tokio-macros",
"winapi",
]
[[package]]
name = "tokio-macros"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2dd85aeaba7b68df939bd357c6afb36c87951be9e80bf9c859f2fc3e9fca0fd"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.0"
@ -2386,7 +1945,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105"
dependencies = [
"cfg-if",
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
@ -2412,16 +1970,6 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "tracing-futures"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
dependencies = [
"pin-project",
"tracing",
]
[[package]]
name = "tracing-log"
version = "0.1.2"

View file

@ -38,6 +38,5 @@ itertools = "0.10"
[dev-dependencies]
temp-dir = "0.1"
httpmock = "0.6"
pretty_assertions = "1.0"
assert_cmd = "2.0"

97
src/cache/mod.rs vendored
View file

@ -31,9 +31,8 @@
use cacache::Metadata;
use chrono::{Duration, TimeZone};
use lazy_static::lazy_static;
use regex::Regex;
use reqwest::{blocking::Response, StatusCode, Url};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use reqwest::{StatusCode, Url};
use serde::de::DeserializeOwned;
use tracing::{info, warn};
mod fetchable;
@ -45,30 +44,13 @@ pub use fetchable::Fetchable;
pub use wrapper::clear_cache as clear;
use crate::{
config::CONF,
error::{Error, Result, ResultExt},
request::{Api, DefaultApi, Headers, Response},
};
/// Returned by most functions in this module.
type TextAndHeaders = (String, Headers);
lazy_static! {
/// Regex to find the next page in a link header
/// Probably only applicable to the current version of the openmensa API.
// TODO: Improve this. How do these LINK headers look in general?
static ref LINK_NEXT_PAGE_RE: Regex = Regex::new(r#"<([^>]*)>; rel="next""#).unwrap();
}
/// Assortment of headers relevant to the program.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Headers {
pub etag: Option<String>,
pub this_page: Option<usize>,
pub next_page: Option<String>,
pub last_page: Option<usize>,
}
/// Possible results from a cache load.
#[derive(Debug, PartialEq)]
enum CacheResult<T> {
@ -175,32 +157,26 @@ fn get_and_update_cache(
etag: Option<String>,
meta: Option<Metadata>,
) -> Result<TextAndHeaders> {
// Construct the request
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;
builder = builder.header(etag_key, etag);
lazy_static! {
static ref API: DefaultApi = DefaultApi::create().expect("Failed to create API");
}
let resp = wrapper::send_request(builder)?;
let status = resp.status();
info!("Request to {:?} returned {}", url, status);
// Send request with optional ETag header
let resp = API.get(url, etag)?;
info!("Request to {:?} returned {}", url, resp.status);
match meta {
Some(meta) if status == StatusCode::NOT_MODIFIED => {
Some(meta) if resp.status == StatusCode::NOT_MODIFIED => {
// If we received code 304 NOT MODIFIED (after adding the If-None-Match)
// our cache is actually fresh and it's timestamp should be updated
let headers = resp.headers().clone().into();
// Just verified, that meta can be unwrapped!
touch_and_load_cache(url, &meta, headers)
touch_and_load_cache(url, &meta, resp.headers)
}
_ if status.is_success() => {
_ if resp.status.is_success() => {
// Request returned successfully, now update the cache with that
update_cache_from_response(resp)
}
_ => {
// Some error occured, just error out
// TODO: Retrying would be an option
Err(Error::NonSuccessStatusCode(url.to_string(), resp.status()))
Err(Error::NonSuccessStatusCode(url.to_string(), resp.status))
}
}
}
@ -209,11 +185,9 @@ fn get_and_update_cache(
///
/// Only relevant headers will be kept.
fn update_cache_from_response(resp: Response) -> Result<TextAndHeaders> {
let headers: Headers = resp.headers().clone().into();
let url = resp.url().as_str().to_owned();
let text = resp.text().map_err(Error::Reqwest)?;
wrapper::write_cache(&headers, &url, &text)?;
Ok((text, headers))
let url = resp.url.to_owned();
wrapper::write_cache(&resp.headers, &url, &resp.body)?;
Ok((resp.body, resp.headers))
}
/// Reset the cache's TTL, load and return it.
@ -248,44 +222,3 @@ fn to_text_and_headers(raw: Vec<u8>, meta: &serde_json::Value) -> Result<TextAnd
})?;
Ok((utf8, headers))
}
impl From<reqwest::header::HeaderMap> for Headers {
fn from(map: reqwest::header::HeaderMap) -> Self {
use reqwest::header::*;
let etag = map
.get(ETAG)
.map(|raw| {
let utf8 = raw.to_str().ok()?;
Some(utf8.to_string())
})
.flatten();
let this_page = map
.get("x-current-page")
.map(|raw| {
let utf8 = raw.to_str().ok()?;
utf8.parse().ok()
})
.flatten();
let next_page = map
.get(LINK)
.map(|raw| {
let utf8 = raw.to_str().ok()?;
let captures = LINK_NEXT_PAGE_RE.captures(utf8)?;
Some(captures[1].to_owned())
})
.flatten();
let last_page = map
.get("x-total-pages")
.map(|raw| {
let utf8 = raw.to_str().ok()?;
utf8.parse().ok()
})
.flatten();
Self {
etag,
this_page,
last_page,
next_page,
}
}
}

28
src/cache/tests.rs vendored
View file

@ -1,6 +1,6 @@
use std::thread;
use httpmock::{Method::GET, MockServer};
use lazy_static::lazy_static;
use pretty_assertions::assert_eq;
use super::*;
@ -33,39 +33,31 @@ fn test_cache_is_empty() {
#[test]
fn basic_caching() {
// === Setup ===
let server = MockServer::start();
server.mock(|when, then| {
when.method(GET).path("/test");
then.status(200)
.header("ETag", "static")
.body("This page works!");
});
let url = "http://invalid.local/test";
// Cache is empty
let val = try_load_cache(&server.url("/test"), Duration::max_value()).unwrap();
let val = try_load_cache(url, Duration::max_value()).unwrap();
print_cache_list("After first read");
assert_eq!(val, CacheResult::Miss);
// Populate the cache with the first request
let val = fetch(server.url("/test"), *TTL, |txt, _| Ok(txt)).unwrap();
assert_eq!(val, "This page works!",);
let val = fetch(url, *TTL, |txt, _| Ok(txt)).unwrap();
assert_eq!(val, "It works",);
// The cache should now be hit
let val = try_load_cache(&server.url("/test"), Duration::max_value()).unwrap();
let val = try_load_cache(url, Duration::max_value()).unwrap();
print_cache_list("After second read");
assert_eq!(
val,
CacheResult::Hit((
"This page works!".into(),
"It works".into(),
Headers {
etag: Some("static".into()),
this_page: None,
this_page: Some(1),
next_page: None,
last_page: None,
last_page: Some(1),
}
))
);
// Let's fake a stale entry
thread::sleep(std::time::Duration::from_secs(1));
let val = try_load_cache(&server.url("/test"), Duration::zero()).unwrap();
let val = try_load_cache(url, Duration::zero()).unwrap();
assert!(matches!(val, CacheResult::Stale(_, _)));
}

View file

@ -3,7 +3,6 @@
//! To make testing easier.
use cacache::Metadata;
use lazy_static::lazy_static;
use reqwest::blocking::{RequestBuilder, Response};
use tracing::info;
use std::{io::Write, path::Path};
@ -42,10 +41,6 @@ pub fn clear_cache() -> Result<()> {
cacache::clear_sync(cache()).map_err(|why| Error::Cache(why, "clearing"))
}
pub fn send_request(builder: RequestBuilder) -> Result<Response> {
builder.send().map_err(Error::Reqwest)
}
#[cfg(test)]
pub fn list_cache() -> impl Iterator<Item = cacache::Result<Metadata>> {
cacache::list_sync(cache())

View file

@ -1,10 +1,9 @@
use chrono::NaiveDate;
use lazy_static::lazy_static;
use reqwest::blocking::Client;
use serde::Deserialize;
use structopt::{clap::arg_enum, StructOpt};
use std::{collections::HashSet, fs, path::Path, time::Duration as StdDuration};
use std::{collections::HashSet, fs, path::Path};
use crate::{
canteen::CanteenId,
@ -22,32 +21,22 @@ pub mod args;
pub mod rule;
lazy_static! {
pub static ref CONF: Config = Config::assemble().unwrap();
static ref REQUEST_TIMEOUT: StdDuration = StdDuration::from_secs(10);
pub static ref CONF: Config = Config::assemble();
}
#[derive(Debug)]
pub struct Config {
pub config: Option<ConfigFile>,
pub client: Client,
pub args: Args,
}
impl Config {
fn assemble() -> Result<Self> {
fn assemble() -> Self {
let args = Args::from_args();
let default_config_path = || DIR.config_dir().join("config.toml");
let path = args.config.clone().unwrap_or_else(default_config_path);
let config = ConfigFile::load_or_log(path);
let client = Client::builder()
.timeout(*REQUEST_TIMEOUT)
.build()
.map_err(Error::Reqwest)?;
Ok(Config {
config,
client,
args,
})
Config { config, args }
}
/// Easy reference to the Command

View file

@ -133,6 +133,7 @@ mod error;
mod geoip;
mod meal;
mod pagination;
mod request;
mod tag;
// #[cfg(test)]
// mod tests;

47
src/request/dummy.rs Normal file
View file

@ -0,0 +1,47 @@
//! This contains the [`DummyApi`] used for testing purposes.
use reqwest::StatusCode;
use crate::error::Result;
use super::{Api, Headers, Response};
/// A dummy API, serving local, deterministic Responses
#[derive(Debug)]
pub struct DummyApi;
impl Api for DummyApi {
fn create() -> Result<Self> {
Ok(DummyApi)
}
fn get<'url, S>(&self, url: &'url str, etag: Option<S>) -> Result<Response<'url>>
where
S: AsRef<str>,
{
if url == "http://invalid.local/test" {
get_test_page(etag)
} else {
panic!("BUG: Invalid url in dummy api: {:?}", url)
}
}
}
/// GET http://invalid.local/test
fn get_test_page<S: AsRef<str>>(etag: Option<S>) -> Result<Response<'static>> {
let etag = etag.map(|etag| etag.as_ref().to_owned());
Ok(Response {
url: "http://invalid.local/test",
status: if etag == Some("static".into()) {
StatusCode::NOT_MODIFIED
} else {
StatusCode::OK
},
headers: Headers {
etag: Some("static".into()),
this_page: Some(1),
next_page: None,
last_page: Some(1),
},
body: "It works".to_owned(),
})
}

51
src/request/mod.rs Normal file
View file

@ -0,0 +1,51 @@
use ::reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use crate::error::Result;
#[cfg(not(test))]
mod reqwest;
#[cfg(not(test))]
pub use self::reqwest::ReqwestApi as DefaultApi;
#[cfg(test)]
mod dummy;
#[cfg(test)]
pub use self::dummy::DummyApi as DefaultApi;
/// Assortment of headers relevant to the program.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Headers {
pub etag: Option<String>,
pub this_page: Option<usize>,
pub next_page: Option<String>,
pub last_page: Option<usize>,
}
/// A subset of a Response, derived from [`reqwest::Response`].
pub struct Response<'url> {
pub url: &'url str,
pub status: StatusCode,
pub headers: Headers,
pub body: String,
}
/// Generalized API endpoint.
///
/// This abstracts away from the real thing to allow for deterministic local
/// tests with a DummyApi.
pub trait Api
where
Self: Sized,
{
/// Create the Api.
fn create() -> Result<Self>;
/// Send a get request.
///
/// Optionally attach an `If-None-Match` header, if `etag` is `Some`.
fn get<'url, S>(&self, url: &'url str, etag: Option<S>) -> Result<Response<'url>>
where
S: AsRef<str>;
}

92
src/request/reqwest.rs Normal file
View file

@ -0,0 +1,92 @@
use lazy_static::lazy_static;
use regex::Regex;
use reqwest::blocking::Client;
use std::time::Duration as StdDuration;
use crate::error::{Error, Result};
use super::{Api, Headers, Response};
lazy_static! {
/// Regex to find the next page in a link header
/// Probably only applicable to the current version of the openmensa API.
// TODO: Improve this. How do these LINK headers look in general?
static ref LINK_NEXT_PAGE_RE: Regex = Regex::new(r#"<([^>]*)>; rel="next""#).unwrap();
static ref REQUEST_TIMEOUT: StdDuration = StdDuration::from_secs(10);
}
/// Real api accessing the inter-webs.
#[derive(Debug)]
pub struct ReqwestApi {
client: Client,
}
impl Api for ReqwestApi {
fn create() -> Result<Self> {
let client = Client::builder()
.timeout(*REQUEST_TIMEOUT)
.build()
.map_err(Error::Reqwest)?;
Ok(ReqwestApi { client })
}
fn get<'url, S>(&self, url: &'url str, etag: Option<S>) -> Result<super::Response<'url>>
where
S: AsRef<str>,
{
let mut builder = self.client.get(url);
if let Some(etag) = etag {
let etag_key = reqwest::header::IF_NONE_MATCH;
builder = builder.header(etag_key, etag.as_ref());
}
let resp = builder.send().map_err(Error::Reqwest)?;
Ok(Response {
url,
status: resp.status(),
headers: resp.headers().clone().into(),
body: resp.text().map_err(Error::Reqwest)?,
})
}
}
impl From<reqwest::header::HeaderMap> for Headers {
fn from(map: reqwest::header::HeaderMap) -> Self {
use reqwest::header::*;
let etag = map
.get(ETAG)
.map(|raw| {
let utf8 = raw.to_str().ok()?;
Some(utf8.to_string())
})
.flatten();
let this_page = map
.get("x-current-page")
.map(|raw| {
let utf8 = raw.to_str().ok()?;
utf8.parse().ok()
})
.flatten();
let next_page = map
.get(LINK)
.map(|raw| {
let utf8 = raw.to_str().ok()?;
let captures = LINK_NEXT_PAGE_RE.captures(utf8)?;
Some(captures[1].to_owned())
})
.flatten();
let last_page = map
.get("x-total-pages")
.map(|raw| {
let utf8 = raw.to_str().ok()?;
utf8.parse().ok()
})
.flatten();
Self {
etag,
this_page,
last_page,
next_page,
}
}
}