typeid 0.1.10 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d4ab90e4532a67246f422ad22f264ae2b238418de7246998d40e2a725dd0c6da
4
- data.tar.gz: 017e98dc29bce78fb73e217f771865f8477ba8f73fb585a1fca07804e1fa48f1
3
+ metadata.gz: af045f36c25f729104c597aec25af3c3755bee1d163bc22badce8fa85952d7ec
4
+ data.tar.gz: 8869cf01f856b513c501229d69fe262233934f7a8c886c8703e1c65a3c53692c
5
5
  SHA512:
6
- metadata.gz: 3ba0df0442f474b7890e03190f21f7162ba8c36ab6f449bd1df2049ef3ea378568bdb4436517ac23795eefd613854060413c87e7722d3f37bd7d5eca02702e9a
7
- data.tar.gz: e4506709a38e017030717e2acd1792e68e1e9715a90668d5e8afea51b4a4cb67595aa88b2b4c16b3b0773026a2612b7a8c08f160b73d650909f34f50ce41eb51
6
+ metadata.gz: 6f0019bff65125af55cf414d9bd0d5145cdafa1fedc1cbc6cf69b207966c4674a461daba129bc4cf4d9cb021c9f976a5b77b875bb3041ceae561c0cfd48f4941
7
+ data.tar.gz: 41def5ca6d85fe6a5a5b4133d64b1c47a3c56632564fe2c83d45c8ddeec752e96523738bf1cc9bf615f547afa60149928552cf01593dcc583c3da729dab1e2f6
@@ -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
- # @param bytes [Array<Integer>]
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
- encoded = Array.new(26, 0)
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
- # @param string [String]
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 == 26
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(16, 0)
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
- # @param timestamp [Integer]
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: Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond))
21
+ def self.generate(timestamp: self.class.timestamp)
11
22
  from_string(UUID7.generate(timestamp: timestamp))
12
23
  end
13
24
 
14
- # @param string [String]
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
- # @param string [String]
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,18 +44,31 @@ class TypeID < String
29
44
  new(bytes)
30
45
  end
31
46
 
32
- # @param bytes [Array<Integer>]
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
 
36
53
  super(string)
37
54
  end
38
55
 
56
+ # Returns the +UUID+ encoded as a base32 +String+.
57
+ #
39
58
  # @return [String]
40
59
  def base32
41
60
  TypeID::UUID::Base32.encode(bytes)
42
61
  end
43
62
 
63
+ # Returns the timestamp of the +UUID+ as milliseconds since the Unix epoch.
64
+ #
65
+ # @return [Integer]
66
+ def timestamp
67
+ bytes[0..5]
68
+ .map.with_index { |byte, index| byte << (5 - index) * 8 }
69
+ .inject(:|)
70
+ end
71
+
44
72
  # @return [String]
45
73
  def inspect
46
74
  "#<#{self.class.name} #{to_s}>"
@@ -1,3 +1,3 @@
1
1
  class TypeID < String
2
- VERSION = "0.1.10".freeze
2
+ VERSION = "0.2.1".freeze
3
3
  end
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
- # @param string [String]
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] then from("", 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 raise Error, "invalid typeid: #{string}"
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,22 +63,28 @@ 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" * SUFFIX_LENGTH)
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: Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond),
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 #{SUFFIX_LENGTH} characters" unless suffix.length == SUFFIX_LENGTH
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) }
57
89
  raise Error, "suffix must start with a 0-7 digit to avoid overflows" unless ("0".."7").cover?(suffix.chars.first)
58
90
 
@@ -62,6 +94,8 @@ class TypeID < String
62
94
  super(string)
63
95
  end
64
96
 
97
+ # Returns the UUID component of the +TypeID+, parsed from the suffix.
98
+ #
65
99
  # @return [TypeID::UUID]
66
100
  def uuid
67
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.1.10
4
+ version: 0.2.1
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-07 00:00:00.000000000 Z
11
+ date: 2023-07-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: uuid7