Skip to content

Commit

Permalink
feat: add typeid
Browse files Browse the repository at this point in the history
  • Loading branch information
okkdev authored and rvcas committed Jun 4, 2024
1 parent 1bb6182 commit 5a810ed
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 3 deletions.
98 changes: 98 additions & 0 deletions src/ids/typeid.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//// Module for generating TypeIDs.

import gleam/bit_array
import gleam/regex
import gleam/result
import gleam/string
import ids/utils
import ids/uuid

@internal
pub const alphabet = "0123456789abcdefghjkmnpqrstvwxyz"

/// Generate a TypeID using the supplied prefix. Prefix may be an empty string.
///
/// ### Usage
/// ```gleam
/// generate("user") // Ok("user_01hz7jxhgpfxh9m33bn41tcg9t")
/// generate("") // Ok("01hz7jv26zfnx9gpb0hvyc4pss")
/// ```
pub fn generate(prefix prefix: String) -> Result(String, String) {
use uuid <- result.try(uuid.generate_v7())
from_uuid(prefix: prefix, uuid: uuid)
}

/// Generate a TypeID using the supplied prefix and UUID. Prefix may be an empty string.
///
/// ### Usage
/// ```gleam
/// from_uuid("user", "018fcec0-b44b-7ce2-b187-2f08349beab9") // Ok("user_01hz7c1d2bfkhb31sf10t9qtns")
/// ```
pub fn from_uuid(
prefix prefix: String,
uuid uuid: String,
) -> Result(String, String) {
let assert Ok(re) = regex.from_string("^([a-z]([a-z_]{0,61}[a-z])?)?$")

case regex.check(re, prefix) {
True -> {
let p = case prefix {
"" -> ""
_ -> prefix <> "_"
}

use raw_uuid <- result.try(
uuid
|> bit_array.from_string
|> uuid.dump,
)

let id = utils.encode_base32(raw_uuid, alphabet)

Ok(p <> id)
}
False ->
Error(
"Error: Prefix must contain at most 63 characters and only lowercase alphabetic ASCII characters [a-z], or an underscore.",
)
}
}

/// Decode a TypeID into a tuple of #(prefix, uuid)
///
/// ### Usage
/// ```gleam
/// decode("user_01hz7c1d2bfkhb31sf10t9qtns") // Ok(#("user", "018fcec0-b44b-7ce2-b187-2f08349beab9"))
/// ```
pub fn decode(tid) -> Result(#(String, String), String) {
case tid |> string.reverse |> string.split_once(on: "_") {
Ok(#(xiffus, xiferp)) -> {
use uuid <- result.try(
xiffus
|> string.reverse
|> decode_suffix,
)
Ok(#(string.reverse(xiferp), uuid))
}
Error(Nil) -> {
use uuid <- result.try(decode_suffix(tid))
Ok(#("", uuid))
}
}
}

fn decode_suffix(suffix) -> Result(String, String) {
use raw_uuid <- result.try(
suffix
|> utils.decode_base32(alphabet)
|> result.replace_error("Error: Couldn't decode suffix."),
)

use uuid <- result.try(
raw_uuid
|> uuid.cast
|> result.replace_error("Error: Couldn't decode UUID."),
)

Ok(uuid)
}
8 changes: 5 additions & 3 deletions src/ids/uuid.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
////

import gleam/bit_array
import gleam/result
import gleam/erlang
import gleam/result

@external(erlang, "crypto", "strong_rand_bytes")
fn crypto_strong_rand_bytes(n: Int) -> BitArray
Expand Down Expand Up @@ -74,7 +74,8 @@ pub fn decode_v7(
})
}

fn cast(raw_uuid: BitArray) -> Result(String, String) {
@internal
pub fn cast(raw_uuid: BitArray) -> Result(String, String) {
case raw_uuid {
<<
a1:size(4),
Expand Down Expand Up @@ -157,7 +158,8 @@ fn cast(raw_uuid: BitArray) -> Result(String, String) {
}
}

fn dump(uuid: BitArray) -> Result(BitArray, String) {
@internal
pub fn dump(uuid: BitArray) -> Result(BitArray, String) {
case uuid {
<<
a1,
Expand Down
99 changes: 99 additions & 0 deletions test/ids/typeid_test.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import gleam/bit_array
import gleam/string
import gleeunit/should
import ids/typeid

pub fn gen_test() {
typeid.generate("test")
|> should.be_ok

typeid.generate("te_st")
|> should.be_ok

typeid.generate("123")
|> should.be_error

let assert Ok(id) = typeid.generate("test")

id
|> string.starts_with("test_")
|> should.be_true

id
|> string.length
|> should.equal(31)
}

pub fn gen_empty_prefix_test() {
let assert Ok(id) = typeid.generate("")

id
|> string.contains("_")
|> should.be_false

id
|> string.length
|> should.equal(26)
}

pub fn from_uuid_test() {
typeid.from_uuid("test", "wobble")
|> should.be_error

typeid.from_uuid("test", "018fcec0-b44b-7ce2-b187-2f08349beab9")
|> should.be_ok

let assert Ok(id) =
typeid.from_uuid("test", "018fcec0-b44b-7ce2-b187-2f08349beab9")

id
|> should.equal("test_01hz7c1d2bfkhb31sf10t9qtns")
}

pub fn decode_test() {
let assert Ok(id1) = typeid.generate("test")
let assert Ok(id2) = typeid.generate("")
let assert Ok(id3) =
typeid.from_uuid("test", "018fcec0-b44b-7ce2-b187-2f08349beab9")

id1
|> typeid.decode
|> should.be_ok

id2
|> typeid.decode
|> should.be_ok

""
|> typeid.decode
|> should.be_error

"lkjgijkldsa"
|> typeid.decode
|> should.be_error

let assert Ok(#(prefix1, suffix1)) = typeid.decode(id1)
let assert Ok(#(prefix2, _suffix2)) = typeid.decode(id2)
let assert Ok(#(_prefix3, suffix3)) = typeid.decode(id3)

prefix1
|> should.equal("test")

prefix2
|> should.equal("")

let assert <<
_:size(64),
45,
_:size(32),
45,
_:size(32),
45,
_:size(32),
45,
_:size(96),
>> = bit_array.from_string(suffix1)

suffix3
|> should.equal("018fcec0-b44b-7ce2-b187-2f08349beab9")
}

0 comments on commit 5a810ed

Please sign in to comment.