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())) } }