Implementation of TypeID in Lua
1local Base32 = require("base32")
2local UUID7 = require("uuid7")
3
4local TypeID = {}
5TypeID.MAX_PREFIX_LENGTH = 63
6
7-- prefix string, suffix a Base32 string
8function TypeID.new(prefix, suffix)
9 if not prefix then
10 error("no prefix specified, though empty string would be valid")
11 end
12
13 -- spec: Valid prefixes match the following regex: ^([a-z]([a-z_]{0,61}[a-z])?)?$
14 -- Lua's patterns aren't expressive enough to paste that in, but these checks
15 -- match that and give more specific errors, to boot.
16 if #prefix > TypeID.MAX_PREFIX_LENGTH then
17 error("type prefix length cannot be greater than " .. TypeID.MAX_PREFIX_LENGTH)
18 end
19
20 if not prefix:match("^[a-z_]*$") then
21 error("type prefix must be lowercase ASCII characters")
22 end
23
24 if prefix:match("^_") or prefix:match("_$") then
25 error("type prefix cannot start or end with an underscore")
26 end
27
28 if #suffix ~= 26 then
29 error("UUID suffix must be 26 characters, is " .. #suffix)
30 end
31
32 if not suffix:match("^[0123456789abcdefghjkmnpqrstvwxyz]*$") then
33 error("UUID suffix must only use Base32 characters")
34 end
35
36 -- check if suffix starts with a 0-7 digit
37 local first_char = suffix:sub(1, 1)
38 if not first_char:match("[0-7]") then
39 error("UUID suffix must start with a 0-7 digit to avoid overflows")
40 end
41
42 local typeid = {
43 prefix = prefix,
44 suffix = suffix,
45
46 uuid = function(self)
47 return UUID7.to_string(Base32.decode(self.suffix))
48 end
49 }
50
51 setmetatable(typeid, {
52 __tostring = function(self)
53 if self.prefix == "" then
54 return self.suffix
55 else
56 return self.prefix .. "_" .. self.suffix
57 end
58 end
59 })
60
61 return typeid
62end
63
64-- create a TypeID with the given prefix, optional millisecond timestamp and seed
65-- (seed is probably only useful for tests)
66function TypeID.generate(prefix, timestamp, seed)
67 local uuid_bytes = UUID7.as_table(timestamp, seed)
68 local suffix = Base32.encode(uuid_bytes)
69 return TypeID.new(prefix, suffix)
70end
71
72-- parse a TypeID from a string like "prefix_01h455vb4pex5vsknk084sn02q"
73function TypeID.parse(str)
74 local prefix, suffix
75
76 local last_underscore_pos = str:match(".*_()") -- Find position after the last underscore
77 if not last_underscore_pos then
78 prefix = ""
79 suffix = str
80 else
81 prefix = str:sub(1, last_underscore_pos - 2) -- -2 because match returns position after underscore
82 suffix = str:sub(last_underscore_pos)
83
84 if prefix == "" then
85 error("prefix cannot be empty when there's a separator")
86 end
87 end
88
89 return TypeID.new(prefix, suffix)
90end
91
92-- create a TypeID from a prefix and a UUID string
93function TypeID.from_uuid_string(prefix, uuid_str)
94 local uuid_bytes = {}
95
96 -- parse groups from UUID string like "01893726-efee-7f02-8fbf-9f2b7bc2f910"
97 local uuid_template = "(%x%x%x%x%x%x%x%x)%-(%x%x%x%x)%-(%x%x%x%x)%-(%x%x%x%x)%-(%x%x%x%x%x%x%x%x%x%x%x%x)"
98 local g1, g2, g3, g4, g5 = uuid_str:match(uuid_template)
99
100 if not g1 then
101 error("invalid UUID format")
102 end
103
104 -- convert hex to bytes
105 local function hex_to_bytes(hex)
106 local bytes = {}
107 for i = 1, #hex, 2 do
108 local byte = tonumber(hex:sub(i, i+1), 16)
109 table.insert(bytes, byte)
110 end
111 return bytes
112 end
113
114 local bytes1 = hex_to_bytes(g1)
115 local bytes2 = hex_to_bytes(g2)
116 local bytes3 = hex_to_bytes(g3)
117 local bytes4 = hex_to_bytes(g4)
118 local bytes5 = hex_to_bytes(g5)
119
120 -- combine all bytes
121 for _, b in ipairs(bytes1) do table.insert(uuid_bytes, b) end
122 for _, b in ipairs(bytes2) do table.insert(uuid_bytes, b) end
123 for _, b in ipairs(bytes3) do table.insert(uuid_bytes, b) end
124 for _, b in ipairs(bytes4) do table.insert(uuid_bytes, b) end
125 for _, b in ipairs(bytes5) do table.insert(uuid_bytes, b) end
126
127 local suffix = Base32.encode(uuid_bytes)
128 return TypeID.new(prefix, suffix)
129end
130
131return TypeID