///| MoonBit port of Nano ID library
/// A tiny, URL-friendly, unique string ID generator
/// MoonBit implementation compatible with the official nanoid JavaScript library
// ========== Error Handling ==========
///|
/// Error types for nanoid operations
pub(all) enum NanoidError {
EmptyAlphabet
OversizedAlphabet(Int)
DuplicateCharacter(Char, Int, Int)
SizeTooSmall(Int)
SizeTooLarge(Int)
RandomGenerationError(String)
} derive(Debug, Eq)
///|
/// Convert NanoidError to a human-readable string with helpful guidance
pub fn NanoidError::to_string(self : NanoidError) -> String {
match self {
EmptyAlphabet =>
"Alphabet cannot be empty. Please provide at least one character."
OversizedAlphabet(len) =>
"Alphabet length (\{len}) exceeds maximum of 256 characters. Consider using a smaller character set."
DuplicateCharacter(char, first_pos, dup_pos) =>
"Duplicate character '\{char}' found at position \{dup_pos}, first seen at \{first_pos}. Each character in the alphabet must be unique."
SizeTooSmall(size) =>
if size < 0 {
"Size (\{size}) cannot be negative. Please provide a positive integer."
} else {
"Size (\{size}) must be greater than 0. Use size=1 for single character IDs."
}
SizeTooLarge(size) =>
"Size (\{size}) exceeds maximum allowed size of 1,000,000. Large IDs may cause memory issues."
RandomGenerationError(msg) =>
"Random generation failed: \{msg}. This may indicate a system-level issue."
}
}
// ========== Unified Validation Module ==========
///|
/// Validation result for parameter checking
priv enum ValidationResult {
Valid
InvalidAlphabet(NanoidError)
InvalidSize(NanoidError)
}
///|
/// Internal alphabet validation
/// Performs validation in a single pass through the string
fn validate_alphabet_uncached(alphabet : String) -> ValidationResult {
// Use an immutable HashMap to efficiently check for duplicates and provide rich error messages.
// The map stores the character and the index where it was first seen.
let mut seen : @immut/hashmap.HashMap[Char, Int] = @immut/hashmap.new()
let mut char_count = 0
// Single pass: count characters and check for duplicates simultaneously
// Use for-in loop to correctly iterate over Unicode characters (including emoji, etc.)
for char in alphabet {
match seen.get(char) {
Some(first_pos) =>
// Duplicate found, return a detailed error with proper error type.
return InvalidAlphabet(DuplicateCharacter(char, first_pos, char_count))
None =>
// First time seeing this character, add it to the map with its index.
seen = seen.add(char, char_count)
}
char_count = char_count + 1
}
// Check for empty alphabet (after loop to maintain single pass)
if char_count == 0 {
return InvalidAlphabet(EmptyAlphabet)
}
// Check for oversized alphabet
if char_count > MAX_ALPHABET_SIZE {
return InvalidAlphabet(OversizedAlphabet(char_count))
}
Valid
}
///|
/// Validate size parameter
fn validate_size(size : Int) -> ValidationResult {
if size <= 0 {
return InvalidSize(SizeTooSmall(size))
}
if size > MAX_ID_SIZE {
return InvalidSize(SizeTooLarge(size))
}
Valid
}
///|
/// Validate custom random output length and byte range.
fn validate_random_bytes(
bytes : Array[Int],
expected_size : Int,
) -> Result[Array[Int], NanoidError] {
if bytes.length() != expected_size {
return Err(
RandomGenerationError(
"Custom random function returned incorrect array length: expected \{expected_size}, got \{bytes.length()}",
),
)
}
for i = 0; i < bytes.length(); i = i + 1 {
let byte = bytes[i]
if byte < 0 || byte >= 256 {
return Err(
RandomGenerationError(
"Custom random function returned invalid byte value at index \{i}: \{byte}. Expected values in range 0..255.",
),
)
}
}
Ok(bytes)
}
///|
/// Validate both alphabet and size parameters
fn validate_parameters(alphabet : String, size : Int) -> ValidationResult {
match validate_alphabet_uncached(alphabet) {
Valid => validate_size(size)
error => error
}
}
///|
/// Helper function to convert string to character array using safe iteration
fn string_to_chars(s : String) -> Array[Char] {
let chars : Array[Char] = []
for char in s {
chars.push(char)
}
chars
}
// ========== Constants and Alphabets ==========
///|
/// Default URL-friendly alphabet (official nanoid order)
/// Uses the same character order as official nanoid: A-Z, a-z, 0-9, underscore, hyphen
pub let url_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-"
///|
/// Default ID length (same as official nanoid)
const DEFAULT_SIZE = 21
// ========== Algorithm Constants ==========
///|
/// Maximum allowed alphabet size to prevent memory issues
const MAX_ALPHABET_SIZE = 256
///|
/// Maximum allowed ID size to prevent memory issues
const MAX_ID_SIZE = 1000000
///|
/// Safety factor for random-byte budget before failing generation.
/// A higher multiplier dramatically reduces false failures for small sizes while
/// still providing a finite upper bound for pathological custom random functions.
const BATCH_GENERATION_SAFETY_FACTOR = 8
///|
/// Minimum batch size for random byte generation
const MIN_BATCH_SIZE = 8
///|
/// Maximum batch size for random byte generation
const MAX_BATCH_SIZE = 256
// ========== Random Number Generation ==========
///|
/// Calculate mask for uniform distribution (power of 2 - 1)
fn calculate_mask(alphabet_length : Int) -> Int {
let mut mask = 1
while mask < alphabet_length {
mask = mask << 1
}
mask - 1
}
///|
/// Generate ID characters using batch processing with rejection sampling
fn generate_id_characters(
alphabet_chars : Array[Char],
alphabet_length : Int,
size : Int,
step : Int,
mask : Int,
get_random : (Int) -> Result[Array[Int], NanoidError],
) -> Result[String, NanoidError] {
let id_chars = Array::make(size, ' ')
let mut counter = 0
let mut batch_count = 0
let max_batches = calculate_max_batches(alphabet_length, size, step, mask)
// Main generation loop with batch processing
while counter < size && batch_count < max_batches {
let random_bytes = match get_random(step) {
Ok(bytes) =>
match validate_random_bytes(bytes, step) {
Ok(valid_bytes) => valid_bytes
Err(e) => return Err(e)
}
Err(e) => return Err(e)
}
// Process each byte in the batch using optimized version
counter = process_random_batch_optimized(
random_bytes, alphabet_chars, id_chars, counter, size, mask, alphabet_length,
)
batch_count = batch_count + 1
}
// Handle insufficient randomness case by returning an error
if counter < size {
return Err(
RandomGenerationError(
"Failed to generate sufficient random characters after \{batch_count} batches. This may indicate an issue with the random number generator.",
),
)
}
Ok(String::from_array(id_chars))
}
///|
/// Process a batch of random bytes and fill ID characters (optimized version)
fn process_random_batch_optimized(
random_bytes : Array[Int],
alphabet_chars : Array[Char],
id_chars : Array[Char],
start_counter : Int,
target_size : Int,
mask : Int,
alphabet_length : Int,
) -> Int {
let mut counter = start_counter
let mut byte_index = 0
while byte_index < random_bytes.length() && counter < target_size {
let byte = random_bytes[byte_index] & mask
if byte < alphabet_length {
// Direct array access is faster than string.get_char()
id_chars[counter] = alphabet_chars[byte]
counter = counter + 1
}
byte_index = byte_index + 1
}
counter
}
///|
/// Heuristic fast-path for optimal batch size when rejection probability is zero.
/// Returns Some(step) only when (mask + 1) == alphabet_length (i.e. power-of-two alphabet)
/// so every generated random byte can map to a character without rejection.
///
/// Strategy:
/// - Use target_size directly (1 batch ideal) to minimize RNG + loop overhead.
/// - Clamp to [MIN_BATCH_SIZE, MAX_BATCH_SIZE] to avoid pathological tiny/huge requests.
/// - Otherwise return None and let the analytical path compute a value.
///
/// Rationale:
/// Previous implementation hard-coded opaque magic numbers (e.g. 64->10) which caused
/// unnecessary multiple batches, increasing RNG calls and memory clearing. This version
/// keeps semantics minimal while enabling single-batch generation for common alphabets
/// like 64 (URL/base64) and 32 (base32) when size is within limits.
fn get_precomputed_batch_size(
alphabet_length : Int,
target_size : Int,
mask : Int,
) -> Int? {
if mask + 1 == alphabet_length { // perfect power-of-two span => zero rejection
// Clamp desired step to reasonable bounds
let mut step = target_size
if step < MIN_BATCH_SIZE {
step = MIN_BATCH_SIZE
}
if step > MAX_BATCH_SIZE {
step = MAX_BATCH_SIZE
}
return Some(step)
}
None
}
///|
/// Calculate optimal batch size for random byte generation
/// Uses entropy efficiency analysis to determine the best batch size
fn calculate_optimal_batch_size(
alphabet_length : Int,
target_size : Int,
mask : Int,
) -> Int {
// Fast path: perfect power-of-two span uses single batch (clamped) to reduce overhead.
let optimized_step = match
get_precomputed_batch_size(alphabet_length, target_size, mask) {
Some(precomputed) => precomputed
None => {
// Analytical estimation: bytes_needed ≈ target_size * waste_factor * safety_margin
// waste_factor = 1 / efficiency, efficiency = alphabet_length / (mask+1)
let efficiency = alphabet_length.to_double() / (mask + 1).to_double()
let waste_factor = 1.0 / efficiency
let safety_margin = 1.2 // modest extra to reduce probability of a second batch
let raw = (target_size.to_double() * waste_factor * safety_margin).to_int()
// Clamp to global bounds
let mut est = raw
if est < MIN_BATCH_SIZE {
est = MIN_BATCH_SIZE
}
if est > MAX_BATCH_SIZE {
est = MAX_BATCH_SIZE
}
est
}
}
// Avoid generating wildly more bytes than needed for very small target sizes.
let max_reasonable = target_size * 2
if optimized_step > max_reasonable {
max_reasonable
} else {
optimized_step
}
}
///|
/// Compute a finite batch budget based on expected random bytes needed.
/// expected_bytes = ceil(size * (mask + 1) / alphabet_length)
/// max_bytes = expected_bytes * safety_factor
fn calculate_max_batches(
alphabet_length : Int,
target_size : Int,
step : Int,
mask : Int,
) -> Int {
let expected_bytes = (target_size * (mask + 1) + alphabet_length - 1) /
alphabet_length
let max_bytes = expected_bytes * BATCH_GENERATION_SAFETY_FACTOR
let max_batches = (max_bytes + step - 1) / step
if max_batches < 1 {
1
} else {
max_batches
}
}
///|
/// Request exact number of bytes from a custom random function.
fn request_random_exact(
random : (Int) -> Result[Array[Int], NanoidError],
size : Int,
) -> Result[Array[Int], NanoidError] {
match random(size) {
Ok(bytes) => validate_random_bytes(bytes, size)
Err(e) => Err(e)
}
}
///|
/// Prepend a probe byte to the remaining random bytes.
fn prepend_probe_byte(first_byte : Int, rest_bytes : Array[Int]) -> Array[Int] {
let merged_bytes = Array::make(rest_bytes.length() + 1, 0)
merged_bytes[0] = first_byte
for i = 0; i < rest_bytes.length(); i = i + 1 {
merged_bytes[i + 1] = rest_bytes[i]
}
merged_bytes
}
///|
/// Try to build random bytes from the cached probe byte.
fn generate_with_probe_byte(
random : (Int) -> Result[Array[Int], NanoidError],
probe_byte : Ref[Int?],
first_byte : Int,
request_size : Int,
) -> Result[Array[Int], NanoidError] {
if request_size == 1 {
probe_byte.val = None
return Ok([first_byte])
}
match request_random_exact(random, request_size - 1) {
Ok(valid_rest_bytes) => {
probe_byte.val = None
Ok(prepend_probe_byte(first_byte, valid_rest_bytes))
}
Err(e) => Err(e)
}
}
///|
/// Generate random bytes, consuming probe byte first when available.
fn generate_replay_random_bytes(
random : (Int) -> Result[Array[Int], NanoidError],
probe_byte : Ref[Int?],
request_size : Int,
) -> Result[Array[Int], NanoidError] {
if request_size <= 0 {
return Ok([])
}
match probe_byte.val {
Some(first_byte) =>
generate_with_probe_byte(random, probe_byte, first_byte, request_size)
None => request_random_exact(random, request_size)
}
}
///|
/// Wrap a custom random function and replay the probe byte so validation does not
/// consume random state from the first generated ID.
fn create_replay_random(
random : (Int) -> Result[Array[Int], NanoidError],
) -> Result[(Int) -> Result[Array[Int], NanoidError], NanoidError] {
match request_random_exact(random, 1) {
Ok(valid_test_bytes) => {
let probe_byte : Ref[Int?] = { val: Some(valid_test_bytes[0]) }
Ok(fn(request_size : Int) -> Result[Array[Int], NanoidError] {
generate_replay_random_bytes(random, probe_byte, request_size)
})
}
Err(e) => Err(e)
}
}
///|
/// Internal generation without validation using precomputed alphabet characters
fn generate_unchecked_with_alphabet_chars(
alphabet_chars : Array[Char],
size : Int,
get_random : (Int) -> Result[Array[Int], NanoidError],
) -> Result[String, NanoidError] {
let alphabet_length = alphabet_chars.length()
// Single-character alphabet: skip RNG entirely during generation.
// Note: the probe-bypass at construction time (in custom_random) is a separate
// but related optimization — this branch handles the generation phase.
if alphabet_length == 1 {
return Ok(String::from_array(Array::make(size, alphabet_chars[0])))
}
let mask = calculate_mask(alphabet_length)
let step = calculate_optimal_batch_size(alphabet_length, size, mask)
generate_id_characters(
alphabet_chars, alphabet_length, size, step, mask, get_random,
)
}
///|
/// Internal generation without validation (caller must validate first)
fn generate_unchecked(
alphabet : String,
size : Int,
get_random : (Int) -> Result[Array[Int], NanoidError],
) -> Result[String, NanoidError] {
generate_unchecked_with_alphabet_chars(
string_to_chars(alphabet),
size,
get_random,
)
}
///|
/// Generate ID with validation
fn generate(
alphabet : String,
size : Int,
get_random : (Int) -> Result[Array[Int], NanoidError],
) -> Result[String, NanoidError] {
match validate_parameters(alphabet, size) {
Valid => generate_unchecked(alphabet, size, get_random)
InvalidAlphabet(error) => Err(error)
InvalidSize(error) => Err(error)
}
}
///|
/// Main nanoid function - generates URL-friendly unique ID
/// Generates random IDs using the default URL-safe alphabet
/// Returns Result with ID string or NanoidError for invalid parameters
/// Usage: nanoid() generates 21-character ID, nanoid(size=10) generates 10-character ID
pub fn nanoid(size? : Int = DEFAULT_SIZE) -> Result[String, NanoidError] {
generate(url_alphabet, size, get_random_bytes)
}
///|
/// Convenience function that returns empty string on error (for backward compatibility)
/// Usage: nanoid_or_empty() generates 21-character ID or empty string on error
pub fn nanoid_or_empty(size? : Int = DEFAULT_SIZE) -> String {
match nanoid(size~) {
Ok(id) => id
Err(_) => ""
}
}
///|
/// Custom alphabet function - returns a nanoid generator with custom alphabet
/// Creates a generator function that uses the specified alphabet and size
/// Returns Result with generator function or NanoidError for invalid parameters
/// Usage: let gen = custom_alphabet("abc123", size=8)?; let id = gen()?
pub fn custom_alphabet(
alphabet : String,
size? : Int = DEFAULT_SIZE,
) -> Result[() -> Result[String, NanoidError], NanoidError] {
match validate_parameters(alphabet, size) {
Valid => {
let alphabet_chars = string_to_chars(alphabet)
Ok(fn() -> Result[String, NanoidError] {
generate_unchecked_with_alphabet_chars(
alphabet_chars, size, get_random_bytes,
)
})
}
InvalidAlphabet(error) => Err(error)
InvalidSize(error) => Err(error)
}
}
///|
/// Convenience custom alphabet function that returns empty string on error
/// Usage: let gen = custom_alphabet_or_empty("abc123", size=8); let id = gen()
pub fn custom_alphabet_or_empty(
alphabet : String,
size? : Int = DEFAULT_SIZE,
) -> () -> String {
match custom_alphabet(alphabet, size~) {
Ok(gen) =>
fn() -> String {
match gen() {
Ok(id) => id
Err(_) => ""
}
}
Err(_) => fn() -> String { "" }
}
}
///|
/// Custom random function - allows custom random generator
/// Creates a generator with custom alphabet, size, and random function
/// Returns Result with generator function or NanoidError for invalid parameters
/// Usage: let gen = custom_random(alphabet, size, random_fn)?; let id = gen()?
pub fn custom_random(
alphabet : String,
size : Int,
random : (Int) -> Result[Array[Int], NanoidError],
) -> Result[() -> Result[String, NanoidError], NanoidError] {
match validate_parameters(alphabet, size) {
Valid => {
let alphabet_chars = string_to_chars(alphabet)
// Single-character alphabet: no randomness needed and no probe call is made.
// This is intentional — the probe-bypass is documented in the public API contract.
if alphabet_chars.length() == 1 {
return Ok(fn() -> Result[String, NanoidError] {
Ok(String::from_array(Array::make(size, alphabet_chars[0])))
})
}
match create_replay_random(random) {
Ok(replay_random) =>
Ok(fn() -> Result[String, NanoidError] {
generate_unchecked_with_alphabet_chars(
alphabet_chars, size, replay_random,
)
})
Err(e) => Err(e)
}
}
InvalidAlphabet(error) => Err(error)
InvalidSize(error) => Err(error)
}
}
///|
/// Convenience custom random function that returns empty string on error
/// Usage: let gen = custom_random_or_empty(alphabet, size, random_fn); let id = gen()
pub fn custom_random_or_empty(
alphabet : String,
size : Int,
random : (Int) -> Array[Int],
) -> () -> String {
let safe_random = fn(size : Int) -> Result[Array[Int], NanoidError] {
Ok(random(size))
}
match custom_random(alphabet, size, safe_random) {
Ok(gen) =>
fn() -> String {
match gen() {
Ok(id) => id
Err(_) => ""
}
}
Err(_) => fn() -> String { "" }
}
}
// ========== Character Sets & Alphabet Presets ==========
// Based on nanoid-dictionary (https://github.com/CyberAP/nanoid-dictionary)
///|
/// Numbers from 0 to 9
pub let numbers = "0123456789"
///|
/// Lowercase English letters
pub let lowercase = "abcdefghijklmnopqrstuvwxyz"
///|
/// Uppercase English letters
pub let uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
///|
/// Combination of lowercase, uppercase letters and numbers
/// Does not include any symbols or special characters
pub let alphanumeric = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
///|
/// Lowercase hexadecimal characters
pub let hex = "0123456789abcdef"
///|
/// Uppercase hexadecimal characters
pub let hex_upper = "0123456789ABCDEF"
///|
/// Numbers and English alphabet without lookalikes
/// Removes: 1, l, I, 0, O, o, u, v, 5, S, s, 2, Z
/// Complete set: 346789ABCDEFGHJKLMNPQRTUVWXYabcdefghijkmnpqrtwxyz
pub let nolookalikes = "346789ABCDEFGHJKLMNPQRTUVWXYabcdefghijkmnpqrtwxyz"
///|
/// Same as nolookalikes but with additional removed characters: 3, 4, x, X, V
/// Also removes vowels to protect from accidentally getting obscene words in generated strings
/// Complete set: 6789BCDFGHJKLMNPQRTWbcdfghjkmnpqrtwz
pub let nolookalikes_safe = "6789BCDFGHJKLMNPQRTWbcdfghjkmnpqrtwz"
///|
/// Base62 encoding alphabet (numbers, uppercase, lowercase)
/// Compatible with most base62 implementations
pub let base62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
///|
/// Base58 encoding alphabet (Bitcoin style - excludes 0, O, I, l)
/// Used in cryptocurrency and other applications to avoid character confusion
pub let base58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz"
///|
/// URL-safe characters that don't require encoding in most contexts
/// Excludes characters that might be problematic in URLs or file systems
/// Same as the default url_alphabet
pub let url_safe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-"
///|
/// Filename-safe characters for cross-platform compatibility
/// Safe for use in filenames on Windows, macOS, and Linux
pub let filename_safe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-"