use std::fmt::Debug;
use serde::{Deserialize, Serialize};
pub use crate::{
cursor::Cursor,
data_source::{DataSource, DataSourceRequest},
error::Error,
offset::Offset,
request::Request,
response::Response,
};
mod cursor;
mod data_source;
mod error;
mod offset;
mod request;
mod response;
pub async fn pagination<Item, Filters, OffsetValue>(
data_source: impl DataSource<Item, Filters, OffsetValue>,
request: Request<Filters>,
default_limit: u16,
) -> Result<Response<Item>, Error>
where
Item: Clone + for<'a> Deserialize<'a> + Offset<OffsetValue> + Serialize,
Filters: Clone + for<'a> Deserialize<'a> + PartialEq + Serialize,
OffsetValue: Clone + Debug + Default + for<'a> Deserialize<'a> + Serialize,
{
let cursor = match request.after {
Some(raw) => Cursor::<Filters, OffsetValue>::parse(raw).map(Some),
_ => Ok(None),
}?;
let limit = request.limit;
let offset = cursor.as_ref().map(|c| c.offset());
let data_source_request =
DataSourceRequest::new(request.filters, offset, limit.unwrap_or(default_limit));
if let Some(cursor) = cursor {
if cursor.req().filters() != data_source_request.filters() {
return Err(Error::CursorAndParamsAreDifferent);
}
}
let items = data_source
.get_items(data_source_request.clone())
.await
.map_err(|e| Error::DataSourceFailed(e.to_string()))?;
Response::from_items_and_req(items, data_source_request)
}
#[cfg(test)]
mod tests {
use serde::{Deserialize, Serialize};
use crate::data_source::{DataSource, DataSourceRequest};
use crate::{pagination, Error, Offset, Request, Response};
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
struct Filters {
param1: Option<bool>,
param2: Option<i64>,
}
impl Default for Filters {
fn default() -> Self {
Self {
param1: Some(true),
param2: Some(10),
}
}
}
const DEFAULT_LIMIT: u16 = 100;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
struct Item(i32);
type OffsetValue = u64;
impl Offset<OffsetValue> for Item {
fn offset(&self) -> OffsetValue {
last_item_timestamp()
}
}
fn last_item_timestamp() -> OffsetValue {
100
}
struct MockDataSource {
get_items: fn(DataSourceRequest<Filters, OffsetValue>) -> anyhow::Result<Vec<Item>>,
}
impl Default for MockDataSource {
fn default() -> Self {
Self {
get_items:
|req: DataSourceRequest<Filters, OffsetValue>| -> anyhow::Result<Vec<Item>> {
(0..req.limit_with_reserve())
.map(|i| Item(i as i32))
.map(Ok)
.collect()
},
}
}
}
#[async_trait::async_trait]
impl DataSource<Item, Filters, OffsetValue> for MockDataSource {
type Error = anyhow::Error;
async fn get_items(
&self,
req: DataSourceRequest<Filters, OffsetValue>,
) -> Result<Vec<Item>, Self::Error> {
(self.get_items)(req)
}
}
#[tokio::test]
async fn pagination_sanity_check() {
let mock_data_source = MockDataSource::default();
let req = Request {
filters: Filters {
param1: Some(true),
param2: Some(10),
},
limit: Some(10),
after: None,
};
let result = pagination::<Item, Filters, OffsetValue>(mock_data_source, req, 0)
.await
.unwrap();
assert_eq!(result, Response {
last_cursor: Some("eyJvZmZzZXQiOjEwMCwicmVxIjp7InBhcmFtMSI6dHJ1ZSwicGFyYW0yIjoxMCwib2Zmc2V0IjpudWxsLCJsaW1pdCI6MTB9fQ==".to_owned()),
has_next_page: true,
data: vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9].into_iter().map(Item).collect::<Vec<_>>(),
});
}
#[tokio::test]
async fn pagination_last_test() {
let mock_data_source = MockDataSource {
get_items: |params: DataSourceRequest<Filters, OffsetValue>| {
(0..params.limit_with_reserve() - 2)
.map(|i| Item(i as i32))
.map(Ok)
.collect()
},
};
let req = Request {
filters: Filters {
param1: Some(true),
param2: Some(10),
},
limit: Some(10),
after: None,
};
let result = pagination::<Item, Filters, OffsetValue>(mock_data_source, req, DEFAULT_LIMIT)
.await
.unwrap();
assert_eq!(result.data.len(), 9);
assert!(!result.has_next_page);
}
#[tokio::test]
async fn pagination_empty_test() {
let mock_data_source = MockDataSource {
get_items: |_| Ok(vec![]),
};
let req = Request {
filters: Filters {
param1: Some(true),
param2: Some(10),
},
limit: Some(4),
after: None,
};
let result = pagination::<Item, Filters, OffsetValue>(mock_data_source, req, DEFAULT_LIMIT)
.await
.unwrap();
assert_eq!(result.data.len(), 0);
assert!(!result.has_next_page);
}
#[tokio::test]
async fn pagination_with_default_limit_test() {
let req = Request {
filters: Filters {
param1: Some(true),
param2: Some(10),
},
limit: None,
after: None,
};
let result = pagination::<Item, Filters, OffsetValue>(MockDataSource::default(), req, 7)
.await
.unwrap();
assert_eq!(result.data.len(), 7);
assert!(result.has_next_page);
}
#[tokio::test]
async fn pagination_from_cursor_test() {
let after = Some("eyJvZmZzZXQiOjEwMCwicmVxIjp7InBhcmFtMSI6dHJ1ZSwicGFyYW0yIjoxMCwib2Zmc2V0IjoxMDAsImxpbWl0Ijo0fX0=".to_owned());
let req = Request {
filters: Filters {
param1: Some(true),
param2: Some(10),
},
limit: Some(4),
after,
};
let result =
pagination::<Item, Filters, OffsetValue>(MockDataSource::default(), req, DEFAULT_LIMIT)
.await
.unwrap();
assert_eq!(result.data.len(), 4);
assert!(result.has_next_page);
}
#[tokio::test]
async fn cursor_and_params_together() {
let mock_data_source = MockDataSource::default();
let after = Some("eyJvZmZzZXQiOjEwMCwicmVxIjp7InBhcmFtMSI6dHJ1ZSwicGFyYW0yIjoxMCwib2Zmc2V0IjoxMDAsImxpbWl0Ijo0fX0=".to_string());
let req = Request {
filters: Filters {
param1: Some(true),
param2: Some(10),
},
limit: Some(4),
after,
};
let result = pagination::<Item, Filters, OffsetValue>(mock_data_source, req, 10)
.await
.unwrap();
assert_eq!(result.data.len(), 4);
assert!(result.has_next_page);
}
#[tokio::test]
async fn error_if_cursor_and_params_is_different() {
let mock_data_source = MockDataSource::default();
let after = Some("eyJvZmZzZXQiOjEwMCwicmVxIjp7InBhcmFtMSI6dHJ1ZSwicGFyYW0yIjoxMCwib2Zmc2V0IjoxMDAsImxpbWl0Ijo0fX0=".to_owned());
let req = Request {
filters: Filters {
param1: Some(false),
param2: Some(1),
},
limit: Some(4),
after,
};
let result = pagination::<Item, Filters, OffsetValue>(mock_data_source, req, 10)
.await
.err()
.unwrap();
assert_eq!(result, Error::CursorAndParamsAreDifferent);
}
}