use crate::ui::page::{Page, Role, Seg};
use crate::ui::pages::{flash, loading_box};
#[derive(Debug)]
pub struct Fetched<'a, T: ?Sized> {
data: Option<&'a T>,
error: Option<&'a str>,
retry: &'a str,
}
impl<T: ?Sized> Clone for Fetched<'_, T> {
fn clone(&self) -> Self {
*self
}
}
impl<T: ?Sized> Copy for Fetched<'_, T> {}
impl<'a, T: ?Sized> Fetched<'a, T> {
pub(crate) fn ready(data: &'a T) -> Self {
let (data, error, retry) = (Some(data), None, "");
Self { data, error, retry }
}
pub fn new_unchecked(
data: Option<&'a T>,
loading: bool,
error: Option<&'a str>,
retry: &'a str,
) -> Self {
let error = error.filter(|_| data.is_none() && !loading);
Self { data, error, retry }
}
pub fn pick<U: ?Sized>(self, f: fn(&'a T) -> Option<&'a U>) -> Fetched<'a, U> {
let picked = self.data.map(f);
let wrong = matches!(picked, Some(None)).then_some("the wrong kind of data");
let (data, error, retry) = (picked.flatten(), self.error.or(wrong), self.retry);
Fetched { data, error, retry }
}
pub fn show(self, page: &mut Page, what: &str) -> Option<&'a T> {
self.show_or(page, what, |page| {
if page.lines.is_empty() {
loading_box(page, vec![Seg::new("Loading…", Role::Meta)]);
} else {
page.line(vec![Seg::new(format!("Loading {what}…"), Role::Meta)]);
}
})
}
pub fn show_or(
self,
page: &mut Page,
what: &str,
loading: impl FnOnce(&mut Page),
) -> Option<&'a T> {
self.show_with(page, what, loading, flash)
}
pub(crate) fn show_with(
self,
page: &mut Page,
what: &str,
loading: impl FnOnce(&mut Page),
failed: impl FnOnce(&mut Page, &str),
) -> Option<&'a T> {
#[expect(clippy::disallowed_methods, reason = "said here")]
match (self.error.is_some(), self.text(what)) {
(_, Ok(data)) => return Some(data),
(true, Err(why)) => failed(page, &why),
(false, Err(_)) => loading(page),
}
None
}
pub fn text(self, what: &str) -> Result<&'a T, String> {
let again = match self.retry {
"" => String::new(),
key => format!(". {key} tries again."),
};
match (self.data, self.error) {
(Some(data), _) => Ok(data),
(None, Some(err)) => {
let err = err.trim_end_matches('.');
Err(format!("Couldn't load {what}: {err}{again}"))
}
(None, None) => Err(format!("Loading {what}…")),
}
}
pub fn ready_unchecked(self) -> Option<&'a T> {
self.data
}
}
#[cfg(test)]
mod tests {
#![expect(clippy::disallowed_methods, reason = "tests each state")]
use super::*;
use crate::ui::page::PageLine;
impl<'a, T: ?Sized> Fetched<'a, T> {
pub(crate) fn failed(err: &'a str) -> Self {
Self::new_unchecked(None, false, Some(err), "r")
}
}
#[test]
fn each_state_shows_as_itself() {
let mut page = Page::new(80);
let ready = Fetched::new_unchecked(Some(&7), true, Some("old"), "r");
assert_eq!(ready.show(&mut page, "x"), Some(&7));
let _ = Fetched::<u8>::new_unchecked(None, false, None, "r").show(&mut page, "x");
let retrying = Fetched::<u8>::new_unchecked(None, true, Some("old"), "r");
assert_eq!(retrying.show(&mut page, "runs"), None);
let failed = Fetched::<u8>::new_unchecked(None, false, Some("timed out"), "r");
assert_eq!(failed.show(&mut page, "runs"), None);
let wrong = Fetched::ready(&7).pick(|_| None::<&u8>);
assert_eq!(wrong.ready_unchecked(), None);
let text: Vec<String> = page.lines.iter().map(PageLine::text).collect();
assert!(
text.first().is_some_and(|l| l.contains("Loading…")),
"{text:?}"
);
assert!(text.iter().any(|l| l == "Loading runs…"), "{text:?}");
let why = "Couldn't load runs: timed out. r tries again.";
assert!(text.iter().any(|l| l.contains(why)), "{text:?}");
let why = "Couldn't load it: the wrong kind of data";
assert_eq!(wrong.text("it"), Err(why.to_owned()));
}
}