#![warn(missing_docs)]
use juniper::{FieldResult, GraphQLObject};
use std::convert::TryInto;
mod traits;
pub trait RelayConnectionNode {
type Cursor: std::string::ToString + std::str::FromStr + Clone;
fn cursor(&self) -> Self::Cursor;
fn connection_type_name() -> &'static str;
fn edge_type_name() -> &'static str;
}
#[derive(Debug)]
#[doc(hidden)]
pub struct RelayConnectionEdge<N> {
node: N,
cursor: String,
}
#[derive(Debug, GraphQLObject)]
#[graphql(name = "PageInfo")]
#[doc(hidden)]
pub struct RelayConnectionPageInfo {
has_previous_page: bool,
has_next_page: bool,
start_cursor: Option<String>,
end_cursor: Option<String>,
}
#[derive(Debug)]
pub struct RelayConnection<N> {
edges: Vec<RelayConnectionEdge<N>>,
page_info: RelayConnectionPageInfo,
}
fn leq_zero(val: i64) -> Result<i64, &'static str> {
if val < 0 {
Err("Pagination argument must be positive")
} else {
Ok(val)
}
}
impl<N> RelayConnection<N>
where
N: RelayConnectionNode,
<N::Cursor as std::str::FromStr>::Err: std::fmt::Display,
{
fn closure_args(
first: Option<i64>,
after: Option<String>,
before: Option<String>,
) -> FieldResult<(Option<N::Cursor>, Option<N::Cursor>, Option<i64>)> {
let after: Option<N::Cursor> = after.map(|s| s.parse()).transpose()?;
let before: Option<N::Cursor> = before.map(|s| s.parse()).transpose()?;
let limit = first.map(|l| l + 1);
Ok((after, before, limit))
}
fn build_connection(
first: Option<i64>,
last: Option<i64>,
edges: Vec<N>,
) -> FieldResult<RelayConnection<N>> {
let edges_len: i64 = edges.len().try_into()?;
let has_previous_page = if let Some(last) = last {
edges_len > last
} else {
false
};
let has_next_page = if let Some(first) = first {
edges_len > first
} else {
false
};
let first = first.unwrap_or(edges_len);
let last = last.unwrap_or(edges_len);
let len_after_take = i64::min(edges_len, first);
let skip = i64::max(0, len_after_take - last);
let edges: Vec<RelayConnectionEdge<N>> = edges
.into_iter()
.take(first.try_into()?)
.skip(skip.try_into()?)
.map(|node| RelayConnectionEdge {
cursor: node.cursor().to_string(),
node,
})
.collect();
Ok(RelayConnection {
page_info: RelayConnectionPageInfo {
has_previous_page,
has_next_page,
start_cursor: edges.first().map(|edge| edge.cursor.clone()),
end_cursor: edges.last().map(|edge| edge.cursor.clone()),
},
edges,
})
}
pub fn new<L>(
first: Option<i32>,
after: Option<String>,
last: Option<i32>,
before: Option<String>,
load: L,
) -> FieldResult<RelayConnection<N>>
where
L: FnOnce(Option<N::Cursor>, Option<N::Cursor>, Option<i64>) -> FieldResult<Vec<N>>,
{
let first: Option<i64> = first.map(Into::into).map(leq_zero).transpose()?;
let last: Option<i64> = last.map(Into::into).map(leq_zero).transpose()?;
let (after, before, limit) = Self::closure_args(first, after, before)?;
let edges = load(after, before, limit)?;
Self::build_connection(first, last, edges)
}
pub async fn new_async<L, F>(
first: Option<i32>,
after: Option<String>,
last: Option<i32>,
before: Option<String>,
load: L,
) -> FieldResult<RelayConnection<N>>
where
L: FnOnce(Option<N::Cursor>, Option<N::Cursor>, Option<i64>) -> F,
F: std::future::Future<Output = FieldResult<Vec<N>>>,
{
let first: Option<i64> = first.map(Into::into).map(leq_zero).transpose()?;
let last: Option<i64> = last.map(Into::into).map(leq_zero).transpose()?;
let (after, before, limit) = Self::closure_args(first, after, before)?;
let edges = load(after, before, limit).await?;
Self::build_connection(first, last, edges)
}
pub fn empty() -> Self {
Self {
edges: vec![],
page_info: RelayConnectionPageInfo {
has_previous_page: false,
has_next_page: false,
start_cursor: None,
end_cursor: None,
},
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[derive(GraphQLObject)]
struct FakeNode {
id: i32,
}
impl RelayConnectionNode for FakeNode {
type Cursor = i32;
fn cursor(&self) -> Self::Cursor {
self.id
}
fn connection_type_name() -> &'static str {
"FakeNodeConnection"
}
fn edge_type_name() -> &'static str {
"FakeNodeConnectionEdge"
}
}
#[test]
fn closure_args_smoke_test() {
assert_eq!(
RelayConnection::<FakeNode>::closure_args(Some(42), Some("8".into()), None),
Ok((Some(8), None, Some(43)))
);
assert_eq!(
RelayConnection::<FakeNode>::closure_args(None, None, Some("95".into())),
Ok((None, Some(95), None))
);
assert!(
RelayConnection::<FakeNode>::closure_args(None, Some("foo".to_string()), None).is_err()
);
}
}