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 { <> -> 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) <> }) 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 } }