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 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
@@ -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
@@ -0,0 +1,3 @@
1
+ class TypeID < String
2
+ VERSION = "0.1.0".freeze
3
+ 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: []