Current section
Files
Jump to
Current section
Files
src/ids/nanoid.gleam
//// A module for generating NanoIDs, i.e., tiny, secure, URL-friendly,
//// and unique string IDs.
////
import gleam/string
import gleam/bit_string
import gleam/float
import gleam/int
import gleam/list
/// The default alphabet used when generating NanoIDs.
pub const default_alphabet: BitString = <<
"_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ":utf8,
>>
/// The default size of the generated NanoIDs.
pub const default_size: Int = 21
@external(erlang, "crypto", "strong_rand_bytes")
fn crypto_strong_rand_bytes(length: Int) -> BitString
@external(erlang, "erlang", "bsl")
fn shift_left(n: Int, s: Int) -> Int
@external(erlang, "erlang", "band")
fn and(left: Int, right: Int) -> Int
@external(erlang, "binary", "bin_to_list")
fn bin_to_list(b: BitString) -> List(Int)
@external(erlang, "math", "log")
fn log(f: Float) -> Float
/// Generates a (random) NanoID. The NanoID produced by this function is
/// generated using a cryptographically secure random number generator.
///
/// ### Usage
/// ```gleam
/// import ids/nanoid
///
/// let assert Ok(id) = nanoid.generate()
/// ```
///
pub fn generate() -> String {
// TODO: When optional arguments with defaults becomes a thing in Gleam
// make it possble to pass an 'alphabet' and 'size'. For now just
// use hardcoded defaults...
let alphabet: BitString = default_alphabet
let assert Ok(alphabet_string) = bit_string.to_string(alphabet)
let alphabet_length: Int = string.length(alphabet_string)
let size: Int = default_size
let assert Ok(True) = check_nanoid_args(size, alphabet)
let mask = calculate_mask(alphabet_length)
let step = calculate_step(mask, size, alphabet_length)
let assert Ok(bitstr_nanoid) =
do_generate(size, alphabet, mask, step, <<"":utf8>>)
let assert Ok(str_nanoid) = bit_string.to_string(bitstr_nanoid)
str_nanoid
}
// Recursively generate a NanoID as long as the given size
// of the ID has not yet been reached
fn do_generate(
size: Int,
alphabet: BitString,
mask: Int,
step: Int,
acc: BitString,
) -> Result(BitString, String) {
case bit_string.byte_size(acc) >= size {
// Truncate the generated ID to the desired size
True -> {
let assert Ok(nanoid) = bit_string.slice(acc, 0, size)
nanoid
|> Ok
}
// The NanoID is not yet the desired size, so continue
// building up the ID
False ->
case generate_nanoid(step, alphabet, mask) {
Ok(partial_nanoid) ->
bit_string.concat([acc, partial_nanoid])
|> do_generate(size, alphabet, mask, step, _)
Error(error) ->
error
|> Error
}
}
}
fn generate_nanoid(
size: Int,
alphabet: BitString,
mask: Int,
) -> Result(BitString, String) {
case check_nanoid_args(size, alphabet) {
Ok(True) ->
size
|> random_bytes()
|> list.map(fn(x: Int) -> BitString {
case bit_string.slice(alphabet, and(x, mask), 1) {
Ok(nanoid) -> nanoid
_ -> <<"":utf8>>
}
})
|> bit_string.concat()
|> Ok
Error(error) ->
error
|> Error
}
}
fn check_nanoid_args(size: Int, alphabet: BitString) -> Result(Bool, String) {
case check_size(size) {
Ok(True) ->
case check_alphabet(alphabet) {
Ok(True) ->
True
|> Ok
Error(error) ->
error
|> Error
}
Error(error) ->
error
|> Error
}
}
fn check_size(size: Int) -> Result(Bool, String) {
case size > 0 {
True ->
True
|> Ok
False -> {
let error: String =
"Error: The specified ID size is too small. Increase the size of the ID."
error
|> Error
}
}
}
fn check_alphabet(alphabet: BitString) -> Result(Bool, String) {
case bit_string.byte_size(alphabet) > 1 {
True ->
True
|> Ok
False -> {
let error: String =
"Error: The specified alphabet size is too small. Increase the size of the alphabet."
error
|> Error
}
}
}
// Internal function for generating a list of cryptographically
// secure random bytes (represented by a list of ints)
fn random_bytes(size: Int) -> List(Int) {
crypto_strong_rand_bytes(size)
|> bin_to_list()
}
// Calculate a bitmask value that can be used to transform byte vaules
// into values that are closer to the size of the alphabet used. The
// bitmask value will be the closest `2^31 - 1` number, that exceeds
// the alphabet size. For example, the bitmask of the alphabet of size
// 30 is 31 (00011111)
fn calculate_mask(alphabet_length: Int) -> Int {
let v1 = log(int.to_float(alphabet_length - 1)) /. log(2.0)
let v2 = float.round(float.floor(v1))
shift_left(2, v2) - 1
}
// Calculate a step value that determines how many random bytes to
// generate. The number of random bytes is decided based on the ID
// 'size', 'bitmask' value, 'alphabet' size, and a number 1.6
// (using 1.6 gives the best performance according to benchmarks).
fn calculate_step(mask: Int, size: Int, alphabet_length: Int) -> Int {
let step: Float =
float.ceiling(
1.6 *. int.to_float(mask) *. int.to_float(size) /. int.to_float(
alphabet_length,
),
)
float.round(step)
}