nanoid

A MoonBit port of the NanoID library

id
generator
nanoid
uuid
random
unique
moon add hustcer/nanoid@0.6.0
Download zip
Author
Version
0.6.0
License
MIT
Last updated
3 months ago
Downloads
49
README

#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

#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