Packages

A Gleam implementation of CUID2, the secure, collision-resistant ids optimized for horizontal scaling and performance.

Current section

Files

Jump to
glecuid src glecuid cuid2.gleam
Raw

src/glecuid/cuid2.gleam

import gleam/float
import gleam/int
import gleam/option.{type Option, None, Some}
import gleam/pair
import gleam/regexp
import gleam/string
import gleam/time/timestamp
import glecuid/internals/util
// ---- Types ---------------------------------------------
/// Type to represent CUIDv2 generator configuration.
///
pub opaque type Generator {
Generator(
counter: Option(fn() -> Int),
fingerprint: Option(String),
length: Int,
randomizer: fn() -> Float,
)
}
// ---- Constants -----------------------------------------
/// Constant representing CUIDv2 default length.
///
pub const default_length = 24
/// Constant representing a default `Generator`.
///
const default_generator = Generator(
counter: None,
fingerprint: None,
length: default_length,
randomizer: float.random,
)
const initial_count_max = 476_782_367
// ---- Main functions ------------------------------------
/// Generates a CUIDv2 using the default generator.
///
/// ## Examples
///
/// ```gleam
/// cuid2.create_id()
/// // -> "avu4793cnw6ljhov1s7Oxidg"
/// ```
///
pub fn create_id() -> String {
generate(default_generator)
}
/// Generates a CUIDv2 using custom generator.
///
/// ## Examples
///
/// ```gleam
/// cuid2.new()
/// |> cuid2.with_length(10)
/// |> cuid2.with_fingerprint("my_machine")
/// |> cuid2.with_counter(fn() { int.random(100) })
/// |> cuid2.with_randomizer(fn() { 0.5 })
/// |> cuid2.generate()
/// // -> "av77nekw5e"
/// ```
///
pub fn generate(g: Generator) -> String {
let Generator(counter, fingerprint, length, randomizer) = g
let first_letter = util.random_letter(randomizer)
let time = {
timestamp.system_time()
|> timestamp.to_unix_seconds_and_nanoseconds()
|> pair.first()
|> int.to_base36()
}
let count = {
case counter {
Some(x) -> x()
None ->
util.random_int(randomizer, initial_count_max)
|> util.bump_or_initialize_counter()
}
|> int.to_base36()
}
let fingerprint = case fingerprint {
Some(x) -> x
None -> util.create_fingerprint(randomizer)
}
let salt = util.create_entropy(randomizer, length)
first_letter
<> { time <> salt <> count <> fingerprint }
|> util.hash()
|> string.slice(1, length - 1)
|> string.lowercase()
}
/// Determines whether or not the string is a valid CUIDv2.
///
pub fn is_cuid(input: String) -> Bool {
let length = input |> string.length()
let assert Ok(regex) = regexp.from_string("^[a-z][0-9a-z]+$")
case regexp.check(regex, input) {
True if 2 <= length && length <= util.big_length -> True
_ -> False
}
}
// ---- Configs functions ---------------------------------
/// Creates a `Generator` with default values to be customized.
///
pub fn new() -> Generator {
default_generator
}
/// Sets a custom counter to be used by the `Generator`.
///
/// Counter is a function that returns an incrementing `Int`
/// every time it is called.
///
pub fn with_counter(g: Generator, counter: fn() -> Int) -> Generator {
Generator(..g, counter: counter |> Some())
}
/// Sets a random counter to be used by the `Generator`.
///
/// This counter returns a random `Int` instead of an incrementing value.
///
pub fn with_random_counter(g: Generator) -> Generator {
Generator(..g, counter: Some(fn() { int.random(util.big_length) }))
}
/// Sets a custom fingerprint to be used by the `Generator`.
///
/// Fingerprint is a unique `String` to help prevent collision
/// when generating ids in a distributed system.
///
pub fn with_fingerprint(g: Generator, fingerprint: String) -> Generator {
Generator(..g, fingerprint: fingerprint |> Some())
}
/// Sets the length of the ids generated by the `Generator`.
///
pub fn with_length(g: Generator, length: Int) -> Generator {
Generator(..g, length:)
}
/// Sets a custom randomizer to be used by the `Generator`.
///
/// Randomizer is a function that returns a random `Float`
/// between zero (inclusive) and one (exclusive).
///
pub fn with_randomizer(g: Generator, randomizer: fn() -> Float) -> Generator {
Generator(..g, randomizer:)
}