mirror of
https://github.com/MalteT/mensa.git
synced 2024-10-22 21:59:17 +02:00
Rework fetachable resources
This commit is contained in:
parent
9158875169
commit
6da8a95a3b
|
@ -39,10 +39,12 @@ use reqwest::{
|
|||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use tracing::{info, warn};
|
||||
|
||||
mod fetchable;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod wrapper;
|
||||
|
||||
pub use fetchable::Fetchable;
|
||||
pub use wrapper::clear_cache as clear;
|
||||
|
||||
use crate::error::{Error, Result, ResultExt};
|
||||
|
|
47
src/cache/fetchable.rs
vendored
Normal file
47
src/cache/fetchable.rs
vendored
Normal file
|
@ -0,0 +1,47 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::Result;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(from = "T", untagged)]
|
||||
pub enum Fetchable<T> {
|
||||
/// The value does not exist, but can be fetched.
|
||||
None,
|
||||
/// The value has been fetched.
|
||||
Fetched(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>,
|
||||
{
|
||||
match self {
|
||||
Self::Fetched(ref value) => Ok(value),
|
||||
Self::None => {
|
||||
let value = f()?;
|
||||
*self = Self::Fetched(value);
|
||||
// This is safe, since we've just fetched successfully
|
||||
Ok(self.unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Panics if the resource doesn't exist
|
||||
fn unwrap(&self) -> &T {
|
||||
match self {
|
||||
Self::Fetched(value) => value,
|
||||
Self::None => panic!("Called .unwrap() on a Fetchable that is not fetched!"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Fetchable<T> {
|
||||
fn from(value: T) -> Self {
|
||||
Fetchable::Fetched(value)
|
||||
}
|
||||
}
|
154
src/canteen.rs
154
src/canteen.rs
|
@ -1,33 +1,49 @@
|
|||
use chrono::Duration;
|
||||
use reqwest::blocking::Client;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::info;
|
||||
|
||||
use std::marker::PhantomData;
|
||||
mod de;
|
||||
mod ser;
|
||||
|
||||
use crate::{
|
||||
cache,
|
||||
config::CanteensState,
|
||||
error::{Error, Result},
|
||||
geoip, get_sane_terminal_dimensions, print_json, ENDPOINT, TTL_CANTEENS,
|
||||
cache::Fetchable, config::CanteensState, error::Result, geoip, get_sane_terminal_dimensions,
|
||||
meal::Meal, pagination::PaginatedList, print_json, ENDPOINT, TTL_CANTEENS,
|
||||
};
|
||||
|
||||
use self::ser::CanteenCompleteWithoutMeals;
|
||||
|
||||
pub type CanteenId = usize;
|
||||
|
||||
const ADRESS_INDENT: &str = " ";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(from = "de::CanteenDeserialized")]
|
||||
pub struct Canteen {
|
||||
id: usize,
|
||||
id: CanteenId,
|
||||
#[serde(flatten)]
|
||||
meta: Fetchable<Meta>,
|
||||
meals: Fetchable<Vec<Meal>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Meta {
|
||||
name: String,
|
||||
city: String,
|
||||
address: String,
|
||||
coordinates: [f32; 2],
|
||||
coordinates: Option<[f32; 2]>,
|
||||
}
|
||||
|
||||
impl Meta {
|
||||
pub fn fetch(id: CanteenId) -> Result<Self> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Canteen {
|
||||
pub fn print(&self, state: &CanteensState) {
|
||||
pub fn print(&mut self, state: &CanteensState) -> Result<()> {
|
||||
let (width, _) = get_sane_terminal_dimensions();
|
||||
let address = textwrap::fill(
|
||||
&self.address,
|
||||
self.address()?,
|
||||
textwrap::Options::new(width)
|
||||
.initial_indent(ADRESS_INDENT)
|
||||
.subsequent_indent(ADRESS_INDENT),
|
||||
|
@ -35,9 +51,29 @@ impl Canteen {
|
|||
println!(
|
||||
"{} {}\n{}",
|
||||
color!(state: format!("{:>4}", self.id); bold, bright_yellow),
|
||||
color!(state: self.name; bold),
|
||||
color!(state: self.meta()?.name; bold),
|
||||
color!(state: address; bright_black),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn id(&self) -> CanteenId {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn name(&mut self) -> Result<&String> {
|
||||
Ok(&self.meta()?.address)
|
||||
}
|
||||
|
||||
pub fn address(&mut self) -> Result<&String> {
|
||||
Ok(&self.meta()?.address)
|
||||
}
|
||||
|
||||
pub fn complete_without_meals(&mut self) -> Result<CanteenCompleteWithoutMeals<'_>> {
|
||||
Ok(CanteenCompleteWithoutMeals {
|
||||
id: self.id,
|
||||
meta: self.meta()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn fetch(state: &CanteensState) -> Result<Vec<Self>> {
|
||||
|
@ -48,99 +84,37 @@ impl Canteen {
|
|||
let (lat, long) = geoip::fetch(state)?;
|
||||
info!(
|
||||
"Fetching canteens for lat: {}, long: {} with radius: {}",
|
||||
lat, long, state.cmd.radius
|
||||
lat, long, state.cmd.geo.radius
|
||||
);
|
||||
format!(
|
||||
"{}/canteens?near[lat]={}&near[lng]={}&near[dist]={}",
|
||||
ENDPOINT, lat, long, state.cmd.radius,
|
||||
ENDPOINT, lat, long, state.cmd.geo.radius,
|
||||
)
|
||||
};
|
||||
PaginatedList::from(&state.client, url, *TTL_CANTEENS)?.try_flatten_and_collect()
|
||||
}
|
||||
|
||||
pub fn print_all(state: &CanteensState, canteens: &[Self]) -> Result<()> {
|
||||
pub fn print_all(state: &CanteensState, canteens: &mut [Self]) -> Result<()> {
|
||||
if state.args.json {
|
||||
print_json(&canteens)
|
||||
Self::print_all_json(canteens)
|
||||
} else {
|
||||
for canteen in canteens {
|
||||
println!();
|
||||
canteen.print(state);
|
||||
canteen.print(state)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PaginatedList<'client, T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
client: &'client Client,
|
||||
next_page: Option<String>,
|
||||
ttl: Duration,
|
||||
__item: PhantomData<T>,
|
||||
}
|
||||
fn print_all_json(canteens: &mut [Self]) -> Result<()> {
|
||||
let serializable: Vec<_> = canteens
|
||||
.iter_mut()
|
||||
.map(|c| c.complete_without_meals())
|
||||
.try_collect()?;
|
||||
print_json(&serializable)
|
||||
}
|
||||
|
||||
impl<'client, T> PaginatedList<'client, T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
pub fn from<S: AsRef<str>>(client: &'client Client, url: S, ttl: Duration) -> Result<Self> {
|
||||
Ok(PaginatedList {
|
||||
client,
|
||||
ttl,
|
||||
next_page: Some(url.as_ref().into()),
|
||||
__item: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'client, T> PaginatedList<'client, 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)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'client, T> Iterator for PaginatedList<'client, T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
type Item = Result<Vec<T>>;
|
||||
|
||||
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 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))
|
||||
});
|
||||
match res {
|
||||
Ok((val, this_page, next_page, last_page)) => {
|
||||
// Only update next_page, if we're not on the last page!
|
||||
// This should be safe for all cases
|
||||
if this_page.unwrap_or_default() < last_page.unwrap_or_default() {
|
||||
// OpenMensa returns empty lists for large pages
|
||||
// this is just to keep me sane
|
||||
if !val.is_empty() {
|
||||
self.next_page = next_page;
|
||||
}
|
||||
}
|
||||
Some(Ok(val))
|
||||
}
|
||||
Err(why) => {
|
||||
// Implicitly does not set the next_page url, so
|
||||
// this iterator is done now
|
||||
Some(Err(why))
|
||||
}
|
||||
}
|
||||
fn meta(&mut self) -> Result<&Meta> {
|
||||
self.meta.fetch(|| Meta::fetch(self.id))
|
||||
}
|
||||
}
|
||||
|
|
30
src/canteen/de.rs
Normal file
30
src/canteen/de.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
use crate::cache::Fetchable;
|
||||
|
||||
use super::{CanteenId, Meta};
|
||||
|
||||
/// For deserializing responses from API/canteens.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CanteenDeserialized {
|
||||
id: CanteenId,
|
||||
name: String,
|
||||
city: String,
|
||||
address: String,
|
||||
coordinates: Option<[f32; 2]>,
|
||||
}
|
||||
|
||||
impl From<CanteenDeserialized> for super::Canteen {
|
||||
fn from(raw: CanteenDeserialized) -> Self {
|
||||
Self {
|
||||
id: raw.id,
|
||||
meta: Fetchable::Fetched(Meta {
|
||||
name: raw.name,
|
||||
city: raw.city,
|
||||
address: raw.address,
|
||||
coordinates: raw.coordinates,
|
||||
}),
|
||||
meals: Fetchable::None,
|
||||
}
|
||||
}
|
||||
}
|
10
src/canteen/ser.rs
Normal file
10
src/canteen/ser.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
use serde::Serialize;
|
||||
|
||||
use super::{CanteenId, Meta};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CanteenCompleteWithoutMeals<'c> {
|
||||
pub id: CanteenId,
|
||||
#[serde(flatten)]
|
||||
pub meta: &'c Meta,
|
||||
}
|
|
@ -63,6 +63,22 @@ pub enum Command {
|
|||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct CanteensCommand {
|
||||
/// Ignore other arguments. List all canteens.
|
||||
#[structopt(long, short)]
|
||||
pub all: bool,
|
||||
|
||||
#[structopt(flatten)]
|
||||
pub geo: GeoCommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, StructOpt)]
|
||||
pub enum CloseCommand {
|
||||
/// Show meals from canteens around you. Will overwrite --id.
|
||||
Close(GeoCommand),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, StructOpt)]
|
||||
pub struct GeoCommand {
|
||||
/// Latitude of your position. If omitted, geoip will be used to guess it.
|
||||
#[structopt(long)]
|
||||
pub lat: Option<f32>,
|
||||
|
@ -74,10 +90,6 @@ pub struct CanteensCommand {
|
|||
/// Maximum distance of potential canteens from your position in km.
|
||||
#[structopt(long, short, default_value = "10")]
|
||||
pub radius: f32,
|
||||
|
||||
/// Ignore other arguments. List all canteens.
|
||||
#[structopt(long, short)]
|
||||
pub all: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, StructOpt)]
|
||||
|
@ -144,6 +156,9 @@ pub struct MealsCommand {
|
|||
|
||||
#[structopt(long, env = "MENSA_FAVS_CATEGORY_SUB")]
|
||||
pub no_favs_cat: Vec<Regex>,
|
||||
|
||||
#[structopt(subcommand)]
|
||||
pub close: Option<CloseCommand>,
|
||||
}
|
||||
|
||||
arg_enum! {
|
||||
|
@ -185,6 +200,7 @@ impl Default for MealsCommand {
|
|||
no_favs_tag: vec![],
|
||||
favs_cat: vec![],
|
||||
no_favs_cat: vec![],
|
||||
close: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ lazy_static! {
|
|||
static ref REQUEST_TIMEOUT: StdDuration = StdDuration::from_secs(10);
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all(deserialize = "kebab-case"))]
|
||||
pub struct ConfigFile {
|
||||
#[serde(default)]
|
||||
|
@ -58,7 +58,7 @@ impl ConfigFile {
|
|||
pub type CanteensState<'s> = State<'s, CanteensCommand>;
|
||||
pub type MealsState<'s> = State<'s, MealsCommand>;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct State<'s, Cmd> {
|
||||
pub config: Option<ConfigFile>,
|
||||
pub client: Client,
|
||||
|
|
|
@ -26,15 +26,16 @@ struct LatLong {
|
|||
/// This will use the cli arguments if given and
|
||||
/// fetch any missing values from api.geoip.rs.
|
||||
pub fn fetch(state: &CanteensState) -> Result<(f32, f32)> {
|
||||
Ok(if state.cmd.lat.is_none() || state.cmd.long.is_none() {
|
||||
let geo = &state.cmd.geo;
|
||||
Ok(if geo.lat.is_none() || geo.long.is_none() {
|
||||
let guessed = fetch_geoip(&state.client)?;
|
||||
(
|
||||
state.cmd.lat.unwrap_or(guessed.latitude),
|
||||
state.cmd.long.unwrap_or(guessed.longitude),
|
||||
geo.lat.unwrap_or(guessed.latitude),
|
||||
geo.long.unwrap_or(guessed.longitude),
|
||||
)
|
||||
} else {
|
||||
// Cannot panic, due to above if
|
||||
(state.cmd.lat.unwrap(), state.cmd.long.unwrap())
|
||||
(geo.lat.unwrap(), geo.long.unwrap())
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -108,6 +108,7 @@ mod config;
|
|||
mod error;
|
||||
mod geoip;
|
||||
mod meal;
|
||||
mod pagination;
|
||||
mod tag;
|
||||
|
||||
use crate::{
|
||||
|
@ -166,8 +167,8 @@ fn real_main() -> Result<()> {
|
|||
}
|
||||
Some(Command::Canteens(cmd)) => {
|
||||
let state = State::from(state, cmd);
|
||||
let canteens = Canteen::fetch(&state)?;
|
||||
Canteen::print_all(&state, &canteens)?;
|
||||
let mut canteens = Canteen::fetch(&state)?;
|
||||
Canteen::print_all(&state, &mut canteens)?;
|
||||
}
|
||||
Some(Command::Tags) => {
|
||||
Tag::print_all(&state)?;
|
||||
|
|
|
@ -5,12 +5,16 @@ use lazy_static::lazy_static;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::{
|
||||
cache::fetch_json,
|
||||
config::{args::MealsCommand, MealsState, PriceTags},
|
||||
error::{pass_info, Result},
|
||||
canteen::{Canteen, CanteenId},
|
||||
config::{
|
||||
args::{CanteensCommand, CloseCommand, GeoCommand, MealsCommand},
|
||||
MealsState, PriceTags,
|
||||
},
|
||||
error::{pass_info, Result, ResultExt},
|
||||
get_sane_terminal_dimensions, print_json,
|
||||
tag::Tag,
|
||||
State, ENDPOINT,
|
||||
|
@ -33,7 +37,7 @@ lazy_static! {
|
|||
static ref TTL_MEALS: Duration = Duration::hours(1);
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(from = "RawMeal")]
|
||||
pub struct Meal {
|
||||
#[serde(rename = "id")]
|
||||
|
@ -55,7 +59,7 @@ struct RawMeal {
|
|||
category: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(debug, serde(deny_unknown_fields))]
|
||||
pub struct Prices {
|
||||
students: f32,
|
||||
|
@ -108,32 +112,55 @@ impl Meal {
|
|||
/// Fetch the meals.
|
||||
///
|
||||
/// This will respect passed cli arguments and the configuration.
|
||||
pub fn fetch(state: &MealsState) -> Result<Vec<Self>> {
|
||||
let url = format!(
|
||||
"{}/canteens/{}/days/{}/meals",
|
||||
ENDPOINT,
|
||||
state.canteen_id()?,
|
||||
state.date()
|
||||
);
|
||||
fetch_json(&state.client, url, *TTL_MEALS)
|
||||
pub fn fetch(state: &MealsState) -> Result<HashMap<CanteenId, Vec<Self>>> {
|
||||
match state.cmd.close {
|
||||
Some(CloseCommand::Close(ref close)) => {
|
||||
let meals = fetch_canteens_for_close_command(state, close)?
|
||||
.into_iter()
|
||||
.map(
|
||||
|canteen| match fetch_meals_for_canteen_id(state, canteen.id()) {
|
||||
Ok(meals) => Ok((canteen.id(), meals)),
|
||||
Err(why) => Err(why),
|
||||
},
|
||||
)
|
||||
// Drop any failed canteens with a warning
|
||||
.filter_map(ResultExt::log_warn)
|
||||
.collect();
|
||||
Ok(meals)
|
||||
}
|
||||
None => {
|
||||
let id = state.canteen_id()?;
|
||||
let meals = fetch_meals_for_canteen_id(state, id)?;
|
||||
let mut map = HashMap::new();
|
||||
map.insert(id, meals);
|
||||
Ok(map)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Print the given meals.
|
||||
///
|
||||
/// Thi will respect passed cli arguments and the configuration.
|
||||
pub fn print_all(state: &MealsState, meals: &[Self]) -> Result<()> {
|
||||
pub fn print_all(state: &MealsState, meals: &HashMap<CanteenId, Vec<Self>>) -> Result<()> {
|
||||
// Load the filter which is used to select which meals to print.
|
||||
let filter = state.get_filter();
|
||||
// Load the favourites which will be used for marking meals.
|
||||
let favs = state.get_favs_rule();
|
||||
let meals = meals.iter().filter(|meal| filter.is_match(meal));
|
||||
// Filter all meals
|
||||
let meal_map = meals.iter().map(|(id, meals)| {
|
||||
let keep = meals.iter().filter(|meal| filter.is_match(meal));
|
||||
(id, keep.collect::<Vec<_>>())
|
||||
});
|
||||
if state.args.json {
|
||||
print_json(&meals.collect::<Vec<_>>())
|
||||
print_json(&meal_map.collect::<HashMap<_, _>>())
|
||||
} else {
|
||||
for meal in meals {
|
||||
let is_fav = favs.is_match(meal);
|
||||
println!();
|
||||
meal.print(state, is_fav);
|
||||
for (id, meals) in meal_map {
|
||||
println!("{}", id);
|
||||
for meal in meals {
|
||||
let is_fav = favs.is_match(meal);
|
||||
println!();
|
||||
meal.print(state, is_fav);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -279,6 +306,28 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
fn fetch_canteens_for_close_command(
|
||||
state: &MealsState,
|
||||
close: &GeoCommand,
|
||||
) -> Result<Vec<Canteen>> {
|
||||
let canteens_cmd = CanteensCommand {
|
||||
all: false,
|
||||
geo: close.clone(),
|
||||
};
|
||||
let canteen_state = State::from(state.clone(), &canteens_cmd);
|
||||
Canteen::fetch(&canteen_state)
|
||||
}
|
||||
|
||||
fn fetch_meals_for_canteen_id(state: &MealsState, canteen_id: usize) -> Result<Vec<Meal>> {
|
||||
let url = format!(
|
||||
"{}/canteens/{}/days/{}/meals",
|
||||
ENDPOINT,
|
||||
canteen_id,
|
||||
state.date()
|
||||
);
|
||||
fetch_json(&state.client, url, *TTL_MEALS)
|
||||
}
|
||||
|
||||
impl fmt::Display for Note {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
|
|
84
src/pagination.rs
Normal file
84
src/pagination.rs
Normal file
|
@ -0,0 +1,84 @@
|
|||
use chrono::Duration;
|
||||
use reqwest::blocking::Client;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use crate::{
|
||||
cache,
|
||||
error::{Error, Result},
|
||||
};
|
||||
|
||||
pub struct PaginatedList<'client, T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
client: &'client Client,
|
||||
next_page: Option<String>,
|
||||
ttl: Duration,
|
||||
__item: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<'client, T> PaginatedList<'client, T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
pub fn from<S: AsRef<str>>(client: &'client Client, url: S, ttl: Duration) -> Result<Self> {
|
||||
Ok(PaginatedList {
|
||||
client,
|
||||
ttl,
|
||||
next_page: Some(url.as_ref().into()),
|
||||
__item: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'client, T> PaginatedList<'client, 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)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'client, T> Iterator for PaginatedList<'client, T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
type Item = Result<Vec<T>>;
|
||||
|
||||
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 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))
|
||||
});
|
||||
match res {
|
||||
Ok((val, this_page, next_page, last_page)) => {
|
||||
// Only update next_page, if we're not on the last page!
|
||||
// This should be safe for all cases
|
||||
if this_page.unwrap_or_default() < last_page.unwrap_or_default() {
|
||||
// OpenMensa returns empty lists for large pages
|
||||
// this is just to keep me sane
|
||||
if !val.is_empty() {
|
||||
self.next_page = next_page;
|
||||
}
|
||||
}
|
||||
Some(Ok(val))
|
||||
}
|
||||
Err(why) => {
|
||||
// Implicitly does not set the next_page url, so
|
||||
// this iterator is done now
|
||||
Some(Err(why))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue