thfmr_util/graphql/
cursor.rs

1//! Common [Cursor] utilities
2//!
3//! This module provides the [Cursor] type which can aid defining a cursor for GraphQL. The [Cursor]
4//! consists of a prefix combined with custom data. The custom data must implement [ToString] and
5//! [FromStr].
6
7use std::str::FromStr;
8use std::string::FromUtf8Error;
9use thiserror::Error;
10
11/// Possible errors from [Cursor::decode_cursor]
12#[derive(Error, Debug, PartialEq, Eq)]
13pub enum CursorError {
14    /// Returned when the identifier part of the [Cursor] is invalid.
15    #[error("invalid identifier")]
16    InvalidIdentifier,
17    #[error("missing data")]
18    MissingData,
19    #[error("invalid data")]
20    InvalidData,
21    #[error("decode error")]
22    DecodeError(#[from] base64::DecodeError),
23    #[error("utf8 error")]
24    FromUtf8Error(#[from] FromUtf8Error),
25}
26
27/// A cursor implementation for use with GraphQL, implements [async_graphql::connection::CursorType].
28///
29/// The [Cursor] consists of a prefix and a ID part. The prefix specifies what kind of object it
30/// refers to and the ID specifies which specific object it refers to.
31///
32/// ```
33/// # use async_graphql::connection::CursorType;
34/// # use crate::thfmr_util::graphql::Cursor;
35/// let cursor = Cursor::new("Album", 1);
36///
37/// assert_eq!(cursor.encode_cursor(), "QWxidW06MQ=="); // Album:1
38/// ```
39#[derive(Clone)]
40pub struct Cursor<T: FromStr + ToString> {
41    prefix: String,
42    value: T,
43}
44
45impl<T> Cursor<T>
46where
47    T: FromStr + ToString + Clone,
48{
49    /// Create a new [Cursor] with the given prefix and value.
50    ///
51    /// This can be used by server implementations to create the appropriate cursor.
52    pub fn new(prefix: &str, value: T) -> Cursor<T> {
53        Cursor {
54            prefix: prefix.to_string(),
55            value: value.clone(),
56        }
57    }
58
59    /// Deconstruct this cursor into the appropriate ID when its prefix matches.
60    ///
61    /// This function can be used by the server to extract the contained ID. It returns
62    /// Error when the cursor does not have the specified prefix.
63    ///
64    /// ```
65    /// # use crate::thfmr_util::graphql::Cursor;
66    /// # use crate::thfmr_util::graphql::CursorError;
67    ///
68    /// let cursor = Cursor::new("MyPrefix", 10);
69    ///
70    /// # let clone_cursor = cursor.clone();
71    /// # let cursor = clone_cursor.clone();
72    /// assert_eq!(cursor.into_prefix("MyPrefix"), Ok(10));
73    /// # let cursor = clone_cursor.clone();
74    /// assert_eq!(cursor.into_prefix("OtherPrefix"), Err(CursorError::InvalidIdentifier));
75    /// ```
76    pub fn into_prefix(self, prefix: &str) -> Result<T, CursorError> {
77        if self.prefix != prefix {
78            Err(CursorError::InvalidIdentifier)
79        } else {
80            Ok(self.value)
81        }
82    }
83}
84
85/// Implementation for async_graphql cursors
86impl<T: FromStr + ToString> async_graphql::connection::CursorType for Cursor<T> {
87    type Error = CursorError;
88
89    /// Decode the GraphQL input string into a [Cursor]
90    fn decode_cursor(s: &str) -> Result<Self, Self::Error> {
91        let cursor = String::from_utf8(base64::decode(s)?)?;
92        let mut parts = cursor.split(':');
93
94        Ok(Cursor {
95            prefix: parts
96                .next()
97                .ok_or_else(|| CursorError::InvalidIdentifier)?
98                .to_string(),
99            value: parts
100                .next()
101                .ok_or_else(|| CursorError::MissingData)?
102                .parse()
103                .map_err(|_| CursorError::InvalidData)?,
104        })
105    }
106
107    /// Encode the [Cursor] to a string for use in the GraphQL answer
108    fn encode_cursor(&self) -> String {
109        base64::encode(format!("{}:{}", self.prefix, self.value.to_string()))
110    }
111}