short_uuid/
lib.rs

1// THE MIT License (MIT)
2//
3// Copyright (c) 2024 Radim Höfer
4// See the license.txt file in the project root
5
6//! Generate and translate standard UUIDs into shorter or just different formats and back.
7//!
8//! A port of the JavaScript npm package [short-uuid](https://www.npmjs.com/package/short-uuid) so big thanks to the author.
9//!
10//! An example of short uuid string in default flickrBase58 alphabet:
11//!```text
12//! mhvXdrZT4jP5T8vBxuvm75
13//!```
14//!
15//! ## Getting started
16//!
17//! Install the package with `cargo`:
18//!
19//! ```sh
20//! cargo add short-uuid
21//! ```
22//!
23//! or add it to your `Cargo.toml`:
24//!
25//! ```toml
26//! [dependencies]
27//! short-uuid = "0.2.0"
28//! ```
29//! ### Examples
30//!
31//! Generate short uuidv4 encoded in flickrBase58 format:
32//
33//! ```rust
34//! use short_uuid::ShortUuid;
35//!
36//! let shortened_uuid = ShortUuid::generate();
37//! ```
38//!
39//! Generate short uuidv4 encoded in flickrBase58 format using macro:
40//! ```rust
41//! use short_uuid::short;
42//!
43//! let shortened_uuid = short!();
44//! ```
45//!
46//! Generate short uuidv4 using custom alphabet:
47//!
48//! ```rust
49//! use short_uuid::{ShortUuidCustom, CustomTranslator};
50//!
51//! let custom_alphabet = "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";
52//! let translator = CustomTranslator::new(custom_alphabet).unwrap();
53//!
54//! let custom_short = ShortUuidCustom::generate(&translator);
55//! let custom_short_string = custom_short.to_string();
56//! ```
57//!
58//! Get shortened uuid from standard uuid:
59//!
60//! ```rust
61//! use short_uuid::ShortUuid;
62//! // create normal uuid v4
63//! let uuid = uuid::Uuid::new_v4();
64//!
65//! let short = ShortUuid::from_uuid(&uuid);
66//! ```
67//! Get shortened uuid from uuid using custom alphabet:
68//!
69//! ```rust
70//! use short_uuid::{ShortUuidCustom, CustomTranslator};
71//!
72//! let custom_alphabet = "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";
73//! let translator = CustomTranslator::new(custom_alphabet).unwrap();
74//!
75//! let uuid = uuid::Uuid::new_v4();
76//! let short_custom = ShortUuidCustom::from_uuid(&uuid, &translator);
77//! let short_custom_string = short_custom.to_string();
78//! ```
79//!
80//! Get shortened uuid from uuid string:
81//!
82//! ```rust
83//! use short_uuid::ShortUuid;
84//!
85//! let uuid_str = "3cfb46e7-c391-42ef-90b8-0c1d9508e752";
86//! let short_uuid = ShortUuid::from_uuid_str(&uuid_str);
87//! ```
88//!
89//! Get shortened uuid from uuid string using custom alphabet:
90//!
91//! ```rust
92//! use short_uuid::{ShortUuidCustom, CustomTranslator};
93//!
94//! let custom_alphabet = "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";
95//! let translator = CustomTranslator::new(custom_alphabet).unwrap();
96//!
97//! let uuid_str = "3cfb46e7-c391-42ef-90b8-0c1d9508e752";
98//! let short_custom = ShortUuidCustom::from_uuid_str(&uuid_str, &translator).unwrap();
99//! let short_custom_string = short_custom.to_string();
100//! ```
101//!
102//! Serialize and deserialize struct with short uuid (you must enable the `serde` feature):
103//!
104//! ```toml
105//! [dependencies]
106//! short-uuid = { version = "0.2.0", features = ["serde"] }
107//! ```
108//!
109//! Example usage:
110//! ```rust
111//! #[cfg(feature = "serde")]
112//! #[derive(Serialize, Deserialize, PartialEq, Debug)]
113//! struct TestStruct {
114//!     id: ShortUuid,
115//! }
116//!
117//! #[cfg(feature = "serde")]
118//! fn example() {
119//!     let uuid_str = "0408510d-ce4f-4761-ab67-2dfe2931c898";
120//!     let short_id = ShortUuid::from_uuid_str(uuid_str).unwrap();
121//!
122//!     let test_struct = TestStruct {
123//!         id: short_id,
124//!     };
125//!
126//!     let serialized = serde_json::to_string(&test_struct).unwrap();
127//! }
128//! ```
129//!
130//! # References
131//! * [Wikipedia: Universally Unique Identifier](http://en.wikipedia.org/wiki/Universally_unique_identifier)
132//! * [uuid crate](https://crates.io/crates/uuid)
133
134use converter::BaseConverter;
135use error::{CustomAlphabetError, ErrorKind, InvalidShortUuid};
136
137/// Convert between different bases
138pub mod converter;
139mod error;
140mod fmt;
141
142mod macros;
143use uuid;
144
145pub const FLICKR_BASE_58: &str = "123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";
146
147pub const COOKIE_BASE_90: &str =
148    "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&'()*+-./:<=>?@[]^_`{|}~";
149
150type UuidError = uuid::Error;
151
152// pub type Bytes = [u8; 16];
153/// A byte array containing the ShortUuid
154pub type Bytes = Vec<u8>;
155
156/// Shortened UUID
157#[derive(Clone, Debug, Eq, PartialEq, Hash)]
158pub struct ShortUuid(Bytes);
159
160/// Shortened UUID using custom alphabet
161#[derive(Clone, Debug, Eq, PartialEq, Hash)]
162pub struct ShortUuidCustom(Bytes);
163
164/// Custom alphabet used for short uuid
165pub type CustomAlphabet = &'static str;
166
167/// Custom translator used use for base conversion
168pub struct CustomTranslator(BaseConverter);
169
170impl CustomTranslator {
171    /// Create new custom translator
172    pub fn new(custom_alphabet: CustomAlphabet) -> Result<Self, CustomAlphabetError> {
173        let converter = BaseConverter::new_custom(custom_alphabet)?;
174        Ok(Self(converter))
175    }
176
177    fn as_slice(&self) -> &BaseConverter {
178        &self.0
179    }
180}
181
182impl From<ShortUuid> for ShortUuidCustom {
183    fn from(short_uuid: ShortUuid) -> Self {
184        ShortUuidCustom(short_uuid.0)
185    }
186}
187
188// serialize into string
189#[cfg(feature = "serde")]
190impl serde::Serialize for ShortUuid {
191    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
192    where
193        S: serde::Serializer,
194    {
195        let string = String::from_utf8(self.0.clone())
196            .map_err(|e| serde::ser::Error::custom(e.to_string()))?;
197        serializer.serialize_str(&string)
198    }
199}
200
201// deserialize from string
202#[cfg(feature = "serde")]
203impl<'de> serde::Deserialize<'de> for ShortUuid {
204    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
205    where
206        D: serde::Deserializer<'de>,
207    {
208        let string = String::deserialize(deserializer)?;
209        Ok(ShortUuid(string.into_bytes()))
210    }
211}
212
213// serialize into string
214#[cfg(feature = "serde")]
215impl serde::Serialize for ShortUuidCustom {
216    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
217    where
218        S: serde::Serializer,
219    {
220        let string = String::from_utf8(self.0.clone())
221            .map_err(|e| serde::ser::Error::custom(e.to_string()))?;
222        serializer.serialize_str(&string)
223    }
224}
225
226// deserialize from string
227#[cfg(feature = "serde")]
228impl<'de> serde::Deserialize<'de> for ShortUuidCustom {
229    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
230    where
231        D: serde::Deserializer<'de>,
232    {
233        let string = String::deserialize(deserializer)?;
234        Ok(ShortUuidCustom(string.into_bytes()))
235    }
236}
237
238impl ShortUuid {
239    /// Generate a short UUID v5 in flickrBase58
240    pub fn generate() -> ShortUuid {
241        generate_short(None)
242    }
243
244    /// Convert uuid to short format using flickrBase58
245    pub fn from_uuid_str(uuid_string: &str) -> Result<ShortUuid, UuidError> {
246        // validate
247        let parsed = uuid::Uuid::parse_str(uuid_string)?;
248
249        let cleaned = parsed.to_string().to_lowercase().replace("-", "");
250
251        let converter = BaseConverter::default();
252
253        // convert to selected base
254        let result = converter.convert(&cleaned).unwrap();
255
256        Ok(ShortUuid(result))
257    }
258
259    /// Convert uuid to short format using flickrBase58
260    pub fn from_uuid(uuid: &uuid::Uuid) -> ShortUuid {
261        let uuid_string = uuid.to_string();
262
263        let cleaned = uuid_string.to_lowercase().replace("-", "");
264
265        let converter = BaseConverter::default();
266
267        // convert to selected base
268        let result = converter.convert(&cleaned).unwrap();
269
270        ShortUuid(result)
271    }
272
273    /// Convert short to uuid
274    pub fn to_uuid(self) -> uuid::Uuid {
275        // Convert to hex
276        let to_hex_converter = BaseConverter::default();
277
278        // Convert to hex string
279        let result = to_hex_converter.convert_to_hex(&self.0).unwrap();
280
281        // Format hex string as uuid
282        format_uuid(result)
283    }
284
285    /// Convert short to uuid string to ShortUuid
286    pub fn parse_str(short_uuid_str: &str) -> Result<Self, InvalidShortUuid> {
287        let expected_len = 22;
288
289        if short_uuid_str.len() != expected_len {
290            return Err(InvalidShortUuid);
291        };
292
293        let byte_vector: Vec<u8> = short_uuid_str.as_bytes().to_vec();
294
295        let to_hex_converter = BaseConverter::default();
296
297        // Convert to hex string
298        let result = to_hex_converter
299            .convert_to_hex(&byte_vector)
300            .map_err(|_| InvalidShortUuid)?;
301
302        // validate
303        uuid::Uuid::try_parse(&result).map_err(|_| InvalidShortUuid)?;
304
305        Ok(Self(byte_vector))
306    }
307
308    pub fn as_slice(&self) -> &[u8] {
309        &self.0
310    }
311}
312
313impl ShortUuidCustom {
314    /// Generate a short UUID v4 in custom alphabet
315    pub fn generate(translator: &CustomTranslator) -> Self {
316        // Generate a short UUID v4 in custom alphabet
317        let generated = generate_short(Some(&translator.as_slice()));
318        let short_custom: ShortUuidCustom = generated.into();
319
320        short_custom
321    }
322
323    /// Convert uuid to short format using custom alphabet
324    pub fn from_uuid(uuid: &uuid::Uuid, translator: &CustomTranslator) -> Self {
325        let uuid_string = uuid.to_string();
326
327        let cleaned = uuid_string.to_lowercase().replace("-", "");
328
329        // convert to selected base
330        let result = translator.as_slice().convert(&cleaned).unwrap();
331
332        Self(result)
333    }
334
335    /// Convert uuid string to short format using custom alphabet
336    pub fn from_uuid_str(
337        uuid_string: &str,
338        translator: &CustomTranslator,
339    ) -> Result<Self, ErrorKind> {
340        // validate
341        let parsed = uuid::Uuid::parse_str(uuid_string).map_err(|e| ErrorKind::UuidError(e))?;
342
343        let cleaned = parsed.to_string().to_lowercase().replace("-", "");
344
345        // convert to selected base
346        let result = translator.as_slice().convert(&cleaned).unwrap();
347
348        Ok(Self(result))
349    }
350
351    /// Convert short to uuid using custom base
352    pub fn to_uuid(self, translator: &CustomTranslator) -> Result<uuid::Uuid, CustomAlphabetError> {
353        // Convert to hex string
354        // Should not fail
355        let result = translator
356            .as_slice()
357            .convert_to_hex(&self.as_slice())
358            .unwrap();
359
360        // Format hex string as uuid
361        let uuid_value = format_uuid(result);
362
363        Ok(uuid_value)
364    }
365
366    /// Validate that short uuid str is valid uuid using custom alphabet
367    pub fn parse_str(
368        short_uuid_str: &str,
369        translator: &CustomTranslator,
370    ) -> Result<Self, InvalidShortUuid> {
371        let byte_vector: Vec<u8> = short_uuid_str.as_bytes().to_vec();
372
373        let result_string = translator
374            .as_slice()
375            .convert_to_hex(&byte_vector)
376            .map_err(|_| InvalidShortUuid)?;
377
378        // validate
379        uuid::Uuid::try_parse(&result_string).map_err(|_| InvalidShortUuid)?;
380
381        Ok(Self(byte_vector))
382    }
383
384    pub fn as_slice(&self) -> &[u8] {
385        &self.0
386    }
387}
388
389fn generate_short(base_converter: Option<&BaseConverter>) -> ShortUuid {
390    // Generate UUID v4
391    let uuid_string = uuid::Uuid::new_v4().to_string();
392
393    // clean string
394    let cleaned = uuid_string.to_lowercase().replace("-", "");
395
396    // convert to selected base
397    let result = base_converter
398        .unwrap_or(&BaseConverter::default())
399        .convert(&cleaned)
400        .unwrap();
401
402    ShortUuid(result)
403}
404
405fn format_uuid(value: String) -> uuid::Uuid {
406    let formatted_uuid = format!(
407        "{}-{}-{}-{}-{}",
408        &value[0..8],
409        &value[8..12],
410        &value[12..16],
411        &value[16..20],
412        &value[20..32]
413    );
414
415    // Should not fail
416    let uuid = uuid::Uuid::parse_str(&formatted_uuid).unwrap();
417
418    return uuid;
419}
420
421impl From<uuid::Uuid> for ShortUuid {
422    fn from(uuid: uuid::Uuid) -> ShortUuid {
423        ShortUuid::from_uuid(&uuid)
424    }
425}
426
427impl From<ShortUuid> for uuid::Uuid {
428    fn from(short_uuid: ShortUuid) -> uuid::Uuid {
429        short_uuid.to_uuid()
430    }
431}