diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f65f30..525b80d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG +**v0.3.0:** **⚠️ BREAKING CHANGE** +- **Breaking change**: IDs change. Algorithm has been fine-tuned for better performance [[Issue #11](https://github.com/sqids/sqids-spec/issues/11)] +- `alphabet` cannot contain multibyte characters +- `min_length` was changed from `usize` to `u8` +- Max blocklist re-encoding attempts has been capped at the length of the alphabet - 1 +- Minimum alphabet length has changed from 5 to 3 +- `min_value()` and `max_value()` functions have been removed + **v0.2.1:** - Bug fix: spec update (PR #7): blocklist filtering in uppercase-only alphabet [[PR #7](https://github.com/sqids/sqids-spec/pull/7)] - Updating Github Actions to use stable toolchain instead of nightly diff --git a/Cargo.toml b/Cargo.toml index fb23d9b..a8f90da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,13 +4,13 @@ description = "Generate YouTube-like ids from numbers." repository = "https://github.com/sqids/sqids-rust" documentation = "https://docs.rs/sqids" homepage = "https://sqids.org/rust" -version = "0.2.1" +version = "0.3.0" license = "MIT" edition = "2021" readme = "README.md" -keywords = ["ids", "encode", "sqids", "hashids"] +keywords = ["ids", "encode", "short", "sqids", "hashids"] [dependencies] derive_more = "0.99.17" serde = "1.0.188" -serde_json = "1.0.105" +serde_json = "1.0.106" diff --git a/README.md b/README.md index 26feea8..3471b9e 100644 --- a/README.md +++ b/README.md @@ -44,34 +44,34 @@ Simple encode & decode: ```rust let sqids = Sqids::default(); -let id = sqids.encode(&[1, 2, 3])?; // "8QRLaD" +let id = sqids.encode(&[1, 2, 3])?; // "86Rf07" let numbers = sqids.decode(&id); // [1, 2, 3] ``` > **Note** > 🚧 Because of the algorithm's design, **multiple IDs can decode back into the same sequence of numbers**. If it's important to your design that IDs are canonical, you have to manually re-encode decoded numbers and check that the generated ID matches. -Randomize IDs by providing a custom alphabet: +Enforce a *minimum* length for IDs: ```rust let sqids = Sqids::new(Some(Options::new( - Some("FxnXM1kBN6cuhsAvjW3Co7l2RePyY8DwaU04Tzt9fHQrqSVKdpimLGIJOgb5ZE".to_string()), None, + Some(10), None, )))?; -let id = sqids.encode(&[1, 2, 3])?; // "B5aMa3" +let id = sqids.encode(&[1, 2, 3])?; // "86Rf07xd4z" let numbers = sqids.decode(&id); // [1, 2, 3] ``` -Enforce a *minimum* length for IDs: +Randomize IDs by providing a custom alphabet: ```rust let sqids = Sqids::new(Some(Options::new( + Some("FxnXM1kBN6cuhsAvjW3Co7l2RePyY8DwaU04Tzt9fHQrqSVKdpimLGIJOgb5ZE".to_string()), None, - Some(10), None, )))?; -let id = sqids.encode(&[1, 2, 3])?; // "75JT1cd0dL" +let id = sqids.encode(&[1, 2, 3])?; // "B4aajs" let numbers = sqids.decode(&id); // [1, 2, 3] ``` @@ -81,9 +81,9 @@ Prevent specific words from appearing anywhere in the auto-generated IDs: let sqids = Sqids::new(Some(Options::new( None, None, - Some(HashSet::from(["word1".to_string(), "word2".to_string()])), + Some(HashSet::from(["86Rf07".to_string()])), )))?; -let id = sqids.encode(&[1, 2, 3])?; // "8QRLaD" +let id = sqids.encode(&[1, 2, 3])?; // "se8ojk" let numbers = sqids.decode(&id); // [1, 2, 3] ``` diff --git a/src/lib.rs b/src/lib.rs index 6c55956..8bcebea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,18 +1,16 @@ use derive_more::Display; -use std::{collections::HashSet, result}; +use std::{cmp::min, collections::HashSet, result}; #[derive(Display, Debug, Eq, PartialEq)] pub enum Error { - #[display(fmt = "Alphabet length must be at least 5")] + #[display(fmt = "Alphabet cannot contain multibyte characters")] + AlphabetMultibyteCharacters, + #[display(fmt = "Alphabet length must be at least 3")] AlphabetLength, #[display(fmt = "Alphabet must contain unique characters")] AlphabetUniqueCharacters, - #[display(fmt = "Minimum length has to be between {min} and {max}")] - MinLength { min: usize, max: usize }, - #[display(fmt = "Encoding supports numbers between {min} and {max}")] - EncodingRange { min: u64, max: u64 }, - #[display(fmt = "Ran out of range checking against the blocklist")] - BlocklistOutOfRange, + #[display(fmt = "Reached max attempts to re-generate the ID")] + BlocklistMaxAttempts, } pub type Result = result::Result; @@ -24,14 +22,14 @@ pub fn default_blocklist() -> HashSet { #[derive(Debug)] pub struct Options { pub alphabet: String, - pub min_length: usize, + pub min_length: u8, pub blocklist: HashSet, } impl Options { pub fn new( alphabet: Option, - min_length: Option, + min_length: Option, blocklist: Option>, ) -> Self { let mut options = Options::default(); @@ -63,7 +61,7 @@ impl Default for Options { #[derive(Debug)] pub struct Sqids { alphabet: Vec, - min_length: usize, + min_length: u8, blocklist: HashSet, } @@ -78,7 +76,13 @@ impl Sqids { let options = options.unwrap_or_default(); let alphabet: Vec = options.alphabet.chars().collect(); - if alphabet.len() < 5 { + for c in alphabet.iter() { + if c.len_utf8() > 1 { + return Err(Error::AlphabetMultibyteCharacters); + } + } + + if alphabet.len() < 3 { return Err(Error::AlphabetLength); } @@ -87,15 +91,6 @@ impl Sqids { return Err(Error::AlphabetUniqueCharacters); } - if options.min_length < Self::min_value() as usize - || options.min_length > options.alphabet.len() - { - return Err(Error::MinLength { - min: Self::min_value() as usize, - max: options.alphabet.len(), - }); - } - let lowercase_alphabet: Vec = alphabet.iter().map(|c| c.to_ascii_lowercase()).collect(); let filtered_blocklist: HashSet = options @@ -111,11 +106,11 @@ impl Sqids { }) .collect(); - let mut sqids = - Sqids { alphabet, min_length: options.min_length, blocklist: filtered_blocklist }; - - sqids.alphabet = sqids.shuffle(&sqids.alphabet); - Ok(sqids) + Ok(Sqids { + alphabet: Self::shuffle(&alphabet), + min_length: options.min_length, + blocklist: filtered_blocklist, + }) } pub fn encode(&self, numbers: &[u64]) -> Result { @@ -123,17 +118,7 @@ impl Sqids { return Ok(String::new()); } - let in_range_numbers: Vec = numbers - .iter() - .copied() - .filter(|&n| n >= Self::min_value() && n <= Self::max_value()) - .collect(); - - if in_range_numbers.len() != numbers.len() { - return Err(Error::EncodingRange { min: Self::min_value(), max: Self::max_value() }); - } - - self.encode_numbers(&in_range_numbers, false) + self.encode_numbers(numbers, 0) } pub fn decode(&self, id: &str) -> Vec { @@ -153,38 +138,25 @@ impl Sqids { let mut alphabet: Vec = self.alphabet.iter().cycle().skip(offset).take(self.alphabet.len()).copied().collect(); - let partition = alphabet[1]; - - alphabet.remove(1); - alphabet.remove(0); + alphabet = alphabet.into_iter().rev().collect(); let mut id = id[1..].to_string(); - let partition_index = id.find(partition); - if let Some(idx) = partition_index { - if idx > 0 && idx < id.len() - 1 { - id = id.split_off(idx + 1); - alphabet = self.shuffle(&alphabet); - } - } - while !id.is_empty() { - let separator = alphabet[alphabet.len() - 1]; - let chunks: Vec<&str> = id.split(separator).collect(); + let separator = alphabet[0]; + let chunks: Vec<&str> = id.split(separator).collect(); if !chunks.is_empty() { - let alphabet_without_separator: Vec = - alphabet.iter().copied().take(alphabet.len() - 1).collect(); - for c in chunks[0].chars() { - if !alphabet_without_separator.contains(&c) { - return vec![]; - } + if chunks[0].is_empty() { + return ret; } - let num = self.to_number(chunks[0], &alphabet_without_separator); - ret.push(num); + + let alphabet_without_separator: Vec = + alphabet.iter().copied().skip(1).collect(); + ret.push(self.to_number(chunks[0], &alphabet_without_separator)); if chunks.len() > 1 { - alphabet = self.shuffle(&alphabet); + alphabet = Self::shuffle(&alphabet); } } @@ -194,80 +166,52 @@ impl Sqids { ret } - pub fn min_value() -> u64 { - 0 - } - - pub fn max_value() -> u64 { - u64::MAX - } + fn encode_numbers(&self, numbers: &[u64], increment: usize) -> Result { + if increment > self.alphabet.len() { + return Err(Error::BlocklistMaxAttempts); + } - fn encode_numbers(&self, numbers: &[u64], partitioned: bool) -> Result { - let offset = numbers.iter().enumerate().fold(numbers.len(), |a, (i, &v)| { + let mut offset = numbers.iter().enumerate().fold(numbers.len(), |a, (i, &v)| { self.alphabet[v as usize % self.alphabet.len()] as usize + i + a }) % self.alphabet.len(); + offset = (offset + increment) % self.alphabet.len(); + let mut alphabet: Vec = self.alphabet.iter().cycle().skip(offset).take(self.alphabet.len()).copied().collect(); let prefix = alphabet[0]; - let partition = alphabet[1]; - alphabet.remove(1); - alphabet.remove(0); + alphabet = alphabet.into_iter().rev().collect(); let mut ret: Vec = vec![prefix.to_string()]; for (i, &num) in numbers.iter().enumerate() { - let alphabet_without_separator: Vec = - alphabet.iter().copied().take(alphabet.len() - 1).collect(); - ret.push(self.to_id(num, &alphabet_without_separator)); + ret.push(self.to_id(num, &alphabet[1..])); if i < numbers.len() - 1 { - let separator = alphabet[alphabet.len() - 1]; - - if partitioned && i == 0 { - ret.push(partition.to_string()); - } else { - ret.push(separator.to_string()); - } - - alphabet = self.shuffle(&alphabet); + ret.push(alphabet[0].to_string()); + alphabet = Self::shuffle(&alphabet); } } let mut id = ret.join(""); - if self.min_length > id.len() { - if !partitioned { - let mut new_numbers = vec![0]; - new_numbers.extend_from_slice(numbers); - id = self.encode_numbers(&new_numbers, true)?; - } + if self.min_length as usize > id.len() { + id += &alphabet[0].to_string(); - if self.min_length > id.len() { - id = id[..1].to_string() - + &alphabet[..(self.min_length - id.len())].iter().collect::() - + &id[1..] - } - } + while self.min_length as usize - id.len() > 0 { + alphabet = Self::shuffle(&alphabet); - if self.is_blocked_id(&id) { - let mut new_numbers; + let slice_len = min(self.min_length as usize - id.len(), alphabet.len()); + let slice: Vec = alphabet.iter().take(slice_len).cloned().collect(); - if partitioned { - if numbers[0] + 1 > Self::max_value() { - return Err(Error::BlocklistOutOfRange); - } else { - new_numbers = numbers.to_vec(); - new_numbers[0] += 1; - } - } else { - new_numbers = vec![0]; - new_numbers.extend_from_slice(numbers); + id += &slice.iter().collect::(); } + } - id = self.encode_numbers(&new_numbers, true)?; + if self.is_blocked_id(&id) { + id = self.encode_numbers(numbers, increment + 1)?; } Ok(id) @@ -301,7 +245,7 @@ impl Sqids { result } - fn shuffle(&self, alphabet: &[char]) -> Vec { + fn shuffle(alphabet: &[char]) -> Vec { let mut chars: Vec = alphabet.to_vec(); for i in 0..(chars.len() - 1) { diff --git a/tests/alphabet.rs b/tests/alphabet.rs index 140ac2c..354aaa2 100644 --- a/tests/alphabet.rs +++ b/tests/alphabet.rs @@ -6,7 +6,7 @@ fn simple() { Sqids::new(Some(Options::new(Some("0123456789abcdef".to_string()), None, None))).unwrap(); let numbers = vec![1, 2, 3]; - let id = "4d9fd2"; + let id = "489158"; assert_eq!(sqids.encode(&numbers).unwrap(), id); assert_eq!(sqids.decode(id), numbers); @@ -14,7 +14,7 @@ fn simple() { #[test] fn short_alphabet() { - let sqids = Sqids::new(Some(Options::new(Some("abcde".to_string()), None, None))).unwrap(); + let sqids = Sqids::new(Some(Options::new(Some("abc".to_string()), None, None))).unwrap(); let numbers = vec![1, 2, 3]; assert_eq!(sqids.decode(&sqids.encode(&numbers).unwrap()), numbers); @@ -32,6 +32,14 @@ fn long_alphabet() { assert_eq!(sqids.decode(&sqids.encode(&numbers).unwrap()), numbers); } +#[test] +fn multibyte_characters() { + assert_eq!( + Sqids::new(Some(Options::new(Some("ë1092".to_string()), None, None,))).err().unwrap(), + Error::AlphabetMultibyteCharacters + ) +} + #[test] fn repeating_alphabet_characters() { assert_eq!( @@ -43,7 +51,7 @@ fn repeating_alphabet_characters() { #[test] fn too_short_alphabet() { assert_eq!( - Sqids::new(Some(Options::new(Some("abcd".to_string()), None, None,))).err().unwrap(), + Sqids::new(Some(Options::new(Some("ab".to_string()), None, None,))).err().unwrap(), Error::AlphabetLength ) } diff --git a/tests/blocklist.rs b/tests/blocklist.rs index aaef7cd..470a7a5 100644 --- a/tests/blocklist.rs +++ b/tests/blocklist.rs @@ -5,32 +5,32 @@ use std::collections::HashSet; fn if_no_custom_blocklist_param_use_default_blocklist() { let sqids = Sqids::default(); - assert_eq!(sqids.decode("sexy"), vec![200044]); - assert_eq!(sqids.encode(&[200044]).unwrap(), "d171vI"); + assert_eq!(sqids.decode("aho1e"), vec![4572721]); + assert_eq!(sqids.encode(&[4572721]).unwrap(), "JExTR"); } #[test] fn if_empty_blocklist_param_passed_dont_use_any_blocklist() { let sqids = Sqids::new(Some(Options::new(None, None, Some(HashSet::new())))).unwrap(); - assert_eq!(sqids.decode("sexy"), vec![200044]); - assert_eq!(sqids.encode(&[200044]).unwrap(), "sexy"); + assert_eq!(sqids.decode("aho1e"), vec![4572721]); + assert_eq!(sqids.encode(&[4572721]).unwrap(), "aho1e"); } #[test] fn if_non_empty_blocklist_param_passed_use_only_that() { let sqids = - Sqids::new(Some(Options::new(None, None, Some(HashSet::from(["AvTg".to_string()]))))) + Sqids::new(Some(Options::new(None, None, Some(HashSet::from(["ArUO".to_string()]))))) .unwrap(); // make sure we don't use the default blocklist - assert_eq!(sqids.decode("sexy"), vec![200044]); - assert_eq!(sqids.encode(&[200044]).unwrap(), "sexy"); + assert_eq!(sqids.decode("aho1e"), vec![4572721]); + assert_eq!(sqids.encode(&[4572721]).unwrap(), "aho1e"); // make sure we are using the passed blocklist - assert_eq!(sqids.decode("AvTg"), vec![100000]); - assert_eq!(sqids.encode(&[100000]).unwrap(), "7T1X8k"); - assert_eq!(sqids.decode("7T1X8k"), vec![100000]); + assert_eq!(sqids.decode("ArUO"), vec![100000]); + assert_eq!(sqids.encode(&[100000]).unwrap(), "QyG4"); + assert_eq!(sqids.decode("QyG4"), vec![100000]); } #[test] @@ -39,17 +39,17 @@ fn blocklist() { None, None, Some(HashSet::from([ - "8QRLaD".to_owned(), // normal result of 1st encoding, let's block that word on purpose - "7T1cd0dL".to_owned(), // result of 2nd encoding - "UeIe".to_owned(), // result of 3rd encoding is `RA8UeIe7`, let's block a substring - "imhw".to_owned(), // result of 4th encoding is `WM3Limhw`, let's block the postfix - "LfUQ".to_owned(), // result of 4th encoding is `LfUQh4HN`, let's block the prefix + "JSwXFaosAN".to_owned(), // normal result of 1st encoding, let's block that word on purpose + "OCjV9JK64o".to_owned(), // result of 2nd encoding + "rBHf".to_owned(), // result of 3rd encoding is `4rBHfOiqd3`, let's block a substring + "79SM".to_owned(), // result of 4th encoding is `dyhgw479SM`, let's block the postfix + "7tE6".to_owned(), // result of 4th encoding is `7tE6jdAHLe`, let's block the prefix ])), ))) .unwrap(); - assert_eq!(sqids.encode(&[1, 2, 3]).unwrap(), "TM0x1Mxz"); - assert_eq!(sqids.decode("TM0x1Mxz"), vec![1, 2, 3]); + assert_eq!(sqids.encode(&[1_000_000, 2_000_000]).unwrap(), "1aYeB7bRUt"); + assert_eq!(sqids.decode("1aYeB7bRUt"), vec![1_000_000, 2_000_000]); } #[test] @@ -58,25 +58,25 @@ fn decoding_blocklist_words_should_still_work() { None, None, Some(HashSet::from([ - "8QRLaD".to_owned(), - "7T1cd0dL".to_owned(), - "RA8UeIe7".to_owned(), - "WM3Limhw".to_owned(), - "LfUQh4HN".to_owned(), + "86Rf07".to_owned(), + "se8ojk".to_owned(), + "ARsz1p".to_owned(), + "Q8AI49".to_owned(), + "5sQRZO".to_owned(), ])), ))) .unwrap(); - assert_eq!(sqids.decode("8QRLaD"), vec![1, 2, 3]); - assert_eq!(sqids.decode("7T1cd0dL"), vec![1, 2, 3]); - assert_eq!(sqids.decode("RA8UeIe7"), vec![1, 2, 3]); - assert_eq!(sqids.decode("WM3Limhw"), vec![1, 2, 3]); - assert_eq!(sqids.decode("LfUQh4HN"), vec![1, 2, 3]); + assert_eq!(sqids.decode("86Rf07"), vec![1, 2, 3]); + assert_eq!(sqids.decode("se8ojk"), vec![1, 2, 3]); + assert_eq!(sqids.decode("ARsz1p"), vec![1, 2, 3]); + assert_eq!(sqids.decode("Q8AI49"), vec![1, 2, 3]); + assert_eq!(sqids.decode("5sQRZO"), vec![1, 2, 3]); } #[test] fn match_against_short_blocklist_word() { - let sqids = Sqids::new(Some(Options::new(None, None, Some(HashSet::from(["pPQ".to_owned()]))))) + let sqids = Sqids::new(Some(Options::new(None, None, Some(HashSet::from(["pnd".to_owned()]))))) .unwrap(); assert_eq!(sqids.decode(&sqids.encode(&[1000]).unwrap()), vec![1000]); @@ -87,13 +87,32 @@ fn blocklist_filtering_in_constructor() { let sqids = Sqids::new(Some(Options::new( Some("ABCDEFGHIJKLMNOPQRSTUVWXYZ".to_string()), None, - Some(HashSet::from(["sqnmpn".to_owned()])), // lowercase blocklist in only-uppercase alphabet + Some(HashSet::from(["sxnzkl".to_owned()])), // lowercase blocklist in only-uppercase alphabet ))) .unwrap(); let id = sqids.encode(&[1, 2, 3]).unwrap(); let numbers = sqids.decode(&id); - assert_eq!(id, "ULPBZGBM".to_string()); // without blocklist, would've been "SQNMPN" + assert_eq!(id, "IBSHOZ".to_string()); // without blocklist, would've been "SXNZKL" assert_eq!(numbers, vec![1, 2, 3]); } + +#[test] +fn max_encoding_attempts() { + let alphabet = "abc".to_string(); + let min_length = 3; + let blocklist = HashSet::from(["cab".to_owned(), "abc".to_owned(), "bca".to_owned()]); + + let sqids = Sqids::new(Some(Options::new( + Some(alphabet.clone()), + Some(min_length), + Some(blocklist.clone()), + ))) + .unwrap(); + + assert_eq!(min_length as usize, alphabet.len()); + assert_eq!(min_length as usize, blocklist.len()); + + assert_eq!(sqids.encode(&[0]).err().unwrap(), Error::BlocklistMaxAttempts); +} diff --git a/tests/encoding.rs b/tests/encoding.rs index 1669790..d6d70d5 100644 --- a/tests/encoding.rs +++ b/tests/encoding.rs @@ -5,7 +5,7 @@ fn simple() { let sqids = Sqids::default(); let numbers = vec![1, 2, 3]; - let id = "8QRLaD"; + let id = "86Rf07"; assert_eq!(sqids.encode(&numbers).unwrap(), id); assert_eq!(sqids.decode(id), numbers); @@ -15,7 +15,7 @@ fn simple() { fn different_inputs() { let sqids = Sqids::default(); - let numbers = vec![0, 0, 0, 1, 2, 3, 100, 1_000, 100_000, 1_000_000, Sqids::max_value()]; + let numbers = vec![0, 0, 0, 1, 2, 3, 100, 1_000, 100_000, 1_000_000, u64::MAX]; assert_eq!(sqids.decode(&sqids.encode(&numbers).unwrap()), numbers); } @@ -25,16 +25,16 @@ fn incremental_numbers() { let sqids = Sqids::default(); let ids = vec![ - ("bV", vec![0]), - ("U9", vec![1]), - ("g8", vec![2]), - ("Ez", vec![3]), - ("V8", vec![4]), - ("ul", vec![5]), - ("O3", vec![6]), - ("AF", vec![7]), - ("ph", vec![8]), - ("n8", vec![9]), + ("bM", vec![0]), + ("Uk", vec![1]), + ("gb", vec![2]), + ("Ef", vec![3]), + ("Vq", vec![4]), + ("uw", vec![5]), + ("OI", vec![6]), + ("AX", vec![7]), + ("p6", vec![8]), + ("nJ", vec![9]), ]; for (id, numbers) in ids { @@ -48,16 +48,16 @@ fn incremental_numbers_same_index_0() { let sqids = Sqids::default(); let ids = vec![ - ("SrIu", vec![0, 0]), - ("nZqE", vec![0, 1]), - ("tJyf", vec![0, 2]), - ("e86S", vec![0, 3]), - ("rtC7", vec![0, 4]), - ("sQ8R", vec![0, 5]), - ("uz2n", vec![0, 6]), - ("7Td9", vec![0, 7]), - ("3nWE", vec![0, 8]), - ("mIxM", vec![0, 9]), + ("SvIz", vec![0, 0]), + ("n3qa", vec![0, 1]), + ("tryF", vec![0, 2]), + ("eg6q", vec![0, 3]), + ("rSCF", vec![0, 4]), + ("sR8x", vec![0, 5]), + ("uY2M", vec![0, 6]), + ("74dI", vec![0, 7]), + ("30WX", vec![0, 8]), + ("moxr", vec![0, 9]), ]; for (id, numbers) in ids { @@ -71,16 +71,16 @@ fn incremental_numbers_same_index_1() { let sqids = Sqids::default(); let ids = vec![ - ("SrIu", vec![0, 0]), - ("nbqh", vec![1, 0]), - ("t4yj", vec![2, 0]), - ("eQ6L", vec![3, 0]), - ("r4Cc", vec![4, 0]), - ("sL82", vec![5, 0]), - ("uo2f", vec![6, 0]), - ("7Zdq", vec![7, 0]), - ("36Wf", vec![8, 0]), - ("m4xT", vec![9, 0]), + ("SvIz", vec![0, 0]), + ("nWqP", vec![1, 0]), + ("tSyw", vec![2, 0]), + ("eX68", vec![3, 0]), + ("rxCY", vec![4, 0]), + ("sV8a", vec![5, 0]), + ("uf2K", vec![6, 0]), + ("7Cdk", vec![7, 0]), + ("3aWP", vec![8, 0]), + ("m2xn", vec![9, 0]), ]; for (id, numbers) in ids { @@ -118,10 +118,3 @@ fn decoding_invalid_character() { let numbers: Vec = vec![]; assert_eq!(sqids.decode("*"), numbers); } - -#[test] -fn decoding_invalid_id_with_repeating_reserved_character() { - let sqids = Sqids::default(); - let numbers: Vec = vec![]; - assert_eq!(sqids.decode("fff"), numbers); -} diff --git a/tests/minlength.rs b/tests/minlength.rs index 1910ab0..c2896cc 100644 --- a/tests/minlength.rs +++ b/tests/minlength.rs @@ -2,32 +2,75 @@ use sqids::*; #[test] fn simple() { - let sqids = Sqids::new(Some(Options::new(None, Some(Options::default().alphabet.len()), None))) - .unwrap(); + let sqids = + Sqids::new(Some(Options::new(None, Some(Options::default().alphabet.len() as u8), None))) + .unwrap(); let numbers = vec![1, 2, 3]; - let id = "75JILToVsGerOADWmHlY38xvbaNZKQ9wdFS0B6kcMEtnRpgizhjU42qT1cd0dL".to_owned(); + let id = "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTM".to_owned(); assert_eq!(sqids.encode(&numbers).unwrap(), id); assert_eq!(sqids.decode(&id), numbers); } +#[test] +fn incremental() { + let numbers = [1, 2, 3]; + let alphabet_length = Options::default().alphabet.len() as u8; + + let map = vec![ + (6 as u8, "86Rf07".to_owned()), + (7, "86Rf07x".to_owned()), + (8, "86Rf07xd".to_owned()), + (9, "86Rf07xd4".to_owned()), + (10, "86Rf07xd4z".to_owned()), + (11, "86Rf07xd4zB".to_owned()), + (12, "86Rf07xd4zBm".to_owned()), + (13, "86Rf07xd4zBmi".to_owned()), + ( + alphabet_length + 0, + "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTM".to_owned(), + ), + ( + alphabet_length + 1, + "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTMy".to_owned(), + ), + ( + alphabet_length + 2, + "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTMyf".to_owned(), + ), + ( + alphabet_length + 3, + "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTMyf1".to_owned(), + ), + ]; + + for (min_length, id) in map { + let sqids = Sqids::new(Some(Options::new(None, Some(min_length), None))).unwrap(); + + assert_eq!(sqids.encode(&numbers).unwrap(), id); + assert_eq!(sqids.encode(&numbers).unwrap().len(), min_length as usize); + assert_eq!(sqids.decode(&id), numbers); + } +} + #[test] fn incremental_numbers() { - let sqids = Sqids::new(Some(Options::new(None, Some(Options::default().alphabet.len()), None))) - .unwrap(); + let sqids = + Sqids::new(Some(Options::new(None, Some(Options::default().alphabet.len() as u8), None))) + .unwrap(); let ids = vec![ - ("jf26PLNeO5WbJDUV7FmMtlGXps3CoqkHnZ8cYd19yIiTAQuvKSExzhrRghBlwf".to_owned(), vec![0, 0]), - ("vQLUq7zWXC6k9cNOtgJ2ZK8rbxuipBFAS10yTdYeRa3ojHwGnmMV4PDhESI2jL".to_owned(), vec![0, 1]), - ("YhcpVK3COXbifmnZoLuxWgBQwtjsSaDGAdr0ReTHM16yI9vU8JNzlFq5Eu2oPp".to_owned(), vec![0, 2]), - ("OTkn9daFgDZX6LbmfxI83RSKetJu0APihlsrYoz5pvQw7GyWHEUcN2jBqd4kJ9".to_owned(), vec![0, 3]), - ("h2cV5eLNYj1x4ToZpfM90UlgHBOKikQFvnW36AC8zrmuJ7XdRytIGPawqYEbBe".to_owned(), vec![0, 4]), - ("7Mf0HeUNkpsZOTvmcj836P9EWKaACBubInFJtwXR2DSzgYGhQV5i4lLxoT1qdU".to_owned(), vec![0, 5]), - ("APVSD1ZIY4WGBK75xktMfTev8qsCJw6oyH2j3OnLcXRlhziUmpbuNEar05QCsI".to_owned(), vec![0, 6]), - ("P0LUhnlT76rsWSofOeyRGQZv1cC5qu3dtaJYNEXwk8Vpx92bKiHIz4MgmiDOF7".to_owned(), vec![0, 7]), - ("xAhypZMXYIGCL4uW0te6lsFHaPc3SiD1TBgw5O7bvodzjqUn89JQRfk2Nvm4JI".to_owned(), vec![0, 8]), - ("94dRPIZ6irlXWvTbKywFuAhBoECQOVMjDJp53s2xeqaSzHY8nc17tmkLGwfGNl".to_owned(), vec![0, 9]), + ("SvIzsqYMyQwI3GWgJAe17URxX8V924Co0DaTZLtFjHriEn5bPhcSkfmvOslpBu".to_owned(), vec![0, 0]), + ("n3qafPOLKdfHpuNw3M61r95svbeJGk7aAEgYn4WlSjXURmF8IDqZBy0CT2VxQc".to_owned(), vec![0, 1]), + ("tryFJbWcFMiYPg8sASm51uIV93GXTnvRzyfLleh06CpodJD42B7OraKtkQNxUZ".to_owned(), vec![0, 2]), + ("eg6ql0A3XmvPoCzMlB6DraNGcWSIy5VR8iYup2Qk4tjZFKe1hbwfgHdUTsnLqE".to_owned(), vec![0, 3]), + ("rSCFlp0rB2inEljaRdxKt7FkIbODSf8wYgTsZM1HL9JzN35cyoqueUvVWCm4hX".to_owned(), vec![0, 4]), + ("sR8xjC8WQkOwo74PnglH1YFdTI0eaf56RGVSitzbjuZ3shNUXBrqLxEJyAmKv2".to_owned(), vec![0, 5]), + ("uY2MYFqCLpgx5XQcjdtZK286AwWV7IBGEfuS9yTmbJvkzoUPeYRHr4iDs3naN0".to_owned(), vec![0, 6]), + ("74dID7X28VLQhBlnGmjZrec5wTA1fqpWtK4YkaoEIM9SRNiC3gUJH0OFvsPDdy".to_owned(), vec![0, 7]), + ("30WXpesPhgKiEI5RHTY7xbB1GnytJvXOl2p0AcUjdF6waZDo9Qk8VLzMuWrqCS".to_owned(), vec![0, 8]), + ("moxr3HqLAK0GsTND6jowfZz3SUx7cQ8aC54Pl1RbIvFXmEJuBMYVeW9yrdOtin".to_owned(), vec![0, 9]), ]; for (id, numbers) in ids { @@ -38,31 +81,21 @@ fn incremental_numbers() { #[test] fn min_lengths() { - for &min_length in &[0, 1, 5, 10, Options::default().alphabet.len()] { + for &min_length in &[0, 1, 5, 10, Options::default().alphabet.len() as u8] { for numbers in &[ - vec![Sqids::min_value()], + vec![u64::MIN], vec![0, 0, 0, 0, 0], vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10], vec![100, 200, 300], vec![1_000, 2_000, 3_000], vec![1_000_000], - vec![Sqids::max_value()], + vec![u64::MAX], ] { let sqids = Sqids::new(Some(Options::new(None, Some(min_length), None))).unwrap(); let id = sqids.encode(&numbers).unwrap(); - assert!(id.len() >= min_length); + assert!(id.len() >= min_length as usize); assert_eq!(sqids.decode(&id), *numbers); } } } - -#[test] -fn out_of_range_invalid_min_length() { - assert_eq!( - Sqids::new(Some(Options::new(None, Some(Options::default().alphabet.len() + 1), None))) - .err() - .unwrap(), - Error::MinLength { min: 0, max: Options::default().alphabet.len() } - ); -} diff --git a/tests/uniques.rs b/tests/uniques.rs index 0026e23..b1e029d 100644 --- a/tests/uniques.rs +++ b/tests/uniques.rs @@ -1,12 +1,13 @@ use sqids::*; use std::collections::HashSet; -const UPPER: u64 = 1_000_000; +const UPPER: u64 = 1_000; #[test] fn uniques_with_padding() { - let sqids = Sqids::new(Some(Options::new(None, Some(Options::default().alphabet.len()), None))) - .unwrap(); + let sqids = + Sqids::new(Some(Options::new(None, Some(Options::default().alphabet.len() as u8), None))) + .unwrap(); let mut set = HashSet::new(); for i in 0..UPPER {