This commit is contained in:
Ivan Akimov 2023-07-18 15:41:09 -05:00
parent cf377536d0
commit 6e189eb22f
6 changed files with 287 additions and 33 deletions

View File

@ -1,7 +1,7 @@
use derive_more::Display;
use std::{collections::HashSet, result};
#[derive(Display, Debug)]
#[derive(Display, Debug, Eq, PartialEq)]
pub enum Error {
#[display(fmt = "Alphabet length must be at least 5")]
AlphabetLength,
@ -23,9 +23,9 @@ pub fn default_blocklist() -> HashSet<String> {
#[derive(Debug)]
pub struct Options {
alphabet: String,
min_length: usize,
blocklist: HashSet<String>,
pub alphabet: String,
pub min_length: usize,
pub blocklist: HashSet<String>,
}
impl Options {
@ -81,6 +81,15 @@ 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 filtered_blocklist: HashSet<String> = options
.blocklist
.iter()
@ -97,15 +106,6 @@ impl Sqids {
let mut sqids =
Sqids { alphabet, min_length: options.min_length, blocklist: filtered_blocklist };
if options.min_length < sqids.min_value() as usize ||
options.min_length > options.alphabet.len()
{
return Err(Error::MinLength {
min: sqids.min_value() as usize,
max: options.alphabet.len(),
});
}
sqids.alphabet = sqids.shuffle(&sqids.alphabet);
Ok(sqids)
}
@ -118,11 +118,11 @@ impl Sqids {
let in_range_numbers: Vec<u64> = numbers
.iter()
.copied()
.filter(|&n| n >= self.min_value() && n <= self.max_value())
.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() });
return Err(Error::EncodingRange { min: Self::min_value(), max: Self::max_value() });
}
self.encode_numbers(&in_range_numbers, false)
@ -181,11 +181,11 @@ impl Sqids {
ret
}
pub fn min_value(&self) -> u64 {
pub fn min_value() -> u64 {
0
}
pub fn max_value(&self) -> u64 {
pub fn max_value() -> u64 {
u64::MAX
}
@ -233,11 +233,9 @@ impl Sqids {
}
if self.min_length > id.len() {
let mut new_id = id.clone();
let alphabet_slice = &alphabet[..(self.min_length - id.len())];
new_id.push_str(&alphabet_slice.iter().collect::<String>());
new_id.push_str(&id[1..]);
id = new_id;
id = id[..1].to_string() +
&alphabet[..(self.min_length - id.len())].iter().collect::<String>() +
&id[1..]
}
}
@ -245,7 +243,7 @@ impl Sqids {
let mut new_numbers;
if partitioned {
if numbers[0] + 1 > self.max_value() {
if numbers[0] + 1 > Self::max_value() {
return Err(Error::BlocklistOutOfRange);
} else {
new_numbers = numbers.to_vec();

49
tests/alphabet.rs Normal file
View File

@ -0,0 +1,49 @@
use sqids::*;
#[test]
fn simple() {
let sqids =
Sqids::new(Some(Options::new(Some("0123456789abcdef".to_string()), None, None))).unwrap();
let numbers = vec![1, 2, 3];
let id = "4d9fd2";
assert_eq!(sqids.encode(&numbers).unwrap(), id);
assert_eq!(sqids.decode(id), numbers);
}
#[test]
fn short_alphabet() {
let sqids = Sqids::new(Some(Options::new(Some("abcde".to_string()), None, None))).unwrap();
let numbers = vec![1, 2, 3];
assert_eq!(sqids.decode(&sqids.encode(&numbers).unwrap()), numbers);
}
#[test]
fn long_alphabet() {
let sqids = Sqids::new(Some(Options::new(
Some("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_+|{}[];:'\"/?.>,<`~".to_string()),
None,
None,
))).unwrap();
let numbers = vec![1, 2, 3];
assert_eq!(sqids.decode(&sqids.encode(&numbers).unwrap()), numbers);
}
#[test]
fn repeating_alphabet_characters() {
assert_eq!(
Sqids::new(Some(Options::new(Some("aabcdefg".to_string()), None, None,))).err().unwrap(),
Error::AlphabetUniqueCharacters
)
}
#[test]
fn too_short_alphabet() {
assert_eq!(
Sqids::new(Some(Options::new(Some("abcd".to_string()), None, None,))).err().unwrap(),
Error::AlphabetLength
)
}

83
tests/blocklist.rs Normal file
View File

@ -0,0 +1,83 @@
use sqids::*;
use std::collections::HashSet;
#[test]
fn if_no_custom_blocklist_param_use_default_blocklist() {
let sqids = Sqids::new(None).unwrap();
assert_eq!(sqids.decode("sexy"), vec![200044]);
assert_eq!(sqids.encode(&[200044]).unwrap(), "d171vI");
}
#[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");
}
#[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()])))))
.unwrap();
// make sure we don't use the default blocklist
assert_eq!(sqids.decode("sexy"), vec![200044]);
assert_eq!(sqids.encode(&[200044]).unwrap(), "sexy");
// 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]);
}
#[test]
fn blocklist() {
let sqids = Sqids::new(Some(Options::new(
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
])),
)))
.unwrap();
assert_eq!(sqids.encode(&[1, 2, 3]).unwrap(), "TM0x1Mxz");
assert_eq!(sqids.decode("TM0x1Mxz"), vec![1, 2, 3]);
}
#[test]
fn decoding_blocklist_words_should_still_work() {
let sqids = Sqids::new(Some(Options::new(
None,
None,
Some(HashSet::from([
"8QRLaD".to_owned(),
"7T1cd0dL".to_owned(),
"RA8UeIe7".to_owned(),
"WM3Limhw".to_owned(),
"LfUQh4HN".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]);
}
#[test]
fn match_against_short_blocklist_word() {
let sqids = Sqids::new(Some(Options::new(None, None, Some(HashSet::from(["pPQ".to_owned()])))))
.unwrap();
assert_eq!(sqids.decode(&sqids.encode(&[1000]).unwrap()), vec![1000]);
}

View File

@ -15,7 +15,7 @@ fn simple() {
fn different_inputs() {
let sqids = Sqids::new(None).unwrap();
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, Sqids::max_value()];
assert_eq!(sqids.decode(&sqids.encode(&numbers).unwrap()), numbers);
}
@ -118,12 +118,3 @@ fn decoding_invalid_character() {
let numbers: Vec<u64> = vec![];
assert_eq!(sqids.decode("*"), numbers);
}
#[test]
#[should_panic]
fn encode_out_of_range_numbers() {
let sqids = Sqids::new(None).unwrap();
assert!(sqids.encode(&[sqids.min_value() - 1]).is_err());
assert!(sqids.encode(&[sqids.max_value() + 1]).is_err());
}

68
tests/minlength.rs Normal file
View File

@ -0,0 +1,68 @@
use sqids::*;
#[test]
fn simple() {
let sqids = Sqids::new(Some(Options::new(None, Some(Options::default().alphabet.len()), None)))
.unwrap();
let numbers = vec![1, 2, 3];
let id = "75JILToVsGerOADWmHlY38xvbaNZKQ9wdFS0B6kcMEtnRpgizhjU42qT1cd0dL".to_owned();
assert_eq!(sqids.encode(&numbers).unwrap(), id);
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 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]),
];
for (id, numbers) in ids {
assert_eq!(sqids.encode(&numbers).unwrap(), id);
assert_eq!(sqids.decode(&id), numbers);
}
}
#[test]
fn min_lengths() {
for &min_length in &[0, 1, 5, 10, Options::default().alphabet.len()] {
for numbers in &[
vec![Sqids::min_value()],
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()],
] {
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_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() }
);
}

65
tests/uniques.rs Normal file
View File

@ -0,0 +1,65 @@
use sqids::*;
use std::collections::HashSet;
const UPPER: u64 = 1_000_000;
#[test]
fn uniques_with_padding() {
let sqids = Sqids::new(Some(Options::new(None, Some(Options::default().alphabet.len()), None)))
.unwrap();
let mut set = HashSet::new();
for i in 0..UPPER {
let numbers = vec![i];
let id = sqids.encode(&numbers).unwrap();
set.insert(id.clone());
assert_eq!(sqids.decode(&id), numbers);
}
assert_eq!(set.len(), UPPER as usize);
}
#[test]
fn uniques_low_ranges() {
let sqids = Sqids::new(None).unwrap();
let mut set = HashSet::new();
for i in 0..UPPER {
let numbers = vec![i];
let id = sqids.encode(&numbers).unwrap();
set.insert(id.clone());
assert_eq!(sqids.decode(&id), numbers);
}
assert_eq!(set.len(), UPPER as usize);
}
#[test]
fn uniques_high_ranges() {
let sqids = Sqids::new(None).unwrap();
let mut set = HashSet::new();
for i in 100_000_000..100_000_000 + UPPER {
let numbers = vec![i];
let id = sqids.encode(&numbers).unwrap();
set.insert(id.clone());
assert_eq!(sqids.decode(&id), numbers);
}
assert_eq!(set.len(), UPPER as usize);
}
#[test]
fn uniques_multi() {
let sqids = Sqids::new(None).unwrap();
let mut set = HashSet::new();
for i in 0..UPPER {
let numbers = vec![i, i, i, i, i];
let id = sqids.encode(&numbers).unwrap();
set.insert(id.clone());
assert_eq!(sqids.decode(&id), numbers);
}
assert_eq!(set.len(), UPPER as usize);
}