typeid 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/typeid/uuid/base32.rb +107 -0
- data/lib/typeid/uuid.rb +59 -0
- data/lib/typeid/version.rb +3 -0
- data/lib/typeid.rb +82 -0
- metadata +89 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9fd64508061798a1cd16f694ee4f865e35987096c1c9b730ed7979a0c6612196
|
4
|
+
data.tar.gz: 4e1e14a858b81f912145dca7824c77f54ac88414e917435d8854607d3a676bbe
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 67b7c8af3ed8ff21c2b34c3d28b46081b09678929cf1e7137df00945bb87a934270f326caadb5027cbb4408733cb36c7aeacbfc83ff03e2307d9d01371fb149f
|
7
|
+
data.tar.gz: ce574ff07880c53102cd72290d7b6227a6708487cefa4756e7ec6cdb02ff73d4d68a3607bc25da0a74aed8b82c7b68ac845faee804cc6024c344a7bb9c6655e0
|
@@ -0,0 +1,107 @@
|
|
1
|
+
class TypeID < String
|
2
|
+
class UUID < String
|
3
|
+
module Base32
|
4
|
+
ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz".freeze
|
5
|
+
|
6
|
+
DEC = [
|
7
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
8
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
9
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
10
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
11
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x01,
|
12
|
+
0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xFF, 0xFF,
|
13
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
14
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
15
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
16
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0A, 0x0B, 0x0C,
|
17
|
+
0x0D, 0x0E, 0x0F, 0x10, 0x11, 0xFF, 0x12, 0x13, 0xFF, 0x14,
|
18
|
+
0x15, 0xFF, 0x16, 0x17, 0x18, 0x19, 0x1A, 0xFF, 0x1B, 0x1C,
|
19
|
+
0x1D, 0x1E, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
20
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
21
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
22
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
23
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
24
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
25
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
26
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
27
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
28
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
29
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
30
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
31
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
32
|
+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
33
|
+
].freeze
|
34
|
+
|
35
|
+
# @param bytes [Array<Integer>]
|
36
|
+
# @return [String]
|
37
|
+
def self.encode(bytes)
|
38
|
+
encoded = Array.new(26, 0)
|
39
|
+
|
40
|
+
# 10 byte timestamp
|
41
|
+
encoded[0] = ALPHABET[(bytes[0]&224)>>5]
|
42
|
+
encoded[1] = ALPHABET[bytes[0]&31]
|
43
|
+
encoded[2] = ALPHABET[(bytes[1]&248)>>3]
|
44
|
+
encoded[3] = ALPHABET[((bytes[1]&7)<<2)|((bytes[2]&192)>>6)]
|
45
|
+
encoded[4] = ALPHABET[(bytes[2]&62)>>1]
|
46
|
+
encoded[5] = ALPHABET[((bytes[2]&1)<<4)|((bytes[3]&240)>>4)]
|
47
|
+
encoded[6] = ALPHABET[((bytes[3]&15)<<1)|((bytes[4]&128)>>7)]
|
48
|
+
encoded[7] = ALPHABET[(bytes[4]&124)>>2]
|
49
|
+
encoded[8] = ALPHABET[((bytes[4]&3)<<3)|((bytes[5]&224)>>5)]
|
50
|
+
encoded[9] = ALPHABET[bytes[5]&31]
|
51
|
+
|
52
|
+
# 16 bytes of entropy
|
53
|
+
encoded[10] = ALPHABET[(bytes[6]&248)>>3]
|
54
|
+
encoded[11] = ALPHABET[((bytes[6]&7)<<2)|((bytes[7]&192)>>6)]
|
55
|
+
encoded[12] = ALPHABET[(bytes[7]&62)>>1]
|
56
|
+
encoded[13] = ALPHABET[((bytes[7]&1)<<4)|((bytes[8]&240)>>4)]
|
57
|
+
encoded[14] = ALPHABET[((bytes[8]&15)<<1)|((bytes[9]&128)>>7)]
|
58
|
+
encoded[15] = ALPHABET[(bytes[9]&124)>>2]
|
59
|
+
encoded[16] = ALPHABET[((bytes[9]&3)<<3)|((bytes[10]&224)>>5)]
|
60
|
+
encoded[17] = ALPHABET[bytes[10]&31]
|
61
|
+
encoded[18] = ALPHABET[(bytes[11]&248)>>3]
|
62
|
+
encoded[19] = ALPHABET[((bytes[11]&7)<<2)|((bytes[12]&192)>>6)]
|
63
|
+
encoded[20] = ALPHABET[(bytes[12]&62)>>1]
|
64
|
+
encoded[21] = ALPHABET[((bytes[12]&1)<<4)|((bytes[13]&240)>>4)]
|
65
|
+
encoded[22] = ALPHABET[((bytes[13]&15)<<1)|((bytes[14]&128)>>7)]
|
66
|
+
encoded[23] = ALPHABET[(bytes[14]&124)>>2]
|
67
|
+
encoded[24] = ALPHABET[((bytes[14]&3)<<3)|((bytes[15]&224)>>5)]
|
68
|
+
encoded[25] = ALPHABET[bytes[15]&31]
|
69
|
+
|
70
|
+
encoded.join
|
71
|
+
end
|
72
|
+
|
73
|
+
# @param string [String]
|
74
|
+
# @return [Array<Integer>]
|
75
|
+
def self.decode(string)
|
76
|
+
bytes = string.bytes
|
77
|
+
|
78
|
+
raise "invalid length" unless bytes.length == 26
|
79
|
+
raise "invalid base32 character" if bytes.any? { |byte| DEC[byte] == 0xFF }
|
80
|
+
|
81
|
+
output = Array.new(16, 0)
|
82
|
+
|
83
|
+
# 6 bytes timestamp (48 bits)
|
84
|
+
output[0] = ((DEC[bytes[0]] << 5) | DEC[bytes[1]]) & 0xFF
|
85
|
+
output[1] = ((DEC[bytes[2]] << 3) | (DEC[bytes[3]] >> 2)) & 0xFF
|
86
|
+
output[2] = ((DEC[bytes[3]] << 6) | (DEC[bytes[4]] << 1) | (DEC[bytes[5]] >> 4)) & 0xFF
|
87
|
+
output[3] = ((DEC[bytes[5]] << 4) | (DEC[bytes[6]] >> 1)) & 0xFF
|
88
|
+
output[4] = ((DEC[bytes[6]] << 7) | (DEC[bytes[7]] << 2) | (DEC[bytes[8]] >> 3)) & 0xFF
|
89
|
+
output[5] = ((DEC[bytes[8]] << 5) | DEC[bytes[9]]) & 0xFF
|
90
|
+
|
91
|
+
# 10 bytes of entropy (80 bits)
|
92
|
+
output[6] = ((DEC[bytes[10]] << 3) | (DEC[bytes[11]] >> 2)) & 0xFF
|
93
|
+
output[7] = ((DEC[bytes[11]] << 6) | (DEC[bytes[12]] << 1) | (DEC[bytes[13]] >> 4)) & 0xFF
|
94
|
+
output[8] = ((DEC[bytes[13]] << 4) | (DEC[bytes[14]] >> 1)) & 0xFF
|
95
|
+
output[9] = ((DEC[bytes[14]] << 7) | (DEC[bytes[15]] << 2) | (DEC[bytes[16]] >> 3)) & 0xFF
|
96
|
+
output[10] = ((DEC[bytes[16]] << 5) | DEC[bytes[17]]) & 0xFF
|
97
|
+
output[11] = ((DEC[bytes[18]] << 3) | DEC[bytes[19]]>>2) & 0xFF
|
98
|
+
output[12] = ((DEC[bytes[19]] << 6) | (DEC[bytes[20]] << 1) | (DEC[bytes[21]] >> 4)) & 0xFF
|
99
|
+
output[13] = ((DEC[bytes[21]] << 4) | (DEC[bytes[22]] >> 1)) & 0xFF
|
100
|
+
output[14] = ((DEC[bytes[22]] << 7) | (DEC[bytes[23]] << 2) | (DEC[bytes[24]] >> 3)) & 0xFF
|
101
|
+
output[15] = ((DEC[bytes[24]] << 5) | DEC[bytes[25]]) & 0xFF
|
102
|
+
|
103
|
+
output
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
data/lib/typeid/uuid.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require "uuid7"
|
2
|
+
require_relative "./uuid/base32.rb"
|
3
|
+
|
4
|
+
class TypeID < String
|
5
|
+
class UUID < String
|
6
|
+
attr_reader :bytes
|
7
|
+
|
8
|
+
# @param timestamp [Integer]
|
9
|
+
# @return [TypeID::UUID]
|
10
|
+
def self.generate(timestamp: Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond))
|
11
|
+
from_string(UUID7.generate(timestamp: timestamp))
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param string [String]
|
15
|
+
# @return [TypeID::UUID]
|
16
|
+
def self.from_base32(string)
|
17
|
+
new(TypeID::UUID::Base32.decode(string))
|
18
|
+
end
|
19
|
+
|
20
|
+
# @param string [String]
|
21
|
+
# @return [TypeID::UUID]
|
22
|
+
def self.from_string(string)
|
23
|
+
bytes = string
|
24
|
+
.tr("-", "")
|
25
|
+
.chars
|
26
|
+
.each_slice(2)
|
27
|
+
.map { |pair| pair.join.to_i(16) }
|
28
|
+
|
29
|
+
new(bytes)
|
30
|
+
end
|
31
|
+
|
32
|
+
# @param bytes [Array<Integer>]
|
33
|
+
def initialize(bytes)
|
34
|
+
@bytes = bytes
|
35
|
+
|
36
|
+
super(string)
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [String]
|
40
|
+
def base32
|
41
|
+
TypeID::UUID::Base32.encode(bytes)
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [String]
|
45
|
+
def inspect
|
46
|
+
"#<#{self.class.name} #{to_s}>"
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# @return [String]
|
52
|
+
def string
|
53
|
+
bytes
|
54
|
+
.map
|
55
|
+
.with_index { |byte, index| ([4, 6, 8, 10].include?(index) ? "-%02x" : "%02x") % byte }
|
56
|
+
.join
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/typeid.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require_relative "./typeid/uuid.rb"
|
2
|
+
|
3
|
+
class TypeID < String
|
4
|
+
SUFFIX_LENGTH = 26
|
5
|
+
MAX_PREFIX_LENGTH = 63
|
6
|
+
|
7
|
+
class Error < StandardError; end
|
8
|
+
|
9
|
+
attr_reader :prefix
|
10
|
+
attr_reader :suffix
|
11
|
+
alias type prefix
|
12
|
+
|
13
|
+
# @param string [String]
|
14
|
+
# @return [TypeID]
|
15
|
+
def self.from_string(string)
|
16
|
+
case string.split("_")
|
17
|
+
in [suffix] then from("", suffix)
|
18
|
+
in [prefix, suffix]
|
19
|
+
raise Error, "prefix cannot be empty when there's a separator" if prefix.empty?
|
20
|
+
|
21
|
+
from(prefix, suffix)
|
22
|
+
else raise Error, "invalid typeid: #{string}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# @param prefix [String]
|
27
|
+
# @param uuid [String]
|
28
|
+
# @return [TypeID]
|
29
|
+
def self.from_uuid(prefix, uuid)
|
30
|
+
from(prefix, TypeID::UUID.from_string(uuid).base32)
|
31
|
+
end
|
32
|
+
|
33
|
+
# @param prefix [String]
|
34
|
+
# @param suffix [String]
|
35
|
+
# @return [TypeID]
|
36
|
+
def self.from(prefix, suffix)
|
37
|
+
new(prefix, suffix: suffix)
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [TypeID]
|
41
|
+
def self.nil
|
42
|
+
from("", "0" * SUFFIX_LENGTH)
|
43
|
+
end
|
44
|
+
|
45
|
+
# @param prefix [String]
|
46
|
+
# @param timestamp [Integer]
|
47
|
+
# @param suffix [String]
|
48
|
+
def initialize(
|
49
|
+
prefix,
|
50
|
+
timestamp: Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond),
|
51
|
+
suffix: TypeID::UUID.generate(timestamp: timestamp).base32
|
52
|
+
)
|
53
|
+
raise Error, "prefix length cannot be greater than #{MAX_PREFIX_LENGTH}" if prefix.length > MAX_PREFIX_LENGTH
|
54
|
+
raise Error, "prefix must be lowercase ASCII characters" unless prefix.match?(/^[a-z]*$/)
|
55
|
+
raise Error, "suffix must be #{SUFFIX_LENGTH} characters" unless suffix.length == SUFFIX_LENGTH
|
56
|
+
raise Error, "suffix must only contain the letters in '#{TypeID::UUID::Base32::ALPHABET}'" unless suffix.chars.all? { |char| TypeID::UUID::Base32::ALPHABET.include?(char) }
|
57
|
+
|
58
|
+
@prefix = prefix
|
59
|
+
@suffix = suffix
|
60
|
+
|
61
|
+
super(string)
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return [TypeID::UUID]
|
65
|
+
def uuid
|
66
|
+
TypeID::UUID.from_base32(suffix)
|
67
|
+
end
|
68
|
+
|
69
|
+
# @return [String]
|
70
|
+
def inspect
|
71
|
+
"#<#{self.class.name} #{self}>"
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
# @return [String]
|
77
|
+
def string
|
78
|
+
return suffix if prefix.empty?
|
79
|
+
|
80
|
+
"#{prefix}_#{suffix}"
|
81
|
+
end
|
82
|
+
end
|
metadata
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: typeid
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andrew Booth
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-06-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: uuid7
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.2.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.2.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.12'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.12'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pry
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.14.2
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.14.2
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
executables: []
|
58
|
+
extensions: []
|
59
|
+
extra_rdoc_files: []
|
60
|
+
files:
|
61
|
+
- lib/typeid.rb
|
62
|
+
- lib/typeid/uuid.rb
|
63
|
+
- lib/typeid/uuid/base32.rb
|
64
|
+
- lib/typeid/version.rb
|
65
|
+
homepage: https://github.com/broothie/typeid-ruby
|
66
|
+
licenses:
|
67
|
+
- Apache
|
68
|
+
metadata:
|
69
|
+
source_code_uri: https://github.com/broothie/typeid-ruby
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options: []
|
72
|
+
require_paths:
|
73
|
+
- lib
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: 3.0.0
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
requirements: []
|
85
|
+
rubygems_version: 3.2.3
|
86
|
+
signing_key:
|
87
|
+
specification_version: 4
|
88
|
+
summary: A type-safe, K-sortable, globally unique identifier inspired by Stripe IDs
|
89
|
+
test_files: []
|