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}