typeid 0.1.9 → 0.2.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 +4 -4
- data/lib/typeid/uuid/base32.rb +53 -33
- data/lib/typeid/uuid.rb +22 -5
- data/lib/typeid/version.rb +1 -1
- data/lib/typeid.rb +44 -9
- metadata +22 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 283674a725da5d9ffa40b5bbc5f0c2a55020eea487f58d6fa6662ac903e51fa7
|
4
|
+
data.tar.gz: 673a27c6e7cd8c29fb0484564b7ae2eda168d49c386dcfc381fe22d4cdb3e3b5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a447c767d7dd80fe7b2c555941677fe163508a6ee0c090e20f03475d97399b0418e1184ab159999eebcd5f6e2d055f806d70274166cbae7ca3859f7a0242daf4
|
7
|
+
data.tar.gz: '08fb41867b5af9809cf36635383947972ca7f16c07c7c6df265dff27180aebee3db26dd6ae423775dd45acc49f0e7f374025fec6feed3009780d1944dce32927'
|
data/lib/typeid/uuid/base32.rb
CHANGED
@@ -1,8 +1,19 @@
|
|
1
1
|
class TypeID < String
|
2
2
|
class UUID < String
|
3
|
+
|
4
|
+
# Provides utilities for encoding and decoding UUIDs to and from base32.
|
5
|
+
# Based on https://github.com/jetpack-io/typeid-go/blob/341e2b135e0609db272e6400f2f551487725824a/base32/base32.go.
|
3
6
|
module Base32
|
7
|
+
DECODED_BYTE_ARRAY_LENGTH = 16
|
8
|
+
ENCODED_STRING_LENGTH = 26
|
9
|
+
|
10
|
+
class Error < StandardError; end
|
11
|
+
|
12
|
+
# Crockford's Base32 alphabet.
|
4
13
|
ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz".freeze
|
5
14
|
|
15
|
+
# Byte to index table for O(1) lookups when unmarshaling.
|
16
|
+
# We use 0xFF as sentinel value for invalid indexes.
|
6
17
|
DEC = [
|
7
18
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
8
19
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
@@ -32,53 +43,62 @@ class TypeID < String
|
|
32
43
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
33
44
|
].freeze
|
34
45
|
|
35
|
-
#
|
46
|
+
# Encodes a size 16 byte +Array+ into a size 26 +String+.
|
47
|
+
# Based on https://github.com/jetpack-io/typeid-go/blob/341e2b135e0609db272e6400f2f551487725824a/base32/base32.go#L14.
|
48
|
+
#
|
49
|
+
# @param bytes [Array<Integer>] size 16 byte array
|
36
50
|
# @return [String]
|
37
51
|
def self.encode(bytes)
|
38
|
-
|
52
|
+
raise Error, "invalid bytes size" unless bytes.size == DECODED_BYTE_ARRAY_LENGTH
|
53
|
+
|
54
|
+
encoded = Array.new(ENCODED_STRING_LENGTH, 0)
|
39
55
|
|
40
56
|
# 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]
|
57
|
+
encoded[0] = ALPHABET[(bytes[0] & 224) >> 5]
|
58
|
+
encoded[1] = ALPHABET[bytes[0] & 31]
|
59
|
+
encoded[2] = ALPHABET[(bytes[1] & 248) >> 3]
|
60
|
+
encoded[3] = ALPHABET[((bytes[1] & 7) << 2) | ((bytes[2] & 192) >> 6)]
|
61
|
+
encoded[4] = ALPHABET[(bytes[2] & 62) >> 1]
|
62
|
+
encoded[5] = ALPHABET[((bytes[2] & 1) << 4) | ((bytes[3] & 240) >> 4)]
|
63
|
+
encoded[6] = ALPHABET[((bytes[3] & 15) << 1) | ((bytes[4] & 128) >> 7)]
|
64
|
+
encoded[7] = ALPHABET[(bytes[4] & 124) >> 2]
|
65
|
+
encoded[8] = ALPHABET[((bytes[4] & 3) << 3) | ((bytes[5] & 224) >> 5)]
|
66
|
+
encoded[9] = ALPHABET[bytes[5] & 31]
|
51
67
|
|
52
68
|
# 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
|
+
encoded[10] = ALPHABET[(bytes[6] & 248) >> 3]
|
70
|
+
encoded[11] = ALPHABET[((bytes[6] & 7) << 2) | ((bytes[7] & 192) >> 6)]
|
71
|
+
encoded[12] = ALPHABET[(bytes[7] & 62) >> 1]
|
72
|
+
encoded[13] = ALPHABET[((bytes[7] & 1) << 4) | ((bytes[8] & 240) >> 4)]
|
73
|
+
encoded[14] = ALPHABET[((bytes[8] & 15) << 1) | ((bytes[9] & 128) >> 7)]
|
74
|
+
encoded[15] = ALPHABET[(bytes[9] & 124) >> 2]
|
75
|
+
encoded[16] = ALPHABET[((bytes[9] & 3) << 3) | ((bytes[10] & 224) >> 5)]
|
76
|
+
encoded[17] = ALPHABET[bytes[10] & 31]
|
77
|
+
encoded[18] = ALPHABET[(bytes[11] & 248) >> 3]
|
78
|
+
encoded[19] = ALPHABET[((bytes[11] & 7) << 2) | ((bytes[12] & 192) >> 6)]
|
79
|
+
encoded[20] = ALPHABET[(bytes[12] & 62) >> 1]
|
80
|
+
encoded[21] = ALPHABET[((bytes[12] & 1) << 4) | ((bytes[13] & 240) >> 4)]
|
81
|
+
encoded[22] = ALPHABET[((bytes[13] & 15) << 1) | ((bytes[14] & 128) >> 7)]
|
82
|
+
encoded[23] = ALPHABET[(bytes[14] & 124) >> 2]
|
83
|
+
encoded[24] = ALPHABET[((bytes[14] & 3) << 3) | ((bytes[15] & 224) >> 5)]
|
84
|
+
encoded[25] = ALPHABET[bytes[15] & 31]
|
69
85
|
|
70
86
|
encoded.join
|
71
87
|
end
|
72
88
|
|
73
|
-
#
|
89
|
+
# Decodes a size 26 +String+ into a size 16 byte +Array+.
|
90
|
+
# Based on https://github.com/jetpack-io/typeid-go/blob/341e2b135e0609db272e6400f2f551487725824a/base32/base32.go#L82.
|
91
|
+
# Each line needs an extra `& 0xFF` because the elements are +Integer+s, which don't truncate on left shifts.
|
92
|
+
#
|
93
|
+
# @param string [String] size 26 base32-encoded string
|
74
94
|
# @return [Array<Integer>]
|
75
95
|
def self.decode(string)
|
76
96
|
bytes = string.bytes
|
77
97
|
|
78
|
-
raise "invalid length" unless bytes.length ==
|
79
|
-
raise "invalid base32 character" if bytes.any? { |byte| DEC[byte] == 0xFF }
|
98
|
+
raise Error, "invalid length" unless bytes.length == ENCODED_STRING_LENGTH
|
99
|
+
raise Error, "invalid base32 character" if bytes.any? { |byte| DEC[byte] == 0xFF }
|
80
100
|
|
81
|
-
output = Array.new(
|
101
|
+
output = Array.new(DECODED_BYTE_ARRAY_LENGTH, 0)
|
82
102
|
|
83
103
|
# 6 bytes timestamp (48 bits)
|
84
104
|
output[0] = ((DEC[bytes[0]] << 5) | DEC[bytes[1]]) & 0xFF
|
@@ -94,7 +114,7 @@ class TypeID < String
|
|
94
114
|
output[8] = ((DEC[bytes[13]] << 4) | (DEC[bytes[14]] >> 1)) & 0xFF
|
95
115
|
output[9] = ((DEC[bytes[14]] << 7) | (DEC[bytes[15]] << 2) | (DEC[bytes[16]] >> 3)) & 0xFF
|
96
116
|
output[10] = ((DEC[bytes[16]] << 5) | DEC[bytes[17]]) & 0xFF
|
97
|
-
output[11] = ((DEC[bytes[18]] << 3) | DEC[bytes[19]]>>2) & 0xFF
|
117
|
+
output[11] = ((DEC[bytes[18]] << 3) | DEC[bytes[19]] >> 2) & 0xFF
|
98
118
|
output[12] = ((DEC[bytes[19]] << 6) | (DEC[bytes[20]] << 1) | (DEC[bytes[21]] >> 4)) & 0xFF
|
99
119
|
output[13] = ((DEC[bytes[21]] << 4) | (DEC[bytes[22]] >> 1)) & 0xFF
|
100
120
|
output[14] = ((DEC[bytes[22]] << 7) | (DEC[bytes[23]] << 2) | (DEC[bytes[24]] >> 3)) & 0xFF
|
data/lib/typeid/uuid.rb
CHANGED
@@ -2,22 +2,37 @@ require "uuid7"
|
|
2
2
|
require_relative "./uuid/base32.rb"
|
3
3
|
|
4
4
|
class TypeID < String
|
5
|
+
# Represents a UUID. Can be treated as a string.
|
5
6
|
class UUID < String
|
7
|
+
# @return [Array<Integer>]
|
6
8
|
attr_reader :bytes
|
7
9
|
|
8
|
-
#
|
10
|
+
# Utility method to generate a timestamp as milliseconds since the Unix epoch.
|
11
|
+
#
|
12
|
+
# @return [Integer]
|
13
|
+
def self.timestamp
|
14
|
+
Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Generates a new +UUID+, using gem "uuid7".
|
18
|
+
#
|
19
|
+
# @param timestamp [Integer] milliseconds since the Unix epoch
|
9
20
|
# @return [TypeID::UUID]
|
10
|
-
def self.generate(timestamp:
|
21
|
+
def self.generate(timestamp: self.class.timestamp)
|
11
22
|
from_string(UUID7.generate(timestamp: timestamp))
|
12
23
|
end
|
13
24
|
|
14
|
-
#
|
25
|
+
# Parses a +UUID+ from a base32 +String+.
|
26
|
+
#
|
27
|
+
# @param string [String] base32-encoded UUID
|
15
28
|
# @return [TypeID::UUID]
|
16
29
|
def self.from_base32(string)
|
17
30
|
new(TypeID::UUID::Base32.decode(string))
|
18
31
|
end
|
19
32
|
|
20
|
-
#
|
33
|
+
# Parses a +UUID+ from a raw +String+.
|
34
|
+
#
|
35
|
+
# @param string [String] raw UUID
|
21
36
|
# @return [TypeID::UUID]
|
22
37
|
def self.from_string(string)
|
23
38
|
bytes = string
|
@@ -29,7 +44,9 @@ class TypeID < String
|
|
29
44
|
new(bytes)
|
30
45
|
end
|
31
46
|
|
32
|
-
#
|
47
|
+
# Initializes a +UUID+ from an array of bytes.
|
48
|
+
#
|
49
|
+
# @param bytes [Array<Integer>] size 16 byte array
|
33
50
|
def initialize(bytes)
|
34
51
|
@bytes = bytes
|
35
52
|
|
data/lib/typeid/version.rb
CHANGED
data/lib/typeid.rb
CHANGED
@@ -1,28 +1,52 @@
|
|
1
1
|
require_relative "./typeid/uuid.rb"
|
2
2
|
|
3
|
+
# Represents a TypeID.
|
4
|
+
# Provides accessors to the underlying prefix, suffix, and UUID.
|
5
|
+
# Can be treated as a string.
|
6
|
+
#
|
7
|
+
# To generate a new +TypeID+:
|
8
|
+
# TypeID.new("foo") #=> #<TypeID foo_01h4vjdvzefw18zfwz5dxw5y8g>
|
9
|
+
#
|
10
|
+
# To parse a +TypeID+ from a string:
|
11
|
+
# TypeID.from_string("foo_01h4vjdvzefw18zfwz5dxw5y8g") #=> #<TypeID foo_01h4vjdvzefw18zfwz5dxw5y8g>
|
12
|
+
#
|
13
|
+
# To parse a +TypeID+ from a UUID:
|
14
|
+
# TypeID.from_uuid("foo", "01893726-efee-7f02-8fbf-9f2b7bc2f910") #=> #<TypeID foo_01h4vjdvzefw18zfwz5dxw5y8g>
|
15
|
+
#
|
16
|
+
# To create a +TypeID+ from a timestamp (in milliseconds since the Unix epoch):
|
17
|
+
# TypeID.new("foo", timestamp: 1688847445998) #=> #<TypeID foo_01h4vjdvzefw18zfwz5dxw5y8g>
|
3
18
|
class TypeID < String
|
4
|
-
SUFFIX_LENGTH = 26
|
5
19
|
MAX_PREFIX_LENGTH = 63
|
6
20
|
|
7
21
|
class Error < StandardError; end
|
8
22
|
|
23
|
+
# @return [String]
|
9
24
|
attr_reader :prefix
|
25
|
+
|
26
|
+
# @return [String]
|
10
27
|
attr_reader :suffix
|
11
28
|
alias type prefix
|
12
29
|
|
13
|
-
#
|
30
|
+
# Parses a +TypeID+ from a string.
|
31
|
+
#
|
32
|
+
# @param string [String] string representation of a +TypeID+
|
14
33
|
# @return [TypeID]
|
15
34
|
def self.from_string(string)
|
16
35
|
case string.split("_")
|
17
|
-
in [suffix]
|
36
|
+
in [suffix]
|
37
|
+
from("", suffix)
|
38
|
+
|
18
39
|
in [prefix, suffix]
|
19
40
|
raise Error, "prefix cannot be empty when there's a separator" if prefix.empty?
|
20
41
|
|
21
42
|
from(prefix, suffix)
|
22
|
-
else
|
43
|
+
else
|
44
|
+
raise Error, "invalid typeid: #{string}"
|
23
45
|
end
|
24
46
|
end
|
25
47
|
|
48
|
+
# Parses a +TypeID+ given a prefix and a raw UUID string.
|
49
|
+
#
|
26
50
|
# @param prefix [String]
|
27
51
|
# @param uuid [String]
|
28
52
|
# @return [TypeID]
|
@@ -30,6 +54,8 @@ class TypeID < String
|
|
30
54
|
from(prefix, TypeID::UUID.from_string(uuid).base32)
|
31
55
|
end
|
32
56
|
|
57
|
+
# Creates a +TypeID+ given a prefix string and a suffix string.
|
58
|
+
#
|
33
59
|
# @param prefix [String]
|
34
60
|
# @param suffix [String]
|
35
61
|
# @return [TypeID]
|
@@ -37,23 +63,30 @@ class TypeID < String
|
|
37
63
|
new(prefix, suffix: suffix)
|
38
64
|
end
|
39
65
|
|
66
|
+
# Returns the +nil+ TypeID.
|
67
|
+
#
|
40
68
|
# @return [TypeID]
|
41
69
|
def self.nil
|
42
|
-
from("", "0" *
|
70
|
+
@nil ||= from("", "0" * TypeID::UUID::Base32::ENCODED_STRING_LENGTH)
|
43
71
|
end
|
44
72
|
|
73
|
+
# Creates a +TypeID+ given a prefix and an optional suffix or timestamp (in milliseconds since the Unix epoch).
|
74
|
+
# When given only a prefix, generates a new +TypeID+.
|
75
|
+
# When +suffix+ or +timestamp+ is provided, creates a +TypeID+ from the given value.
|
76
|
+
#
|
45
77
|
# @param prefix [String]
|
46
|
-
# @param timestamp [Integer]
|
47
|
-
# @param suffix [String]
|
78
|
+
# @param timestamp [Integer] milliseconds since the Unix epoch
|
79
|
+
# @param suffix [String] base32-encoded UUID
|
48
80
|
def initialize(
|
49
81
|
prefix,
|
50
|
-
timestamp:
|
82
|
+
timestamp: TypeID::UUID.timestamp,
|
51
83
|
suffix: TypeID::UUID.generate(timestamp: timestamp).base32
|
52
84
|
)
|
53
85
|
raise Error, "prefix length cannot be greater than #{MAX_PREFIX_LENGTH}" if prefix.length > MAX_PREFIX_LENGTH
|
54
86
|
raise Error, "prefix must be lowercase ASCII characters" unless prefix.match?(/^[a-z]*$/)
|
55
|
-
raise Error, "suffix must be #{
|
87
|
+
raise Error, "suffix must be #{TypeID::UUID::Base32::ENCODED_STRING_LENGTH} characters" unless suffix.length == TypeID::UUID::Base32::ENCODED_STRING_LENGTH
|
56
88
|
raise Error, "suffix must only contain the letters in '#{TypeID::UUID::Base32::ALPHABET}'" unless suffix.chars.all? { |char| TypeID::UUID::Base32::ALPHABET.include?(char) }
|
89
|
+
raise Error, "suffix must start with a 0-7 digit to avoid overflows" unless ("0".."7").cover?(suffix.chars.first)
|
57
90
|
|
58
91
|
@prefix = prefix
|
59
92
|
@suffix = suffix
|
@@ -61,6 +94,8 @@ class TypeID < String
|
|
61
94
|
super(string)
|
62
95
|
end
|
63
96
|
|
97
|
+
# Returns the UUID component of the +TypeID+, parsed from the suffix.
|
98
|
+
#
|
64
99
|
# @return [TypeID::UUID]
|
65
100
|
def uuid
|
66
101
|
TypeID::UUID.from_base32(suffix)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: typeid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Booth
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-07-
|
11
|
+
date: 2023-07-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: uuid7
|
@@ -25,33 +25,47 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 0.2.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: pry
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 0.14.2
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 0.14.2
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: rake
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: 0
|
47
|
+
version: '13.0'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: 0
|
54
|
+
version: '13.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.12'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.12'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: simplecov
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|