use std::num::NonZeroU32;
use governor::Quota;
use ingester::{Endpoint, Error, RateLimit, Request, Response, ingest};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Story {
#[serde(rename = "objectID")]
id: String,
title: Option<String>,
url: Option<String>,
}
struct FrontPage {
page: u32,
nb_pages: Option<u64>,
}
impl FrontPage {
fn new() -> Self {
Self {
page: 0,
nb_pages: None,
}
}
}
impl Endpoint for FrontPage {
type Item = Story;
fn next_request(&mut self, last: Option<&Response>) -> Option<Request> {
if let Some(resp) = last {
self.nb_pages = resp.json::<serde_json::Value>().ok()?["nbPages"].as_u64();
}
if self.nb_pages.is_some_and(|max| u64::from(self.page) >= max) {
return None;
}
let url = format!(
"https://hn.algolia.com/api/v1/search?tags=front_page&page={}",
self.page
);
self.page += 1;
Some(Request::get(url.parse().expect("valid URL")))
}
fn parse(&self, response: &Response) -> Result<Vec<Story>, Error> {
let body: serde_json::Value = response.json()?;
Ok(serde_json::from_value(body["hits"].clone())?)
}
}
#[tokio::main]
async fn main() -> Result<(), Error> {
let rate_limit = RateLimit::from_quota(Quota::per_second(
NonZeroU32::new(2).expect("dev can't read and constructed a NonZeroU32 with 0"),
));
let stories = ingest(FrontPage::new())
.with_rate_limit(rate_limit)
.collect()
.await?;
println!("fetched {} stories", stories.len());
for story in stories.iter().take(10) {
println!(
"- [{}] {} ({})",
story.id,
story.title.as_deref().unwrap_or("<no title>"),
story.url.as_deref().unwrap_or("<no url>")
);
}
Ok(())
}