Implementation of TypeID in Lua
at main 3.8 kB view raw
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