nanoid

    A MoonBit port of the NanoID library

    id
    generator
    nanoid
    uuid
    random
    unique
    Download zip
    Author
    Version
    0.6.1
    License
    MIT
    Last updated
    8 hours ago
    Downloads
    53

    #MoonBit Nanoid

    MIT License

    A tiny, URL-friendly ID generator for MoonBit.

    This is a MoonBit port of the popular Nano ID JavaScript library, preserving the core algorithm and behavior while providing MoonBit-idiomatic Result-based error handling.

    #Features

    • Small & Fast: Minimal overhead, optimized for performance
    • URL-Safe: Generated IDs are safe for use in URLs, filenames, and databases
    • Customizable: Support for custom alphabets and ID sizes
    • Unicode Support: Full support for Unicode alphabets including emoji and CJK characters
    • Error Safe: Proper error handling with Result types instead of runtime panics
    • Type Safe: Full MoonBit type system support with Debug and Eq on error types
    • Zero Dependencies: No external dependencies beyond MoonBit core
    • OS-Backed Entropy on Native, JS, and LLVM: JS uses Web Crypto (crypto.getRandomValues) with a Node crypto.randomBytes fallback; native/llvm uses getrandom//dev/urandom, arc4random_buf, or BCryptGenRandom; wasm/wasm-gc falls back to a runtime-seeded ChaCha8 PRNG
    • No Shared RNG on Native, JS, and LLVM: Native, JS, and LLVM generation no longer shares mutable RNG state

    Note: MoonBit core still does not expose a standard system-entropy API. Native and llvm builds use OS entropy directly (getrandom//dev/urandom, arc4random_buf, or BCryptGenRandom), and JS builds use Web Crypto or Node crypto.randomBytes. WASM and wasm-gc keep the runtime-seeded ChaCha8 fallback so moon test --target all remains self-contained; for security-sensitive WASM IDs/tokens, pass a host-crypto-backed source via custom_random. Thread safety: The default global RNG used by the wasm fallback is not thread-safe.

    #Quick Start

    1. Add this module as a dependency to your MoonBit project.

      moon update moon add hustcer/nanoid

    2. Import hustcer/nanoid package where you need it.

      import { "hustcer/nanoid", }

    #Basic Usage

    // Generate a URL-safe ID with default length (21 characters)
    let id_result = @nanoid.nanoid()
    match id_result {
    Ok(id) => println(id) // => "rMf19KHCD5GQw0wzQnEKd"
    Err(e) => println("Error: \{e.to_string()}")
    }

    // Generate an ID with custom length
    let short_id_result = @nanoid.nanoid(size=10)
    match short_id_result {
    Ok(short_id) => println(short_id) // => "yue8fl8f9X"
    Err(e) => println("Error: \{e.to_string()}")
    }

    // For convenience, use the backward-compatible functions
    let id = @nanoid.nanoid_or_empty()
    println(id) // => "rMf19KHCD5GQw0wzQnEKd" or "" on error

    #Custom Alphabets

    // Create a generator with custom alphabet
    let hex_generator_result = @nanoid.custom_alphabet(@nanoid.hex, size=8)
    match hex_generator_result {
    Ok(hex_generator) => {
    match hex_generator() {
    Ok(hex_id) => println(hex_id) // => "a1b2c3d4"
    Err(e) => println("Generation error: \{e.to_string()}")
    }
    }
    Err(e) => println("Setup error: \{e.to_string()}")
    }

    // For convenience, use the backward-compatible functions
    let safe_generator = @nanoid.custom_alphabet_or_empty(@nanoid.nolookalikes, size=12)
    let safe_id = safe_generator()
    println(safe_id) // => "6B9CMnpqrt7w" or "" on error

    // Unicode alphabets are fully supported
    let emoji_gen = @nanoid.custom_alphabet("😀😁😂🤣😃😄", size=5)

    #API Reference

    #Core Functions

    #nanoid(size? : Int = 21) -> Result[String, NanoidError]

    Generates a URL-safe unique ID using the default alphabet.

    • size: Length of the generated ID (default: 21)
    • Returns: Ok(String) with the generated ID, or Err(NanoidError) for invalid parameters

    let id1 = @nanoid.nanoid() // Ok("...") with 21 characters
    let id2 = @nanoid.nanoid(size=10) // Ok("...") with 10 characters
    let id3 = @nanoid.nanoid(size=0) // Err(SizeTooSmall(0))

    #nanoid_or_empty(size? : Int = 21) -> String

    Convenience function that returns empty string on error (for backward compatibility).

    • size: Length of the generated ID (default: 21)
    • Returns: A random string ID, or empty string if invalid

    let id1 = @nanoid.nanoid_or_empty() // 21 characters or ""
    let id2 = @nanoid.nanoid_or_empty(size=10) // 10 characters or ""
    let id3 = @nanoid.nanoid_or_empty(size=0) // ""

    #custom_alphabet(alphabet : String, size? : Int = 21) -> Result[() -> Result[String, NanoidError], NanoidError]

    Creates a generator function with a custom alphabet.

    • alphabet: String containing unique characters to use (1-256 characters, no duplicates)
    • size: Default length for generated IDs (must be positive)
    • Returns: Ok(generator) or Err(NanoidError) for invalid parameters

    let generator_result = @nanoid.custom_alphabet("0123456789", size=8)
    match generator_result {
    Ok(generator) => {
    match generator() {
    Ok(numeric_id) => println(numeric_id) // => "12345678"
    Err(e) => println("Generation error: \{e.to_string()}")
    }
    }
    Err(e) => println("Setup error: \{e.to_string()}")
    }

    let bad_result = @nanoid.custom_alphabet("", size=8)
    // Returns Err(EmptyAlphabet)

    #custom_alphabet_or_empty(alphabet : String, size? : Int = 21) -> () -> String

    Convenience function that returns empty string on error (for backward compatibility).

    • alphabet: String containing unique characters to use
    • size: Default length for generated IDs
    • Returns: A generator function that returns string or empty string on error

    #custom_random(alphabet : String, size : Int, random : (Int) -> Result[Array[Int], NanoidError]) -> Result[() -> Result[String, NanoidError], NanoidError]

    Creates a generator with custom alphabet and random function.

    • alphabet: String containing unique characters to use (1-256 characters, no duplicates)
    • size: Length for generated IDs (must be positive)
    • random: Custom random byte generator function that returns Result; it is probed once during setup with size=1, and every call must return exactly the requested number of bytes in the range 0..255. For single-character alphabets, the random function is never called (the probe is also skipped).
    • Returns: Ok(generator) or Err(NanoidError) for invalid parameters or invalid custom-random behavior during setup

    Generation retries rejected byte values within a finite budget, with a minimum budget of 128 bytes to tolerate rejection streaks for short IDs. A source that keeps returning rejected values produces RandomGenerationError; source errors and invalid byte arrays still fail immediately.

    #custom_random_or_empty(alphabet : String, size : Int, random : (Int) -> Array[Int]) -> () -> String

    Convenience function that returns empty string on error (for backward compatibility).

    • alphabet: String containing unique characters to use (1-256 characters, no duplicates)
    • size: Length for generated IDs (must be positive)
    • random: Custom random byte generator function; it is validated once during setup with size=1, and every call should return exactly the requested number of bytes in the range 0..255. For single-character alphabets, the random function is never called (the probe is also skipped).
    • Returns: A generator function that returns string or empty string on setup or generation error

    #Error Handling

    The library uses MoonBit's Result type for proper error handling:

    pub(all) enum NanoidError {
    EmptyAlphabet // Alphabet cannot be empty
    OversizedAlphabet(Int) // Alphabet exceeds 256 characters
    DuplicateCharacter(Char, Int, Int) // Duplicate character found at positions
    SizeTooSmall(Int) // Size must be greater than 0
    SizeTooLarge(Int) // Size exceeds maximum allowed (1,000,000)
    RandomGenerationError(String) // Random number generation failed
    } derive(Debug, Eq)

    #Error Handling Examples

    // Handle errors explicitly
    match @nanoid.nanoid(size=-1) {
    Ok(id) => println("Generated: \{id}")
    Err(SizeTooSmall(size)) => println("Invalid size: \{size}")
    Err(e) => println("Other error: \{e.to_string()}")
    }

    // Detect duplicate characters in alphabet
    match @nanoid.custom_alphabet("ABCA", size=5) {
    Err(DuplicateCharacter(char, first, dup)) =>
    println("'\{char}' duplicated at \{dup}, first at \{first}")
    _ => ()
    }

    // Use convenience functions for backward compatibility
    let id = @nanoid.nanoid_or_empty(size=-1) // Returns "" on error

    // Propagate errors from a custom random source (e.g. an HSM that may fail)
    let rng = fn(size : Int) -> Result[Array[Int], @nanoid.NanoidError] {
    match read_from_hsm(size) {
    Ok(bytes) => Ok(bytes)
    Err(_) => Err(@nanoid.RandomGenerationError("HSM offline"))
    }
    }
    match @nanoid.custom_random(@nanoid.url_alphabet, 21, rng) {
    Ok(generator) => match generator() {
    Ok(id) => println(id)
    Err(@nanoid.RandomGenerationError(msg)) => println("RNG failure: \{msg}")
    Err(e) => println("Other error: \{e.to_string()}")
    }
    Err(e) => println("Setup failed: \{e.to_string()}")
    }

    #Predefined Alphabets

    Based on nanoid-dictionary, we provide several predefined character sets:

    #Basic Sets

    AlphabetCharactersDescription
    numbers0123456789Numbers only
    lowercaseabcdefghijklmnopqrstuvwxyzLowercase letters
    uppercaseABCDEFGHIJKLMNOPQRSTUVWXYZUppercase letters
    alphanumericA-Za-z0-9 (62 chars)Letters and numbers

    #Hexadecimal

    AlphabetCharactersDescription
    hex0123456789abcdefLowercase hexadecimal
    hex_upper0123456789ABCDEFUppercase hexadecimal

    #Special Purpose

    AlphabetCharactersDescription
    url_alphabetA-Za-z0-9_- (64 chars)Default URL-safe alphabet
    nolookalikes346789ABCD...xyz (49 chars)No confusing characters (removes 1,l,I,0,O,o,u,v,5,S,s,2,Z)
    nolookalikes_safe6789BCDF...twz (35 chars)No lookalikes + no vowels (safer for public IDs)
    base58123456789ABCD...xyz (58 chars)Bitcoin-style (excludes 0,O,I,l)
    base620-9A-Za-z (62 chars)Standard base62 encoding
    url_safeA-Za-z0-9_- (64 chars)Same character set as url_alphabet
    filename_safeA-Za-z0-9_- (64 chars)Same character set as url_alphabet, for filename contexts

    #Usage Examples

    // Use different alphabets for different purposes
    let uuid_like = @nanoid.custom_alphabet(@nanoid.hex, size=32)
    let readable = @nanoid.custom_alphabet(@nanoid.nolookalikes, size=8)
    let safe_public = @nanoid.custom_alphabet(@nanoid.nolookalikes_safe, size=6)
    let crypto_style = @nanoid.custom_alphabet(@nanoid.base58, size=16)

    #Testing

    Run the comprehensive test suite:

    moon test --target all

    The test suite includes:

    • Basic functionality and custom alphabet tests
    • Error handling and edge case tests (boundaries, Unicode, duplicate detection)
    • OS-backed default random generation and custom random validation tests
    • Unicode emoji and CJK character support tests

    #License

    MIT License - see LICENSE file for details.

    #Acknowledgments

    #Contributing

    Contributions are welcome! Please feel free to submit a Pull Request.

    #See Also

    NanoidError

    pub(all) enum NanoidError {
    EmptyAlphabet
    OversizedAlphabet(Int)
    DuplicateCharacter(Char, Int, Int)
    SizeTooSmall(Int)
    SizeTooLarge(Int)
    RandomGenerationError(String)
    } derive(Eq,
    Debug
    )

    Error types for nanoid operations

    NanoidError::to_string

    fn NanoidError::to_string(self : NanoidError) -> String

    Convert NanoidError to a human-readable string with helpful guidance

    alphanumeric

    let alphanumeric : String

    Combination of lowercase, uppercase letters and numbers Does not include any symbols or special characters

    base58

    let base58 : String

    Base58 encoding alphabet (Bitcoin style - excludes 0, O, I, l) Used in cryptocurrency and other applications to avoid character confusion

    base62

    let base62 : String

    Base62 encoding alphabet (numbers, uppercase, lowercase) Compatible with most base62 implementations

    custom_alphabet

    fn custom_alphabet(alphabet : String, size? : Int) -> Result[() -> Result[String, NanoidError], NanoidError]

    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()?

    custom_alphabet_or_empty

    fn custom_alphabet_or_empty(alphabet : String, size? : Int) -> (() -> String)

    Convenience custom alphabet function that returns empty string on error Usage: let gen = custom_alphabet_or_empty("abc123", size=8); let id = gen()

    custom_random

    fn custom_random(alphabet : String, size : Int, random : (Int) -> Result[Array[Int], NanoidError]) -> Result[() -> Result[String, NanoidError], NanoidError]

    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()?

    custom_random_or_empty

    fn custom_random_or_empty(alphabet : String, size : Int, random : (Int) -> Array[Int]) -> (() -> String)

    Convenience custom random function that returns empty string on error Usage: let gen = custom_random_or_empty(alphabet, size, random_fn); let id = gen()

    filename_safe

    let filename_safe : String

    Filename-safe characters for cross-platform compatibility Safe for use in filenames on Windows, macOS, and Linux

    hex

    let hex : String

    Lowercase hexadecimal characters

    hex_upper

    let hex_upper : String

    Uppercase hexadecimal characters

    lowercase

    let lowercase : String

    Lowercase English letters

    nanoid

    fn nanoid(size? : Int) -> Result[String, NanoidError]

    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

    nanoid_or_empty

    fn nanoid_or_empty(size? : Int) -> String

    Convenience function that returns empty string on error (for backward compatibility) Usage: nanoid_or_empty() generates 21-character ID or empty string on error

    nolookalikes

    let nolookalikes : String

    Numbers and English alphabet without lookalikes Removes: 1, l, I, 0, O, o, u, v, 5, S, s, 2, Z Complete set: 346789ABCDEFGHJKLMNPQRTUVWXYabcdefghijkmnpqrtwxyz

    nolookalikes_safe

    let nolookalikes_safe : String

    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

    numbers

    let numbers : String

    Numbers from 0 to 9

    uppercase

    let uppercase : String

    Uppercase English letters

    url_alphabet

    let url_alphabet : String

    Default URL-friendly alphabet (official nanoid order) Uses the same character order as official nanoid: A-Z, a-z, 0-9, underscore, hyphen

    url_safe

    let url_safe : String

    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