Skip to main content

asimov_id/
handle.rs

1// This is free and unencumbered software released into the public domain.
2
3//! ASIMOV handle.
4
5use crate::HandleError;
6use alloc::{format, string::String};
7use core::{borrow::Borrow, ops::RangeInclusive, str::FromStr};
8use derive_more::Display;
9
10pub const HANDLE_PREFIX: &str = "Ⓐ";
11
12pub const HANDLE_LEN_MIN: usize = 3;
13pub const HANDLE_LEN_MAX: usize = 31; // should be < PUBLIC_KEY_LEN_MIN
14pub const HANDLE_LEN: RangeInclusive<usize> = HANDLE_LEN_MIN..=HANDLE_LEN_MAX;
15
16#[derive(Clone, Debug, Display, Eq, Hash, Ord, PartialEq, PartialOrd)]
17#[display("{}", self.0)]
18#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
19#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
20pub struct Handle(pub(crate) String);
21
22impl Handle {
23    pub fn validate(input: &str) -> Result<(), HandleError> {
24        if input.is_empty() {
25            return Err(HandleError::EmptyInput);
26        }
27
28        if input.starts_with('-') {
29            return Err(HandleError::InvalidFirstChar('-'));
30        }
31
32        input
33            .chars()
34            .find(|c| !matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-'))
35            .map_or(Ok(()), |c| Err(HandleError::InvalidChar(c)))?;
36
37        if input.len() < HANDLE_LEN_MIN || input.len() > HANDLE_LEN_MAX {
38            return Err(HandleError::InvalidLength(input.len()));
39        }
40
41        Ok(())
42    }
43
44    pub fn as_bytes(&self) -> &[u8] {
45        &self.0.as_bytes()
46    }
47
48    pub fn as_str(&self) -> &str {
49        &self.0
50    }
51
52    pub fn as_string(&self) -> &String {
53        &self.0
54    }
55
56    pub fn into_string(self) -> String {
57        self.0
58    }
59
60    pub fn glyph(&self) -> &str {
61        "Ⓐ"
62    }
63
64    pub fn to_string_with_glyph(&self) -> String {
65        format!("Ⓐ{}", self)
66    }
67
68    pub fn to_uri(&self) -> String {
69        format!("https://asimov.social/{}", self.0)
70    }
71}
72
73impl FromStr for Handle {
74    type Err = HandleError;
75
76    fn from_str(input: &str) -> Result<Self, Self::Err> {
77        Self::validate(input)?;
78        Ok(Self(input.into()))
79    }
80}
81
82impl TryFrom<String> for Handle {
83    type Error = HandleError;
84
85    fn try_from(input: String) -> Result<Self, Self::Error> {
86        Self::from_str(&input)
87    }
88}
89
90impl AsRef<[u8]> for Handle {
91    fn as_ref(&self) -> &[u8] {
92        self.as_bytes()
93    }
94}
95
96impl Borrow<str> for Handle {
97    fn borrow(&self) -> &str {
98        &self.0
99    }
100}
101
102impl Into<String> for Handle {
103    fn into(self) -> String {
104        self.into_string()
105    }
106}
107
108#[cfg(feature = "eloquent")]
109impl eloquent::ToSql for Handle {
110    fn to_sql(&self) -> Result<String, eloquent::error::EloquentError> {
111        use alloc::string::ToString;
112        Ok(self.to_string())
113    }
114}
115
116#[cfg(feature = "libsql")]
117impl libsql::params::IntoValue for Handle {
118    fn into_value(self) -> libsql::Result<libsql::Value> {
119        Ok(libsql::Value::Text(self.into_string()))
120    }
121}
122
123#[cfg(feature = "rocket")]
124impl<'r> rocket::request::FromParam<'r> for Handle {
125    type Error = HandleError;
126
127    fn from_param(input: &'r str) -> Result<Self, Self::Error> {
128        Self::from_str(input)
129    }
130}
131
132#[cfg(feature = "turso")]
133impl turso::IntoValue for Handle {
134    fn into_value(self) -> turso::Result<turso::Value> {
135        Ok(turso::Value::Text(self.into_string()))
136    }
137}