use crate::core::encoding::{EncodingFormat, encode_base64, encode_hex};
use crate::core::error::Result;
use crate::core::id::{
IdEncodings, IdGenerator, IdKind, InspectionResult, ParsedId, Timestamp, ValidationResult,
};
use serde_json::json;
pub const DEFAULT_ALPHABET: &str =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz-";
pub const DEFAULT_LENGTH: usize = 21;
pub struct NanoIdGenerator {
pub alphabet: String,
pub length: usize,
}
impl Default for NanoIdGenerator {
fn default() -> Self {
Self {
alphabet: DEFAULT_ALPHABET.to_string(),
length: DEFAULT_LENGTH,
}
}
}
impl NanoIdGenerator {
pub fn new() -> Self {
Self::default()
}
pub fn with_length(mut self, length: usize) -> Self {
self.length = length;
self
}
pub fn with_alphabet(mut self, alphabet: &str) -> Self {
self.alphabet = alphabet.to_string();
self
}
}
impl IdGenerator for NanoIdGenerator {
fn generate(&self) -> Result<String> {
let alphabet: Vec<char> = self.alphabet.chars().collect();
Ok(nanoid::format(
nanoid::rngs::default,
&alphabet,
self.length,
))
}
}
pub struct ParsedNanoId {
value: String,
input: String,
}
impl ParsedNanoId {
pub fn parse(input: &str) -> Result<Self> {
let input_trimmed = input.trim();
if input_trimmed.is_empty() {
return Err(crate::core::error::IdtError::ParseError(
"Empty NanoID".to_string(),
));
}
Ok(Self {
value: input_trimmed.to_string(),
input: input_trimmed.to_string(),
})
}
pub fn is_default_format(input: &str) -> bool {
if input.len() != DEFAULT_LENGTH {
return false;
}
input
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
}
}
impl ParsedId for ParsedNanoId {
fn kind(&self) -> IdKind {
IdKind::NanoId
}
fn canonical(&self) -> String {
self.value.clone()
}
fn as_bytes(&self) -> Vec<u8> {
self.value.as_bytes().to_vec()
}
fn timestamp(&self) -> Option<Timestamp> {
None
}
fn inspect(&self) -> InspectionResult {
let bytes = self.as_bytes();
let entropy_bits = (self.value.len() as f64 * 6.0) as u32;
let components = json!({
"length": self.value.len(),
"charset": "URL-safe (default)",
});
InspectionResult {
id_type: "nanoid".to_string(),
input: self.input.clone(),
canonical: self.canonical(),
valid: true,
timestamp: None,
timestamp_iso: None,
timestamp_local_iso: None,
version: None,
variant: None,
random_bits: Some(entropy_bits),
components: Some(components),
encodings: IdEncodings {
hex: encode_hex(&bytes),
base32: String::new(), base58: String::new(),
base64: encode_base64(&bytes),
int: None,
},
}
}
fn validate(&self) -> ValidationResult {
if Self::is_default_format(&self.value) {
ValidationResult::valid("nanoid")
} else {
ValidationResult::valid("nanoid").with_hint("Non-standard length or alphabet")
}
}
fn encode(&self, format: EncodingFormat) -> String {
let bytes = self.as_bytes();
match format {
EncodingFormat::Canonical => self.canonical(),
EncodingFormat::Hex => encode_hex(&bytes),
EncodingFormat::Base64 => encode_base64(&bytes),
_ => self.canonical(), }
}
}
pub fn is_nanoid(input: &str) -> bool {
ParsedNanoId::is_default_format(input)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_default() {
let generator = NanoIdGenerator::new();
let id = generator.generate().unwrap();
assert_eq!(id.len(), DEFAULT_LENGTH);
}
#[test]
fn test_generate_custom_length() {
let generator = NanoIdGenerator::new().with_length(32);
let id = generator.generate().unwrap();
assert_eq!(id.len(), 32);
}
#[test]
fn test_generate_hex_alphabet() {
let generator = NanoIdGenerator::new()
.with_alphabet("0123456789abcdef")
.with_length(16);
let id = generator.generate().unwrap();
assert_eq!(id.len(), 16);
assert!(id.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_is_default_format() {
assert!(ParsedNanoId::is_default_format("V1StGXR8_Z5jdHi6B-myT"));
assert!(!ParsedNanoId::is_default_format("too-short"));
}
}