use crate::client::{Error, RequestOptions, Steel};
use futures::stream::Stream;
use serde::de::DeserializeOwned;
pub(crate) type NextCursor<T> = fn(&[T], Option<&str>) -> Option<String>;
pub struct Page<T> {
client: Steel,
path: String,
params: Vec<(String, String)>,
next_cursor: NextCursor<T>,
items_field: &'static str,
cursor_field: Option<&'static str>,
has_more_field: Option<&'static str>,
cursor_param: &'static str,
options: RequestOptions,
pub data: Vec<T>,
pub has_more: bool,
cursor: Option<String>,
}
impl<T: DeserializeOwned> Page<T> {
#[allow(clippy::too_many_arguments)]
pub(crate) async fn fetch(
client: Steel,
path: String,
params: Vec<(String, String)>,
next_cursor: NextCursor<T>,
items_field: &'static str,
cursor_field: Option<&'static str>,
has_more_field: Option<&'static str>,
cursor_param: &'static str,
options: RequestOptions,
) -> Result<Self, Error> {
let body: serde_json::Value = client
.request_with(
reqwest::Method::GET,
&path,
Some(¶ms),
None::<&()>,
&options,
)
.await?;
let (data, cursor, has_more) = parse_page(
&body,
next_cursor,
items_field,
cursor_field,
has_more_field,
)?;
Ok(Page {
client,
path,
params,
next_cursor,
items_field,
cursor_field,
has_more_field,
cursor_param,
options,
data,
has_more,
cursor,
})
}
pub async fn next_page(&self) -> Result<Option<Page<T>>, Error> {
if !self.has_more {
return Ok(None);
}
let cursor = match &self.cursor {
Some(c) => c.clone(),
None => return Ok(None),
};
let params = with_cursor(&self.params, self.cursor_param, cursor);
let page = Self::fetch(
self.client.clone(),
self.path.clone(),
params,
self.next_cursor,
self.items_field,
self.cursor_field,
self.has_more_field,
self.cursor_param,
self.options.clone(),
)
.await?;
Ok(Some(page))
}
pub fn into_stream(self) -> impl Stream<Item = Result<T, Error>> {
async_stream::try_stream! {
let mut current = self;
loop {
let Page { client, path, params, next_cursor, items_field, cursor_field, has_more_field, cursor_param, options, data, has_more, cursor } = current;
for item in data {
yield item;
}
let next = match (has_more, cursor) {
(true, Some(c)) => c,
_ => break,
};
let next_params = with_cursor(¶ms, cursor_param, next);
current = Page::fetch(client, path, next_params, next_cursor, items_field, cursor_field, has_more_field, cursor_param, options).await?;
}
}
}
}
fn with_cursor(params: &[(String, String)], key: &str, value: String) -> Vec<(String, String)> {
let mut out: Vec<(String, String)> = params.iter().filter(|(k, _)| k != key).cloned().collect();
out.push((key.to_string(), value));
out
}
#[allow(clippy::type_complexity, clippy::result_large_err)]
fn parse_page<T: DeserializeOwned>(
body: &serde_json::Value,
next_cursor: NextCursor<T>,
items_field: &str,
cursor_field: Option<&str>,
has_more_field: Option<&str>,
) -> Result<(Vec<T>, Option<String>, bool), Error> {
let data: Vec<T> = match body.get(items_field) {
Some(v) => serde_json::from_value(v.clone())?,
None => Vec::new(),
};
let cursor_value = cursor_field
.and_then(|f| body.get(f))
.and_then(|v| v.as_str());
let cursor = next_cursor(&data, cursor_value);
let has_more = match has_more_field {
Some(f) => body.get(f).and_then(|v| v.as_bool()).unwrap_or(false),
None => cursor.is_some(),
};
Ok((data, cursor, has_more))
}
#[cfg(test)]
mod pagination_tests {
use super::*;
fn passthrough(_items: &[serde_json::Value], cursor: Option<&str>) -> Option<String> {
cursor.map(str::to_string)
}
#[test]
fn parse_page_is_field_name_agnostic() {
let shape_a = serde_json::json!({
"sessions": [{"id": "a"}, {"id": "b"}],
"nextCursor": "c2"
});
let (items, cursor, has_more): (Vec<serde_json::Value>, _, _) =
parse_page(&shape_a, passthrough, "sessions", Some("nextCursor"), None).unwrap();
assert_eq!(items.len(), 2);
assert_eq!(cursor.as_deref(), Some("c2"));
assert!(has_more);
let shape_b = serde_json::json!({
"data": [{"id": "x"}],
"cursor": serde_json::Value::Null
});
let (items, cursor, has_more): (Vec<serde_json::Value>, _, _) =
parse_page(&shape_b, passthrough, "data", Some("cursor"), None).unwrap();
assert_eq!(items.len(), 1);
assert_eq!(cursor, None);
assert!(!has_more);
}
#[test]
fn parse_page_respects_has_more_field() {
let body = serde_json::json!({"items": [], "hasMore": true, "nextCursor": "n"});
let (_items, _cursor, has_more): (Vec<serde_json::Value>, _, _) = parse_page(
&body,
passthrough,
"items",
Some("nextCursor"),
Some("hasMore"),
)
.unwrap();
assert!(has_more);
}
}