1use core::fmt;
85
86#[derive(Clone, Copy, Debug, Eq, PartialEq)]
88#[non_exhaustive]
89pub enum ParseHandleError {
90 TooShort { min: usize },
92 TooLong { max: usize },
94 InvalidCharacter(char),
96 InvalidFormat,
98 InvalidPercentEncoding,
100 InvalidUtf8,
102}
103
104impl fmt::Display for ParseHandleError {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 match self {
107 Self::TooShort { min } => write!(f, "handle must contain at least {min} characters"),
108 Self::TooLong { max } => write!(f, "handle must contain at most {max} characters"),
109 Self::InvalidCharacter(c) => write!(f, "invalid character in handle: {c:?}"),
110 Self::InvalidFormat => f.write_str("invalid handle format"),
111 Self::InvalidPercentEncoding => f.write_str("invalid percent escape in handle"),
112 Self::InvalidUtf8 => f.write_str("percent-decoded handle is not UTF-8"),
113 }
114 }
115}
116
117impl core::error::Error for ParseHandleError {}
118
119pub fn validate_length(input: &str, min: usize, max: usize) -> Result<(), ParseHandleError> {
121 let length = input.chars().take(max.saturating_add(1)).count();
122 if length < min {
123 Err(ParseHandleError::TooShort { min })
124 } else if length > max {
125 Err(ParseHandleError::TooLong { max })
126 } else {
127 Ok(())
128 }
129}
130
131pub fn validate_ascii(input: &str, punctuation: &str) -> Result<(), ParseHandleError> {
133 match input
134 .chars()
135 .find(|c| !c.is_ascii_alphanumeric() && !punctuation.contains(*c))
136 {
137 Some(c) => Err(ParseHandleError::InvalidCharacter(c)),
138 None => Ok(()),
139 }
140}
141
142#[doc(hidden)]
145#[macro_export]
146macro_rules! impl_handle {
147 ($handle:ident, $min:expr, $max:expr, $scalar_name:literal) => {
148 impl $handle {
149 pub const MIN_LENGTH: usize = $min;
151 pub const MAX_LENGTH: usize = $max;
156
157 pub fn as_str(&self) -> &str {
159 &self.0
160 }
161
162 pub fn into_string(self) -> alloc::string::String {
164 self.0
165 }
166 }
167
168 impl AsRef<str> for $handle {
169 fn as_ref(&self) -> &str {
170 self.as_str()
171 }
172 }
173
174 impl TryFrom<&str> for $handle {
175 type Error = $crate::handle::ParseHandleError;
176
177 fn try_from(input: &str) -> Result<Self, Self::Error> {
178 input.parse()
179 }
180 }
181
182 impl TryFrom<alloc::string::String> for $handle {
183 type Error = $crate::handle::ParseHandleError;
184
185 fn try_from(input: alloc::string::String) -> Result<Self, Self::Error> {
186 input.parse()
187 }
188 }
189
190 #[cfg(feature = "serde")]
191 impl serde::Serialize for $handle {
192 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
193 serializer.serialize_str(self.as_str())
194 }
195 }
196
197 #[cfg(feature = "serde")]
198 impl<'de> serde::Deserialize<'de> for $handle {
199 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
200 let input = <alloc::string::String as serde::Deserialize>::deserialize(deserializer)?;
201 input.parse().map_err(serde::de::Error::custom)
202 }
203 }
204
205 #[cfg(feature = "async-graphql")]
206 #[async_graphql::Scalar(name = $scalar_name)]
207 impl async_graphql::ScalarType for $handle {
208 fn parse(value: async_graphql::Value) -> async_graphql::InputValueResult<Self> {
209 match value {
210 async_graphql::Value::String(input) => input
211 .parse()
212 .map_err(async_graphql::InputValueError::custom),
213 value => Err(async_graphql::InputValueError::expected_type(value)),
214 }
215 }
216
217 fn is_valid(value: &async_graphql::Value) -> bool {
218 matches!(value, async_graphql::Value::String(input) if input.parse::<Self>().is_ok())
219 }
220
221 fn to_value(&self) -> async_graphql::Value {
222 async_graphql::Value::String(self.0.clone())
223 }
224 }
225
226 #[cfg(feature = "async-graphql")]
227 impl async_graphql::connection::CursorType for $handle {
228 type Error = $crate::handle::ParseHandleError;
229
230 fn decode_cursor(input: &str) -> Result<Self, Self::Error> {
231 input.parse()
232 }
233
234 fn encode_cursor(&self) -> alloc::string::String {
235 self.0.clone()
236 }
237 }
238
239 #[cfg(feature = "sqlx")]
240 impl<DB: sqlx::Database> sqlx::Type<DB> for $handle
241 where
242 alloc::string::String: sqlx::Type<DB>,
243 {
244 fn type_info() -> DB::TypeInfo {
245 <alloc::string::String as sqlx::Type<DB>>::type_info()
246 }
247
248 fn compatible(ty: &DB::TypeInfo) -> bool {
249 <alloc::string::String as sqlx::Type<DB>>::compatible(ty)
250 }
251 }
252
253 #[cfg(feature = "sqlx-postgres")]
254 impl sqlx::postgres::PgHasArrayType for $handle {
255 fn array_type_info() -> sqlx::postgres::PgTypeInfo {
256 <alloc::string::String as sqlx::postgres::PgHasArrayType>::array_type_info()
257 }
258
259 fn array_compatible(ty: &sqlx::postgres::PgTypeInfo) -> bool {
260 <alloc::string::String as sqlx::postgres::PgHasArrayType>::array_compatible(ty)
261 }
262 }
263
264 #[cfg(feature = "sqlx")]
265 impl<'q, DB: sqlx::Database> sqlx::Encode<'q, DB> for $handle
266 where
267 alloc::string::String: sqlx::Encode<'q, DB>,
268 {
269 fn encode_by_ref(
270 &self,
271 buf: &mut DB::ArgumentBuffer,
272 ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
273 <alloc::string::String as sqlx::Encode<DB>>::encode_by_ref(&self.0, buf)
274 }
275
276 fn produces(&self) -> Option<DB::TypeInfo> {
277 <alloc::string::String as sqlx::Encode<DB>>::produces(&self.0)
278 }
279
280 fn size_hint(&self) -> usize {
281 <alloc::string::String as sqlx::Encode<DB>>::size_hint(&self.0)
282 }
283 }
284
285 #[cfg(feature = "sqlx")]
286 impl<'r, DB: sqlx::Database> sqlx::Decode<'r, DB> for $handle
287 where
288 alloc::string::String: sqlx::Decode<'r, DB>,
289 {
290 fn decode(value: DB::ValueRef<'r>) -> Result<Self, sqlx::error::BoxDynError> {
291 let input = <alloc::string::String as sqlx::Decode<DB>>::decode(value)?;
292 input.parse().map_err(Into::into)
293 }
294 }
295
296 #[cfg(test)]
297 mod handle_tests {
298 use super::$handle;
299 use alloc::string::ToString;
300
301 #[test]
302 fn length_bounds_and_fallible_conversions() {
303 for length in [$handle::MIN_LENGTH, $handle::MAX_LENGTH] {
304 let input = "a".repeat(length);
305 let handle: $handle = input.parse().expect("valid boundary length");
306 assert_eq!(handle.as_str(), input);
307 assert_eq!(handle.to_string(), input);
308 assert_eq!(handle.clone().into_string(), input);
309 assert_eq!($handle::try_from(input.as_str()), Ok(handle.clone()));
310 assert_eq!($handle::try_from(input), Ok(handle));
311 }
312 for length in [0, $handle::MIN_LENGTH - 1, $handle::MAX_LENGTH + 1] {
313 let input = "a".repeat(length);
314 assert!(input.parse::<$handle>().is_err());
315 assert!($handle::try_from(input.as_str()).is_err());
316 assert!($handle::try_from(input).is_err());
317 }
318 }
319
320 #[cfg(feature = "serde")]
321 #[test]
322 fn serde_cannot_bypass_validation() {
323 use serde::{Deserialize, de::value::{Error, StringDeserializer}};
324
325 for input in [alloc::string::String::new(), "a".repeat($handle::MAX_LENGTH + 1)] {
326 assert!($handle::deserialize(StringDeserializer::<Error>::new(input)).is_err());
327 }
328 let decoded = $handle::deserialize(StringDeserializer::<Error>::new("Alice123".into()))
329 .expect("valid handle");
330 assert_eq!(decoded.as_str(), "Alice123".parse::<$handle>().expect("valid handle").as_str());
331 }
332
333 #[cfg(feature = "async-graphql")]
334 #[test]
335 fn graphql_and_cursors_cannot_bypass_validation() {
336 use async_graphql::{InputType, ScalarType, Value, connection::CursorType};
337
338 assert_eq!(<$handle as InputType>::type_name(), stringify!($handle));
339 for input in [alloc::string::String::new(), "a".repeat($handle::MAX_LENGTH + 1)] {
340 let value = Value::String(input.clone());
341 assert!(!<$handle as ScalarType>::is_valid(&value));
342 assert!(<$handle as ScalarType>::parse(value).is_err());
343 assert!($handle::decode_cursor(&input).is_err());
344 }
345 let handle: $handle = "Alice123".parse().expect("valid handle");
346 let decoded = $handle::decode_cursor(&handle.encode_cursor()).expect("valid cursor");
347 assert_eq!(decoded.as_str(), handle.as_str());
348 }
349 }
350 };
351}
352
353#[doc(hidden)]
356#[macro_export]
357macro_rules! impl_handle_comparison {
358 ($handle:ident) => {
359 impl PartialEq for $handle {
360 fn eq(&self, other: &Self) -> bool {
361 self.comparison_key() == other.comparison_key()
362 }
363 }
364
365 impl PartialOrd for $handle {
366 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
367 Some(self.cmp(other))
368 }
369 }
370
371 impl Ord for $handle {
372 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
373 self.comparison_key().cmp(&other.comparison_key())
374 }
375 }
376
377 impl core::hash::Hash for $handle {
378 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
379 core::hash::Hash::hash(&self.comparison_key(), state);
380 }
381 }
382
383 #[cfg(test)]
384 mod comparison_tests {
385 extern crate std;
386
387 use super::$handle;
388 use std::collections::{BTreeSet, HashSet};
389
390 #[test]
391 fn case_preserving_identity_in_collections() {
392 let upper: $handle = "Alice123".parse().expect("valid handle");
393 let lower: $handle = "alice123".parse().expect("valid handle");
394 assert_eq!(upper.as_str(), "Alice123");
395 assert_eq!(lower.as_str(), "alice123");
396 assert_eq!(upper, lower);
397 assert_eq!(upper.cmp(&lower), core::cmp::Ordering::Equal);
398 assert_eq!(upper.partial_cmp(&lower), Some(core::cmp::Ordering::Equal));
399 assert_eq!(HashSet::from([upper.clone(), lower.clone()]).len(), 1);
400 assert_eq!(BTreeSet::from([upper, lower]).len(), 1);
401 }
402 }
403 };
404}