use crate::cursor_errors::CursorError;
use base64::prelude::*;
use juniper::{GraphQLScalar, ParseScalarResult, ParseScalarValue, ScalarToken, ScalarValue};
use std::fmt::{Display, Formatter};
const CURSOR_SEGMENT_DELIMITER: &str = "||";
pub trait Cursor {
type CursorType;
fn to_raw_string(&self) -> String;
fn new(raw: &str, parts: Vec<&str>) -> Result<Self::CursorType, CursorError>;
fn from_encoded_string(input: &str) -> Result<Self::CursorType, CursorError> {
let decoded = BASE64_URL_SAFE.decode(input)?;
let decoded_string = String::from_utf8(decoded)?;
Self::new(
decoded_string.as_str(),
decoded_string.split(CURSOR_SEGMENT_DELIMITER).collect(),
)
}
fn to_encoded_string(&self) -> String {
BASE64_URL_SAFE.encode(self.to_raw_string().as_bytes())
}
fn to_output(&self) -> String {
self.to_encoded_string()
}
fn from_input(input: &str) -> Result<Self::CursorType, Box<str>> {
let res = Self::from_encoded_string(input);
match res {
Ok(cursor) => Ok(cursor),
Err(err) => Err(err.to_string().into_boxed_str()),
}
}
fn parse_token<S: ScalarValue>(value: ScalarToken<'_>) -> ParseScalarResult<S> {
<String as ParseScalarValue<S>>::from_str(value)
}
}
pub fn cursor_from_encoded_string<T>(input: &str) -> Result<T, CursorError>
where
T: Cursor<CursorType = T>,
{
let cursor = T::from_encoded_string(input)?;
Ok(cursor)
}
#[derive(Debug, GraphQLScalar, Default, Clone)]
#[graphql(
name = "OffsetCursor",
to_output_with = Self::to_output,
from_input_with = Self::from_input
)]
pub struct OffsetCursor {
pub offset: i32,
pub first: Option<i32>,
}
impl OffsetCursor {
pub fn new(offset: i32, first: Option<i32>) -> Self {
OffsetCursor { offset, first }
}
}
impl Cursor for OffsetCursor {
type CursorType = OffsetCursor;
fn to_raw_string(&self) -> String {
if let Some(first) = self.first {
format!(
"offset{}{}{}{}",
CURSOR_SEGMENT_DELIMITER, self.offset, CURSOR_SEGMENT_DELIMITER, first
)
} else {
format!("offset{}{}", CURSOR_SEGMENT_DELIMITER, self.offset)
}
}
fn new(_raw: &str, parts: Vec<&str>) -> Result<OffsetCursor, CursorError> {
if parts.len() != 2 && parts.len() != 3 {
return Err(CursorError::InvalidCursor);
}
let offset = parts[1].parse::<i32>().unwrap_or(0);
let first: Option<i32> = if parts.len() == 2 {
None
} else {
parts[2].parse::<i32>().ok()
};
Ok(OffsetCursor { offset, first })
}
}
impl Display for OffsetCursor {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_raw_string())
}
}
#[derive(Debug, GraphQLScalar, Clone)]
pub struct StringCursor {
pub value: String,
}
impl StringCursor {
pub fn new(value: String) -> Self {
StringCursor { value }
}
}
impl Cursor for StringCursor {
type CursorType = StringCursor;
fn to_raw_string(&self) -> String {
format!("string{}{}", CURSOR_SEGMENT_DELIMITER, self.value.clone())
}
fn new(_raw: &str, parts: Vec<&str>) -> Result<Self::CursorType, CursorError> {
let raw_parts_value = parts[1].to_string();
Ok(StringCursor {
value: raw_parts_value,
})
}
}
impl Display for StringCursor {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_raw_string())
}
}
impl Default for StringCursor {
fn default() -> Self {
StringCursor {
value: "".to_string(),
}
}
}
#[cfg(test)]
mod tests {
mod offset_cursor_tests {
use crate::{Cursor, OffsetCursor};
#[test]
fn test_new_offset_first() {
let cursor = OffsetCursor::new(1, Some(10));
assert_eq!(cursor.offset, 1);
assert_eq!(cursor.first, Some(10));
}
#[test]
fn test_offset_cursor_default() {
let cursor = OffsetCursor::default();
assert_eq!(cursor.offset, 0);
assert_eq!(cursor.first, None);
}
#[test]
fn test_offset_cursor_raw_string() {
let cursor = OffsetCursor {
offset: 1,
first: Some(10),
};
assert_eq!(cursor.to_string(), "offset||1||10");
}
#[test]
fn test_offset_cursor_encoded_string() {
let cursor = OffsetCursor {
offset: 1,
first: Some(10),
};
assert_eq!(cursor.to_encoded_string(), "b2Zmc2V0fHwxfHwxMA==");
}
#[test]
fn test_offset_cursor_from_encoded_string() {
let cursor = OffsetCursor::from_encoded_string("b2Zmc2V0fHwxfHwxMA==").unwrap();
assert_eq!(cursor.offset, 1);
assert_eq!(cursor.first, Some(10));
}
}
mod string_cursor_tests {
use crate::{Cursor, StringCursor};
#[test]
fn test_string_cursor_raw_string() {
let cursor = StringCursor {
value: "some-cursor".to_string(),
};
assert_eq!(cursor.to_string(), "string||some-cursor");
}
#[test]
fn test_string_cursor_encoded_string() {
let cursor = StringCursor {
value: "some-cursor".to_string(),
};
assert_eq!(cursor.to_encoded_string(), "c3RyaW5nfHxzb21lLWN1cnNvcg==");
}
#[test]
fn test_string_cursor_from_encoded_string() {
let cursor = StringCursor::from_encoded_string("c3RyaW5nfHxzb21lLWN1cnNvcg==").unwrap();
assert_eq!(cursor.value, "some-cursor");
}
}
}