use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct Page<T> {
pub items: Vec<T>,
#[serde(default)]
pub next_cursor: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct User {
pub id: u64,
pub name: String,
pub email: String,
}
use std::collections::VecDeque;
use crate::page::Page;
pub trait PageFetcher<T> {
type Error;
fn fetch(&self, cursor: Option<&str>) -> Result<Page<T>, Self::Error>;
}
pub struct PaginatedIter<F, T>
where
F: PageFetcher<T>,
{
fetcher: F,
buffer: VecDeque<T>,
cursor: Option<String>,
done: bool,
}
impl<F, T> PaginatedIter<F, T>
where
F: PageFetcher<T>,
{
pub fn new(fetcher: F) -> Self {
PaginatedIter {
fetcher,
buffer: VecDeque::new(),
cursor: None,
done: false,
}
}
}
impl<F, T> Iterator for PaginatedIter<F, T>
where
F: PageFetcher<T>,
{
type Item = Result<T, F::Error>;
fn next(&mut self) -> Option<Self::Item> {
if let Some(item) = self.buffer.pop_front() {
return Some(Ok(item));
}
if self.done {
return None;
}
match self.fetcher.fetch(self.cursor.as_deref()) {
Ok(page) => {
self.buffer.extend(page.items);
self.cursor = page.next_cursor;
self.done = self.cursor.is_none();
self.buffer.pop_front().map(Ok)
}
Err(e) => {
self.done = true;
Some(Err(e))
}
}
}
}
use reqwest::blocking::Client;
use crate::page::{Page, User};
use crate::paginated::{PageFetcher, PaginatedIter};
pub struct UserClient {
http: Client,
base_url: String,
}
struct UserFetcher<'a> {
http: &'a Client,
endpoint: String,
}
impl<'a> PageFetcher<User> for UserFetcher<'a> {
type Error = reqwest::Error;
fn fetch(&self, cursor: Option<&str>) -> Result<Page<User>, Self::Error> {
let mut req = self.http.get(&self.endpoint);
if let Some(c) = cursor {
req = req.query(&[("cursor", c)]);
}
req.send()?.error_for_status()?.json::<Page<User>>()
}
}
impl UserClient {
pub fn new(base_url: impl Into<String>) -> Self {
UserClient {
http: Client::new(),
base_url: base_url.into(),
}
}
pub fn users(&self) -> impl Iterator<Item = Result<User, reqwest::Error>> + '_ {
let fetcher = UserFetcher {
http: &self.http,
endpoint: format!("{}/users", self.base_url),
};
PaginatedIter::new(fetcher)
}
}
This snippet shows how to wrap a cursor-based paginated HTTP API behind Rust's Iterator trait so callers can consume every record with ordinary loops, for, and adaptors like take, filter, and collect — without ever knowing that pages are fetched lazily under the hood. The pattern turns a stateful network resource into a pull-based stream, where each call to next yields exactly one item and network requests happen only when the current buffer runs dry.
In page.rs, the wire format is modeled with serde: Page<T> carries a vector of items plus an optional next_cursor. A None cursor is the API's end-of-stream signal, which the iterator later uses to stop fetching. Keeping the DTO generic over T means the same paging machinery works for users, orders, or any resource.
The core is paginated.rs. PageFetcher is a small trait abstracting one thing — given an optional cursor, return the next Page. This indirection decouples the iterator logic from the HTTP client, making it trivial to swap in a fake fetcher for tests. PaginatedIter holds the fetcher, a buffer of already-fetched items (drained front-to-back via VecDeque), the current cursor, and a done flag. Its Iterator::Item is Result<T, Error> rather than bare T, because network calls fail; surfacing errors through the item type lets callers handle them per-element instead of panicking. The next implementation is the crux: it returns buffered items immediately, stops early when done is set, and otherwise fetches one page, refills the buffer, and updates done based on whether next_cursor was present. Note that a fetch error sets done = true so a persistently failing source can't loop forever.
client.rs supplies the real PageFetcher using reqwest's blocking client, attaching the cursor as a query parameter and deserializing the JSON body. UserClient::users returns impl Iterator, hiding the concrete type entirely.
The main trade-off is laziness versus eager collection: iteration spreads latency across the stream and lets callers bail out early with take, but it also means a single for loop can issue many sequential requests. Because Item is a Result, collect::<Result<Vec<_>, _>>() conveniently short-circuits on the first failure.
Related snips
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
struct Config<'a> {
name: &'a str,
value: &'a str,
}
fn parse_config(line: &str) -> Config {
Lifetime annotations for flexible borrowing in structs
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
use tracing::{info, instrument};
#[instrument]
fn process_request(user_id: u64) {
info!(user_id, "Processing request");
// Work happens here
tracing for structured logging and distributed tracing
Share this code
Here's the card — post it anywhere.