human_friendly_ids/
id.rs

1// src/id.rs
2//! Core ID type and associated operations
3
4use std::{fmt, str::FromStr};
5
6use rand::Rng;
7
8use crate::{
9    alphabet::{self, CHECK_ALPHABET},
10    error::IdError,
11};
12
13/// A user-friendly identifier with check bit validation
14///
15/// # Example
16/// ```no_run
17/// use human_friendly_ids::Id;
18/// use std::str::FromStr;
19///
20/// let id = Id::from_str("abc-").unwrap();
21/// assert_eq!(id.as_str(), "abc-");
22/// ```
23#[derive(Debug, Clone, PartialEq, Eq, Hash)]
24pub struct Id(pub(crate) String);
25
26impl Id {
27    /// Get string slice representation
28    #[must_use]
29    pub fn as_str(&self) -> &str {
30        &self.0
31    }
32
33    /// Calculate maximum valid ID length for current configuration
34    #[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    /// Generate a new ID with a given length
51    ///
52    /// See: [`Id::new`] if you want to use the default RNG.
53    ///
54    #[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            // Avoid ambiguous sequences
68            match (last_char, c) {
69                (Some('r'), 'n') | (Some('v'), 'v') => {}
70                // Don't end with 'r' or 'v', because the check-bit could create an ambiguous sequence
71                (_, '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    /// Generate a new ID with a given length
86    ///
87    /// This method uses the default RNG from the `rand` crate.
88    #[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")]
167/// This module provides custom implementations for the `Serialize` and `Deserialize` traits
168/// for the `UploadId` type. These implementations allow `UploadId` to be serialized as a string
169/// and deserialized from a string using Serde.
170///
171/// # Examples
172///
173/// ```
174/// use serde::{Serialize, Deserialize};
175/// use human_friendly_ids::Id;
176///
177/// #[derive(Serialize, Deserialize)]
178/// struct MyStruct {
179///     id: Id,
180/// }
181/// ```
182mod 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}