1use std::{fmt, str::FromStr};
5
6use rand::Rng;
7
8use crate::{
9 alphabet::{self, CHECK_ALPHABET},
10 error::IdError,
11};
12
13#[derive(Debug, Clone, PartialEq, Eq, Hash)]
24pub struct Id(pub(crate) String);
25
26impl Id {
27 #[must_use]
29 pub fn as_str(&self) -> &str {
30 &self.0
31 }
32
33 #[allow(
35 clippy::arithmetic_side_effects,
36 clippy::cast_possible_truncation,
37 reason = "assert macro"
38 )]
39 #[must_use]
40 pub const fn max_length() -> usize {
41 const _: () = assert!(
42 CHECK_ALPHABET.len() > 2,
43 "CHECK_ALPHABET length must be greater than 2"
44 );
45 const _: () = assert!(std::mem::size_of::<usize>() == 8,);
46 let max_value = u64::MAX / (CHECK_ALPHABET.len() - 1) as u64;
47 (max_value + 1) as usize
48 }
49
50 #[allow(
55 clippy::missing_panics_doc,
56 reason = "Internal invariant - won't generate a string that would panic."
57 )]
58 #[must_use]
59 pub fn new_with_rng<R: Rng>(len: usize, rng: &mut R) -> Self {
60 let mut body = String::with_capacity(len.saturating_sub(1));
61 let mut last_char = None;
62
63 while body.len() < len.saturating_sub(1) {
64 let idx = rng.random_range(0..alphabet::GEN_ALPHABET.len());
65 #[allow(clippy::indexing_slicing, reason = "index is generated within bounds")]
66 let c = alphabet::GEN_ALPHABET[idx];
67 match (last_char, c) {
69 (Some('r'), 'n') | (Some('v'), 'v') => {}
70 (_, 'r' | 'v') if body.len() == len.saturating_sub(2) => {}
72 _ => {
73 body.push(c);
74 last_char = Some(c);
75 }
76 }
77 }
78
79 let check_char = alphabet::calculate_check_char(&body)
80 .expect("Generated body should be valid for check calculation");
81
82 Id(format!("{}{}", body, check_char))
83 }
84
85 #[must_use]
89 pub fn new(len: usize) -> Self {
90 let mut rng = rand::rng();
91 Self::new_with_rng(len, &mut rng)
92 }
93}
94
95#[cfg_attr(test, mutants::skip)]
96impl AsRef<str> for Id {
97 fn as_ref(&self) -> &str {
98 self.as_str()
99 }
100}
101
102#[cfg_attr(test, mutants::skip)]
103impl std::ops::Deref for Id {
104 type Target = str;
105
106 fn deref(&self) -> &Self::Target {
107 self.as_str()
108 }
109}
110
111#[cfg_attr(test, mutants::skip)]
112impl From<Id> for String {
113 fn from(id: Id) -> Self {
114 id.0
115 }
116}
117
118#[cfg_attr(test, mutants::skip)]
119impl From<Id> for Box<str> {
120 fn from(id: Id) -> Self {
121 id.0.into_boxed_str()
122 }
123}
124
125impl FromStr for Id {
126 type Err = IdError;
127
128 fn from_str(s: &str) -> Result<Self, Self::Err> {
129 let normalized = alphabet::normalize_string(s);
130
131 if normalized.len() <= 3 {
132 return Err(IdError::TooShort);
133 }
134
135 let (body, check_char) = normalized
136 .split_at_checked(normalized.len().checked_sub(1).expect("checked above"))
137 .ok_or(IdError::InvalidCharacter)?;
138 let expected_check = alphabet::calculate_check_char(body)?;
139
140 if check_char != expected_check.to_string() {
141 return Err(IdError::InvalidCheckBit);
142 }
143
144 for c in body.chars() {
145 alphabet::validate_char(c)?;
146 }
147
148 Ok(Self(normalized))
149 }
150}
151
152impl TryFrom<String> for Id {
153 type Error = IdError;
154
155 fn try_from(value: String) -> Result<Self, Self::Error> {
156 Self::from_str(&value)
157 }
158}
159
160impl fmt::Display for Id {
161 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162 write!(f, "{}", self.0)
163 }
164}
165
166#[cfg(feature = "serde")]
167mod serde_impl {
183 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
184
185 use super::Id;
186
187 impl Serialize for Id {
188 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
189 where
190 S: Serializer,
191 {
192 serializer.serialize_str(self.as_str())
193 }
194 }
195
196 impl<'de> Deserialize<'de> for Id {
197 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
198 where
199 D: Deserializer<'de>,
200 {
201 let s = String::deserialize(deserializer)?;
202 s.parse().map_err(D::Error::custom)
203 }
204 }
205
206 #[cfg(test)]
207 mod tests {
208 use super::*;
209
210 #[test]
211 fn test_serde_roundtrip() {
212 let id = Id::try_from("wcfytxww4opin4jmjjes4ccfd".to_string())
213 .expect("Failed to decode UploadId");
214 let serialized = serde_json::to_string(&id).expect("Failed to serialize UploadId");
215
216 insta::assert_json_snapshot!(serialized);
217
218 let deserialized: Id =
219 serde_json::from_str(&serialized).expect("Failed to deserialize UploadId");
220 assert_eq!(id, deserialized);
221
222 insta::assert_debug_snapshot!(deserialized);
223 }
224 }
225}