Current section
Files
Jump to
Current section
Files
src/typeid.gleam
import gleam/bool
import gleam/dynamic/decode
import gleam/list
import gleam/regexp
import gleam/result
import gleam/string
import youid/uuid
const default_typeid = TypeId(prefix: "", suffix: "")
const prefix_regex = "^([a-z]([a-z_]{0,61}[a-z])?)?$"
const suffix_regex = "^[01234567][0123456789abcdefghjkmnpqrstvwxyz]{25}$"
/// TypeId type with phantom type `a` to indicate the type of the TypeId.
pub opaque type TypeId(a) {
TypeId(prefix: String, suffix: String)
}
/// Create a new TypeId with the given prefix.
/// Prefix must be a string containing at most 63 characters and only lowercase
/// alphabetic ASCII characters [a-z], or an underscore.
/// The prefix may also be empty.
///
/// ### Usage
/// ```gleam
/// let assert Ok(id) = new("user")
/// ```
pub fn new(prefix prefix: String) -> Result(TypeId(a), String) {
from_uuid(prefix, uuid.v7_string())
}
/// Generate a TypeID using the supplied prefix and UUID.
///
/// ### Usage
/// ```gleam
/// let assert Ok(id) = from_uuid("user", "018fcec0-b44b-7ce2-b187-2f08349beab9")
/// ```
pub fn from_uuid(
prefix prefix: String,
uuid uuid: String,
) -> Result(TypeId(a), String) {
let assert Ok(re) = regexp.from_string(prefix_regex)
case regexp.check(re, prefix) {
True -> {
case uuid.from_string(uuid) {
Ok(uuid) -> {
let suffix =
uuid.to_bit_array(uuid)
|> encode_base32
TypeId(prefix:, suffix:)
|> Ok
}
Error(_) -> Error("Invalid UUID")
}
}
False ->
"Prefix must contain at most 63 characters and only lowercase alphabetic ASCII characters [a-z], or an underscore."
|> Error
}
}
/// Parse a TypeId from a string.
///
/// ### Usage
/// ```gleam
/// let assert Ok(id) = parse("user_01h455vb4pex5vsknk084sn02q")
/// ```
pub fn parse(raw_typeid: String) -> Result(TypeId(a), String) {
let assert Ok(rs) = regexp.from_string(suffix_regex)
case prefix_suffix(raw_typeid) {
Ok(#(prefix, suffix)) if prefix != "" -> {
let assert Ok(rp) = regexp.from_string(prefix_regex)
use <- bool.guard(!regexp.check(rp, prefix), Error("Prefix is malformed"))
use <- bool.guard(!regexp.check(rs, suffix), Error("Suffix is malformed"))
TypeId(prefix:, suffix:)
|> Ok
}
_ -> {
use <- bool.guard(
!regexp.check(rs, raw_typeid),
Error("Suffix is malformed"),
)
TypeId(prefix: "", suffix: raw_typeid)
|> Ok
}
}
}
/// TypeId decoder for use in Gleam's `decode` module.
/// When decoding the type(prefix) is known, which is why this function takes
/// the prefix as an argument to verify the prefix of the TypeId.
///
/// ### Usage
/// ```gleam
/// let decoder = {
/// use id <- decode.field("id", typeid.decode("user"))
/// use name <- decode.field("name", decode.string)
/// decode.success(User(id:, name:))
/// }
/// ```
pub fn decode(prefix kind: String) -> decode.Decoder(TypeId(a)) {
decode.string
|> decode.then(fn(id) {
case prefix_suffix(id), parse(id) {
Ok(#(prefix, _suffix)), Ok(typeid) if prefix == kind ->
decode.success(typeid)
_, _ -> decode.failure(default_typeid, "TypeId(a)")
}
})
}
/// Returns the UUID string representation of the TypeId suffix.
///
/// ### Usage
/// ```gleam
/// let assert Ok(id) = new("user")
/// uuid(id) // "0110c853-1d09-52d8-d73e-1194e95b5f19"
/// ```
pub fn uuid(typeid: TypeId(a)) -> String {
let assert Ok(bits) = decode_base32(typeid.suffix)
let assert Ok(uuid) = uuid.from_bit_array(bits)
uuid.to_string(uuid)
}
/// Returns the prefix of the TypeId.
///
/// ### Usage
/// ```gleam
/// let assert Ok(id) = new("user")
/// prefix(id) // "user"
/// ```
pub fn prefix(typeid: TypeId(a)) -> String {
typeid.prefix
}
/// Returns the suffix of the TypeId.
///
/// ### Usage
/// ```gleam
/// let assert Ok(id) = new("user")
/// suffix(id) // "01h455vb4pex5vsknk084sn02q"
/// ```
pub fn suffix(typeid: TypeId(a)) -> String {
typeid.suffix
}
/// Returns the string representation of a TypeId.
///
/// ### Usage
/// ```gleam
/// let assert Ok(id) = new("user")
/// to_string(id) // "user_01h455vb4pex5vsknk084sn02q"
///
/// let assert Ok(id) = new("")
/// to_string(id) // "01h455vb4pex5vsknk084sn02q"
/// ```
pub fn to_string(typeid: TypeId(a)) -> String {
case typeid.prefix {
"" -> typeid.suffix
_ -> typeid.prefix <> "_" <> typeid.suffix
}
}
fn prefix_suffix(raw_typeid: String) -> Result(#(String, String), Nil) {
string.reverse(raw_typeid)
|> string.split_once(on: "_")
|> result.try(fn(sp) {
let #(xiffus, xiferp) = sp
let prefix = string.reverse(xiferp)
let suffix = string.reverse(xiffus)
Ok(#(prefix, suffix))
})
}
fn encode_base32(bits: BitArray) -> String {
// pad 2 bits because we only supply 128 bits of data but need 130 bits for encoding
encode_bits_base32(<<0:size(2), bits:bits-size(128)>>, "")
}
/// Recursively grabs 5 bits and uses them as index in the alphabet and concatinate them to a string.
fn encode_bits_base32(binary: BitArray, acc: String) -> String {
case binary {
<<index:unsigned-size(5), rest:bits>> ->
encode_bits_base32(rest, acc <> index_to_alphabet(index))
_ -> acc
}
}
fn decode_base32(enc_suffix: String) -> Result(BitArray, Nil) {
let bits =
string.to_graphemes(enc_suffix)
|> list.fold(<<>>, fn(acc, c) {
let index = alphabet_to_index(c)
<<acc:bits, index:5>>
})
case bits {
<<0:size(2), res:bits-size(128)>> -> Ok(res)
_ -> Error(Nil)
}
}
fn index_to_alphabet(index: Int) -> String {
case index {
0 -> "0"
1 -> "1"
2 -> "2"
3 -> "3"
4 -> "4"
5 -> "5"
6 -> "6"
7 -> "7"
8 -> "8"
9 -> "9"
10 -> "a"
11 -> "b"
12 -> "c"
13 -> "d"
14 -> "e"
15 -> "f"
16 -> "g"
17 -> "h"
18 -> "j"
19 -> "k"
20 -> "m"
21 -> "n"
22 -> "p"
23 -> "q"
24 -> "r"
25 -> "s"
26 -> "t"
27 -> "v"
28 -> "w"
29 -> "x"
30 -> "y"
31 -> "z"
_ -> "0"
}
}
fn alphabet_to_index(char: String) -> Int {
case char {
"0" -> 0
"1" -> 1
"2" -> 2
"3" -> 3
"4" -> 4
"5" -> 5
"6" -> 6
"7" -> 7
"8" -> 8
"9" -> 9
"a" -> 10
"b" -> 11
"c" -> 12
"d" -> 13
"e" -> 14
"f" -> 15
"g" -> 16
"h" -> 17
"j" -> 18
"k" -> 19
"m" -> 20
"n" -> 21
"p" -> 22
"q" -> 23
"r" -> 24
"s" -> 25
"t" -> 26
"v" -> 27
"w" -> 28
"x" -> 29
"y" -> 30
"z" -> 31
_ -> 0
}
}