1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
//! Common [Cursor] utilities
//!
//! This module provides the [Cursor] type which can aid defining a cursor for GraphQL. The [Cursor]
//! consists of a prefix combined with custom data. The custom data must implement [ToString] and
//! [FromStr].

use std::str::FromStr;
use std::string::FromUtf8Error;
use thiserror::Error;

/// Possible errors from [Cursor::decode_cursor]
#[derive(Error, Debug, PartialEq, Eq)]
pub enum CursorError {
    /// Returned when the identifier part of the [Cursor] is invalid.
    #[error("invalid identifier")]
    InvalidIdentifier,
    #[error("missing data")]
    MissingData,
    #[error("invalid data")]
    InvalidData,
    #[error("decode error")]
    DecodeError(#[from] base64::DecodeError),
    #[error("utf8 error")]
    FromUtf8Error(#[from] FromUtf8Error),
}

/// A cursor implementation for use with GraphQL, implements [async_graphql::connection::CursorType].
///
/// The [Cursor] consists of a prefix and a ID part. The prefix specifies what kind of object it
/// refers to and the ID specifies which specific object it refers to.
///
/// ```
/// # use async_graphql::connection::CursorType;
/// # use crate::thfmr_util::graphql::Cursor;
/// let cursor = Cursor::new("Album", 1);
///
/// assert_eq!(cursor.encode_cursor(), "QWxidW06MQ=="); // Album:1
/// ```
#[derive(Clone)]
pub struct Cursor<T: FromStr + ToString> {
    prefix: String,
    value: T,
}

impl<T> Cursor<T>
where
    T: FromStr + ToString + Clone,
{
    /// Create a new [Cursor] with the given prefix and value.
    ///
    /// This can be used by server implementations to create the appropriate cursor.
    pub fn new(prefix: &str, value: T) -> Cursor<T> {
        Cursor {
            prefix: prefix.to_string(),
            value: value.clone(),
        }
    }

    /// Deconstruct this cursor into the appropriate ID when its prefix matches.
    ///
    /// This function can be used by the server to extract the contained ID. It returns
    /// Error when the cursor does not have the specified prefix.
    ///
    /// ```
    /// # use crate::thfmr_util::graphql::Cursor;
    /// # use crate::thfmr_util::graphql::CursorError;
    ///
    /// let cursor = Cursor::new("MyPrefix", 10);
    ///
    /// # let clone_cursor = cursor.clone();
    /// # let cursor = clone_cursor.clone();
    /// assert_eq!(cursor.into_prefix("MyPrefix"), Ok(10));
    /// # let cursor = clone_cursor.clone();
    /// assert_eq!(cursor.into_prefix("OtherPrefix"), Err(CursorError::InvalidIdentifier));
    /// ```
    pub fn into_prefix(self, prefix: &str) -> Result<T, CursorError> {
        if self.prefix != prefix {
            Err(CursorError::InvalidIdentifier)
        } else {
            Ok(self.value)
        }
    }
}

/// Implementation for async_graphql cursors
impl<T: FromStr + ToString> async_graphql::connection::CursorType for Cursor<T> {
    type Error = CursorError;

    /// Decode the GraphQL input string into a [Cursor]
    fn decode_cursor(s: &str) -> Result<Self, Self::Error> {
        let cursor = String::from_utf8(base64::decode(s)?)?;
        let mut parts = cursor.split(':');

        Ok(Cursor {
            prefix: parts
                .next()
                .ok_or_else(|| CursorError::InvalidIdentifier)?
                .to_string(),
            value: parts
                .next()
                .ok_or_else(|| CursorError::MissingData)?
                .parse()
                .map_err(|_| CursorError::InvalidData)?,
        })
    }

    /// Encode the [Cursor] to a string for use in the GraphQL answer
    fn encode_cursor(&self) -> String {
        base64::encode(format!("{}:{}", self.prefix, self.value.to_string()))
    }
}