The ingester crate reads paginated APIs and makes an async stream of parsed
items.
You tell the crate two things:
- The request that it must send next.
- The procedure that parses each response into items.
The crate then does the work. It sends each request through an HTTP client
that you can replace. It obeys a rate limit that you can configure. It gives
the parsed items to you as a futures::Stream.
Your endpoint sees the last response before it makes the next request. Thus pagination with cursors, with tokens, or with a page count in the response body is possible.
This example gets all the front-page story IDs from the Hacker News Algolia API. The endpoint gets the page count from the first response.
use ingester::{Endpoint, Error, Request, Response, ingest};
struct StoryIds {
page: u32,
max_pages: Option<u64>,
}
impl Endpoint for StoryIds {
type Item = u64;
fn next_request(&mut self, last: Option<&Response>) -> Option<Request> {
if let Some(resp) = last {
self.max_pages = resp.json::<serde_json::Value>().ok()?["nbPages"].as_u64();
}
if self.max_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().unwrap()))
}
fn parse(&self, response: &Response) -> Result<Vec<u64>, Error> {
let body: serde_json::Value = response.json()?;
Ok(body["hits"]
.as_array()
.into_iter()
.flatten()
.filter_map(|hit| hit["objectID"].as_str()?.parse().ok())
.collect())
}
}
async fn example() -> Result<(), Error> {
let stories = ingest(StoryIds { page: 0, max_pages: None })
.collect()
.await?;
Ok(())
}The collect() method collects all the items into a Vec. As an
alternative, use into_stream() to get each item when it arrives. If an
error occurs, the stream gives one Err item and then stops.
Implement the Endpoint trait. The trait has two methods:
next_request(&mut self, last: Option<&Response>) -> Option<Request>— make the next request. Thelastparameter contains the previous response. On the first call,lastisNone. For cursor pagination, read the cursor from the last response body. For a page counter, keep the counter inself. ReturnNoneto stop.parse(&self, response: &Response) -> Result<Vec<Item>, Error>— parse a response into zero or more items.
The ingest(endpoint) function makes an Ingester with the default
configuration. The default configuration is a reqwest HTTP client and no
rate limit. Use with_client and with_rate_limit to change the
configuration:
let ingester = ingest(endpoint)
.with_rate_limit(RateLimit::from_quota(quota)) // governor feature
.with_client(Reqwest::builder().user_agent("my-scraper").build()?);Enable the governor feature to use a governor rate limiter. This
example permits a maximum of 2 requests each second:
use std::num::NonZeroU32;
use governor::Quota;
use ingester::RateLimit;
let stories = ingest(StoryIds { page: 0, max_pages: None })
.with_rate_limit(RateLimit::from_quota(Quota::per_second(
NonZeroU32::new(2).unwrap(),
)))
.collect()
.await?;The HTTP client and the rate limiter are traits. You can supply your own implementations. Some examples are:
- A different HTTP library.
- A rate limiter that your full application shares.
- A mock client for tests.
Implement ingester::client::Backend for an HTTP client. Implement
ingester::rate_limit::Backend for a rate limiter. Give the backend
directly to Ingester::new, with_client, or with_rate_limit. The crate
wraps the backend for you.
struct MockClient { /* prepared responses */ }
impl ingester::client::Backend for MockClient {
async fn execute(&self, req: Request) -> Result<Response, Error> {
// return the next prepared response
}
}
let items = Ingester::new(endpoint, MockClient::new(responses), NoRateLimit)
.collect()
.await?;| Feature | Default | Effect |
|---|---|---|
reqwest |
yes | The default HTTP client backend and the ingest() function |
governor |
no | governor rate limiters as RateLimit backends, from_quota |
If you set default-features = false, the crate contains no HTTP client.
You must then supply your own client::Backend.
- MIT license (LICENSE)