use juniper_relay_helpers::{Cursor, OffsetCursor, PageInfo, PageRequest};
#[derive(Debug, Clone)]
pub struct PaginationMetadata {
pub total_count: i32,
pub page_request: Option<PageRequest>,
}
pub trait CursorProvider {
fn get_cursor_for_item<T>(
&self,
metadata: &PaginationMetadata,
item_idx: i32,
item: &T,
) -> impl Cursor;
fn get_page_info<T>(&self, metadata: &PaginationMetadata, items: &[T]) -> PageInfo;
}
pub struct OffsetCursorProvider;
impl CursorProvider for OffsetCursorProvider {
fn get_cursor_for_item<T>(
&self,
metadata: &PaginationMetadata,
item_idx: i32,
_item: &T,
) -> impl Cursor {
let mut offset_adjust = 0;
let default_cursor = OffsetCursor::default();
let current_cursor = match &metadata.page_request {
Some(pr) => match pr.parsed_cursor() {
Ok(c) => match c {
Some(cc) => {
offset_adjust = 1;
cc
}
None => default_cursor,
},
Err(_) => default_cursor,
},
None => default_cursor,
};
OffsetCursor {
offset: current_cursor.offset + offset_adjust + item_idx,
first: current_cursor.first,
}
}
fn get_page_info<T>(&self, metadata: &PaginationMetadata, items: &[T]) -> PageInfo {
let default_cursor = OffsetCursor::default();
let current_cursor = match &metadata.page_request {
Some(pr) => match pr.parsed_cursor() {
Ok(c) => c.unwrap_or(default_cursor),
Err(_) => default_cursor,
},
None => default_cursor,
};
let has_next_page = if let Some(pr) = &metadata.page_request {
if let Some(first) = pr.first {
current_cursor.offset + first < metadata.total_count
} else {
false
}
} else {
false
};
let last_index = items.len() - 1;
PageInfo {
has_prev_page: current_cursor.offset > 0,
has_next_page,
start_cursor: if !items.is_empty() {
Some(
self.get_cursor_for_item(metadata, 0, &items[0])
.to_encoded_string(),
)
} else {
None
},
end_cursor: if !items.is_empty() {
Some(
self.get_cursor_for_item(metadata, last_index as i32, &items[last_index])
.to_encoded_string(),
)
} else {
None
},
}
}
}
impl Default for OffsetCursorProvider {
fn default() -> Self {
Self::new()
}
}
impl OffsetCursorProvider {
pub fn new() -> Self {
OffsetCursorProvider
}
}
#[cfg(test)]
mod tests {
mod offset_cursor_provider {
use crate::{
Cursor, CursorProvider, OffsetCursor, OffsetCursorProvider, PageRequest,
PaginationMetadata,
};
#[derive(Debug, Clone)]
struct Location {
#[allow(dead_code)]
name: String,
}
fn data() -> Vec<Location> {
vec![
Location {
name: "LumiƩre".to_owned(),
},
Location {
name: "Flying Waters".to_owned(),
},
]
}
#[test]
fn test_page_info_no_request() {
let p = OffsetCursorProvider::new();
let pi = p.get_page_info(
&PaginationMetadata {
total_count: 2,
page_request: None,
},
&data(),
);
assert!(!pi.has_prev_page);
assert!(!pi.has_next_page);
assert_eq!(
pi.start_cursor,
Some(
OffsetCursor {
offset: 0,
first: None
}
.to_encoded_string()
)
);
assert_eq!(
pi.end_cursor,
Some(
OffsetCursor {
offset: 1,
first: None
}
.to_encoded_string()
)
);
}
#[test]
fn test_page_info_no_request_mismatch_results_count() {
let p = OffsetCursorProvider::new();
let pi = p.get_page_info(
&PaginationMetadata {
total_count: 27,
page_request: None,
},
&data(),
);
assert!(!pi.has_prev_page);
assert!(!pi.has_next_page);
assert_eq!(
pi.start_cursor,
Some(
OffsetCursor {
offset: 0,
first: None
}
.to_encoded_string()
)
);
assert_eq!(
pi.end_cursor,
Some(
OffsetCursor {
offset: 1,
first: None
}
.to_encoded_string()
)
);
}
#[test]
fn test_page_info_has_request_first_page() {
let p = OffsetCursorProvider::new();
let pi = p.get_page_info(
&PaginationMetadata {
total_count: 27,
page_request: Some(PageRequest {
first: Some(10),
after: None,
}),
},
&data(),
);
assert!(!pi.has_prev_page);
assert!(pi.has_next_page);
assert_eq!(
pi.start_cursor,
Some(
OffsetCursor {
offset: 0,
first: None
}
.to_encoded_string()
)
);
assert_eq!(
pi.end_cursor,
Some(
OffsetCursor {
offset: 1,
first: None
}
.to_encoded_string()
)
);
}
#[test]
fn test_page_info_paginating_through_set() {
let p = OffsetCursorProvider::new();
let total_items = 13;
let data = vec![
Location {
name: "LumiƩre".to_owned(),
},
Location {
name: "Spring Meadows".to_owned(),
},
Location {
name: "Flying Waters".to_owned(),
},
Location {
name: "Gestral Village".to_owned(),
},
Location {
name: "Stone Wave Cliffs".to_owned(),
},
];
let pi1 = p.get_page_info(
&PaginationMetadata {
total_count: total_items,
page_request: Some(PageRequest {
first: Some(5),
after: None,
}),
},
&data,
);
assert!(!pi1.has_prev_page);
assert!(pi1.has_next_page);
assert_eq!(
pi1.start_cursor,
Some(
OffsetCursor {
offset: 0,
first: None
}
.to_encoded_string()
)
);
assert_eq!(
pi1.end_cursor,
Some(
OffsetCursor {
offset: 4,
first: None
}
.to_encoded_string()
)
);
let pi2 = p.get_page_info(
&PaginationMetadata {
total_count: total_items,
page_request: Some(PageRequest {
first: Some(5),
after: pi1.end_cursor.clone(),
}),
},
&data,
);
assert!(pi2.has_prev_page);
assert!(pi2.has_next_page);
assert_eq!(
pi2.start_cursor,
Some(
OffsetCursor {
offset: 5,
first: None
}
.to_encoded_string()
)
);
assert_eq!(
pi2.end_cursor,
Some(
OffsetCursor {
offset: 9,
first: None
}
.to_encoded_string()
)
);
let pi3 = p.get_page_info(
&PaginationMetadata {
total_count: total_items,
page_request: Some(PageRequest {
first: Some(5),
after: pi2.end_cursor.clone(),
}),
},
&[data[0].clone(), data[1].clone(), data[2].clone()],
);
assert!(pi3.has_prev_page);
assert!(!pi3.has_next_page);
assert_eq!(
pi3.start_cursor,
Some(
OffsetCursor {
offset: 10,
first: None
}
.to_encoded_string()
)
);
assert_eq!(
pi3.end_cursor,
Some(
OffsetCursor {
offset: 12,
first: None
}
.to_encoded_string()
)
);
}
}
}