ruby-ulid 0.0.16 → 0.1.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 +4 -4
- data/README.md +173 -15
- data/lib/ulid.rb +245 -237
- data/lib/ulid/crockford_base32.rb +68 -0
- data/lib/ulid/monotonic_generator.rb +42 -21
- data/lib/ulid/uuid.rb +38 -0
- data/lib/ulid/version.rb +1 -1
- data/sig/ulid.rbs +56 -44
- metadata +4 -22
@@ -0,0 +1,68 @@
|
|
1
|
+
# coding: us-ascii
|
2
|
+
# frozen_string_literal: true
|
3
|
+
# Copyright (C) 2021 Kenichi Kamiya
|
4
|
+
|
5
|
+
class ULID
|
6
|
+
# Currently supporting only for `subset` for actual use-case`
|
7
|
+
# Original decoding spec allows other characters.
|
8
|
+
# But I think ULID should allow `subset` of Crockford's Base32.
|
9
|
+
# See below
|
10
|
+
# * https://github.com/ulid/spec/pull/57
|
11
|
+
# * https://github.com/kachick/ruby-ulid/issues/57
|
12
|
+
# * https://github.com/kachick/ruby-ulid/issues/78
|
13
|
+
module CrockfordBase32
|
14
|
+
class SetupError < ScriptError; end
|
15
|
+
|
16
|
+
n32_chars = [*'0'..'9', *'A'..'V'].map(&:freeze).freeze
|
17
|
+
raise SetupError, 'obvious bug exists in the mapping algorithm' unless n32_chars.size == 32
|
18
|
+
|
19
|
+
n32_char_by_number = {}
|
20
|
+
n32_chars.each_with_index do |char, index|
|
21
|
+
n32_char_by_number[index] = char
|
22
|
+
end
|
23
|
+
n32_char_by_number.freeze
|
24
|
+
|
25
|
+
crockford_base32_mappings = {
|
26
|
+
'J' => 18,
|
27
|
+
'K' => 19,
|
28
|
+
'M' => 20,
|
29
|
+
'N' => 21,
|
30
|
+
'P' => 22,
|
31
|
+
'Q' => 23,
|
32
|
+
'R' => 24,
|
33
|
+
'S' => 25,
|
34
|
+
'T' => 26,
|
35
|
+
'V' => 27,
|
36
|
+
'W' => 28,
|
37
|
+
'X' => 29,
|
38
|
+
'Y' => 30,
|
39
|
+
'Z' => 31
|
40
|
+
}.freeze
|
41
|
+
|
42
|
+
N32_CHAR_BY_CROCKFORD_BASE32_CHAR = CROCKFORD_BASE32_ENCODING_STRING.chars.map(&:freeze).each_with_object({}) do |encoding_char, map|
|
43
|
+
if n = crockford_base32_mappings[encoding_char]
|
44
|
+
char_32 = n32_char_by_number.fetch(n)
|
45
|
+
map[encoding_char] = char_32
|
46
|
+
end
|
47
|
+
end.freeze
|
48
|
+
raise SetupError, 'obvious bug exists in the mapping algorithm' unless N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys == crockford_base32_mappings.keys
|
49
|
+
CROCKFORD_BASE32_CHAR_PATTERN = /[#{N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys.join}]/.freeze
|
50
|
+
|
51
|
+
CROCKFORD_BASE32_CHAR_BY_N32_CHAR = N32_CHAR_BY_CROCKFORD_BASE32_CHAR.invert.freeze
|
52
|
+
N32_CHAR_PATTERN = /[#{CROCKFORD_BASE32_CHAR_BY_N32_CHAR.keys.join}]/.freeze
|
53
|
+
|
54
|
+
# @param [String] string
|
55
|
+
# @return [Integer]
|
56
|
+
def self.decode(string)
|
57
|
+
n32encoded = string.upcase.gsub(CROCKFORD_BASE32_CHAR_PATTERN, N32_CHAR_BY_CROCKFORD_BASE32_CHAR)
|
58
|
+
n32encoded.to_i(32)
|
59
|
+
end
|
60
|
+
|
61
|
+
# @param [Integer] integer
|
62
|
+
# @return [String]
|
63
|
+
def self.encode(integer)
|
64
|
+
n32encoded = integer.to_s(32)
|
65
|
+
n32encoded.upcase.gsub(N32_CHAR_PATTERN, CROCKFORD_BASE32_CHAR_BY_N32_CHAR).rjust(ENCODED_LENGTH, '0')
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -4,39 +4,60 @@
|
|
4
4
|
|
5
5
|
class ULID
|
6
6
|
class MonotonicGenerator
|
7
|
-
# @
|
8
|
-
|
7
|
+
# @return [ULID, nil]
|
8
|
+
attr_reader :prev
|
9
|
+
|
10
|
+
undef_method :instance_variable_set
|
9
11
|
|
10
12
|
def initialize
|
11
|
-
|
13
|
+
@mutex = Thread::Mutex.new
|
14
|
+
@prev = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [String]
|
18
|
+
def inspect
|
19
|
+
"ULID::MonotonicGenerator(prev: #{@prev.inspect})"
|
12
20
|
end
|
21
|
+
alias_method :to_s, :inspect
|
13
22
|
|
14
23
|
# @param [Time, Integer] moment
|
15
24
|
# @return [ULID]
|
16
25
|
# @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
|
17
|
-
# @raise [
|
26
|
+
# @raise [UnexpectedError] if the generated ULID is an invalid value in monotonicity spec.
|
27
|
+
# Basically will not happen. Just means this feature prefers error rather than invalid value.
|
18
28
|
def generate(moment: ULID.current_milliseconds)
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
@latest_entropy = ULID.reasonable_entropy
|
25
|
-
else
|
26
|
-
@latest_entropy += 1
|
27
|
-
end
|
29
|
+
@mutex.synchronize do
|
30
|
+
unless @prev
|
31
|
+
@prev = ULID.generate(moment: moment)
|
32
|
+
return @prev
|
33
|
+
end
|
28
34
|
|
29
|
-
|
30
|
-
end
|
35
|
+
milliseconds = ULID.milliseconds_from_moment(moment)
|
31
36
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
37
|
+
ulid = if @prev.milliseconds < milliseconds
|
38
|
+
ULID.generate(moment: milliseconds)
|
39
|
+
else
|
40
|
+
ULID.from_milliseconds_and_entropy(milliseconds: @prev.milliseconds, entropy: @prev.entropy.succ)
|
41
|
+
end
|
42
|
+
|
43
|
+
unless ulid > @prev
|
44
|
+
base_message = "monotonicity broken from unexpected reasons # generated: #{ulid.inspect}, prev: #{@prev.inspect}"
|
45
|
+
additional_information = if Thread.list == [Thread.main]
|
46
|
+
'# NOTE: looks single thread only exist'
|
47
|
+
else
|
48
|
+
'# NOTE: ran on multi threads, so this might from concurrency issue'
|
49
|
+
end
|
50
|
+
|
51
|
+
raise UnexpectedError, base_message + additional_information
|
52
|
+
end
|
53
|
+
|
54
|
+
@prev = ulid
|
55
|
+
ulid
|
56
|
+
end
|
38
57
|
end
|
39
58
|
|
59
|
+
undef_method :freeze
|
60
|
+
|
40
61
|
# @raise [TypeError] always raises exception and does not freeze self
|
41
62
|
# @return [void]
|
42
63
|
def freeze
|
data/lib/ulid/uuid.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# coding: us-ascii
|
2
|
+
# frozen_string_literal: true
|
3
|
+
# Copyright (C) 2021 Kenichi Kamiya
|
4
|
+
|
5
|
+
# Extracted features around UUID from some reasons
|
6
|
+
# ref:
|
7
|
+
# * https://github.com/kachick/ruby-ulid/issues/105
|
8
|
+
# * https://github.com/kachick/ruby-ulid/issues/76
|
9
|
+
class ULID
|
10
|
+
# Imported from https://stackoverflow.com/a/38191104/1212807, thank you!
|
11
|
+
UUIDV4_PATTERN = /\A[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}\z/i.freeze
|
12
|
+
private_constant :UUIDV4_PATTERN
|
13
|
+
|
14
|
+
# @param [String, #to_str] uuid
|
15
|
+
# @return [ULID]
|
16
|
+
# @raise [ParserError] if the given format is not correct for UUIDv4 specs
|
17
|
+
def self.from_uuidv4(uuid)
|
18
|
+
uuid = String.try_convert(uuid)
|
19
|
+
raise ArgumentError, 'ULID.from_uuidv4 takes only strings' unless uuid
|
20
|
+
|
21
|
+
prefix_trimmed = uuid.sub(/\Aurn:uuid:/, '')
|
22
|
+
unless UUIDV4_PATTERN.match?(prefix_trimmed)
|
23
|
+
raise ParserError, "given `#{uuid}` does not match to `#{UUIDV4_PATTERN.inspect}`"
|
24
|
+
end
|
25
|
+
|
26
|
+
normalized = prefix_trimmed.gsub(/[^0-9A-Fa-f]/, '')
|
27
|
+
from_integer(normalized.to_i(16))
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [String]
|
31
|
+
def to_uuidv4
|
32
|
+
# This code referenced https://github.com/ruby/ruby/blob/121fa24a3451b45c41ac0a661b64e9fc8600e589/lib/securerandom.rb#L221-L241
|
33
|
+
array = octets.pack('C*').unpack('NnnnnN')
|
34
|
+
array[2] = (array[2] & 0x0fff) | 0x4000
|
35
|
+
array[3] = (array[3] & 0x3fff) | 0x8000
|
36
|
+
('%08x-%04x-%04x-%04x-%04x%08x' % array).freeze
|
37
|
+
end
|
38
|
+
end
|
data/lib/ulid/version.rb
CHANGED
data/sig/ulid.rbs
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# Classes
|
2
2
|
class ULID
|
3
3
|
VERSION: String
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
CROCKFORD_BASE32_ENCODING_STRING: String
|
5
|
+
TIMESTAMP_ENCODED_LENGTH: 10
|
6
|
+
RANDOMNESS_ENCODED_LENGTH: 16
|
7
|
+
ENCODED_LENGTH: 26
|
8
8
|
TIMESTAMP_OCTETS_LENGTH: 6
|
9
9
|
RANDOMNESS_OCTETS_LENGTH: 10
|
10
10
|
OCTETS_LENGTH: 16
|
@@ -12,12 +12,11 @@ class ULID
|
|
12
12
|
MAX_ENTROPY: 1208925819614629174706175
|
13
13
|
MAX_INTEGER: 340282366920938463463374607431768211455
|
14
14
|
TIME_FORMAT_IN_INSPECT: '%Y-%m-%d %H:%M:%S.%3N %Z'
|
15
|
-
|
16
|
-
|
15
|
+
RANDOM_INTEGER_GENERATOR: ^() -> Integer
|
16
|
+
PATTERN_WITH_CROCKFORD_BASE32_SUBSET: Regexp
|
17
|
+
STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET: Regexp
|
18
|
+
SCANNING_PATTERN: Regexp
|
17
19
|
UUIDV4_PATTERN: Regexp
|
18
|
-
REPLACING_MAP: Hash[String, String]
|
19
|
-
REPLACING_PATTERN: Regexp
|
20
|
-
MONOTONIC_GENERATOR: MonotonicGenerator
|
21
20
|
MIN: ULID
|
22
21
|
MAX: ULID
|
23
22
|
include Comparable
|
@@ -34,66 +33,75 @@ class ULID
|
|
34
33
|
class ParserError < Error
|
35
34
|
end
|
36
35
|
|
37
|
-
class
|
36
|
+
class UnexpectedError < Error
|
37
|
+
end
|
38
|
+
|
39
|
+
module CrockfordBase32
|
40
|
+
class SetupError < ScriptError
|
41
|
+
end
|
42
|
+
|
43
|
+
N32_CHAR_BY_CROCKFORD_BASE32_CHAR: Hash[String, String]
|
44
|
+
CROCKFORD_BASE32_CHAR_PATTERN: Regexp
|
45
|
+
CROCKFORD_BASE32_CHAR_BY_N32_CHAR: Hash[String, String]
|
46
|
+
N32_CHAR_PATTERN: Regexp
|
47
|
+
|
48
|
+
def self.encode: (Integer integer) -> String
|
49
|
+
def self.decode: (String string) -> Integer
|
38
50
|
end
|
39
51
|
|
40
52
|
class MonotonicGenerator
|
41
|
-
|
42
|
-
|
53
|
+
@mutex: Thread::Mutex
|
54
|
+
attr_reader prev: ULID | nil
|
43
55
|
def initialize: -> void
|
44
56
|
def generate: (?moment: moment) -> ULID
|
45
|
-
def
|
57
|
+
def inspect: -> String
|
58
|
+
alias to_s inspect
|
46
59
|
def freeze: -> void
|
47
60
|
end
|
48
61
|
|
62
|
+
interface _ULID
|
63
|
+
def to_ulid: () -> ULID
|
64
|
+
end
|
65
|
+
|
49
66
|
type octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
|
50
67
|
type timestamp_octets = [Integer, Integer, Integer, Integer, Integer, Integer]
|
51
68
|
type randomness_octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
|
69
|
+
type period = Range[Time] | Range[nil] | Range[ULID]
|
52
70
|
|
53
|
-
@milliseconds: Integer
|
54
|
-
@entropy: Integer
|
55
71
|
@string: String?
|
56
|
-
@integer: Integer
|
57
|
-
@octets: octets?
|
58
|
-
@timestamp_octets: timestamp_octets?
|
59
|
-
@randomness_octets: randomness_octets?
|
72
|
+
@integer: Integer
|
60
73
|
@timestamp: String?
|
61
74
|
@randomness: String?
|
62
75
|
@inspect: String?
|
63
76
|
@time: Time?
|
64
|
-
@next: ULID?
|
65
|
-
@pattern: Regexp?
|
66
|
-
@strict_pattern: Regexp?
|
67
|
-
@uuidv4: String?
|
68
|
-
@matchdata: MatchData?
|
69
77
|
|
70
|
-
def self.generate: (?moment: moment, ?entropy: Integer) ->
|
71
|
-
def self.
|
78
|
+
def self.generate: (?moment: moment, ?entropy: Integer) -> self
|
79
|
+
def self.at: (Time time) -> self
|
72
80
|
def self.current_milliseconds: -> Integer
|
73
|
-
def self.milliseconds_from_time: (Time time) -> Integer
|
74
81
|
def self.milliseconds_from_moment: (moment moment) -> Integer
|
75
|
-
def self.range: (
|
82
|
+
def self.range: (period period) -> Range[ULID]
|
76
83
|
def self.floor: (Time time) -> Time
|
77
|
-
def self.
|
78
|
-
def self.parse: (String string) -> ULID
|
84
|
+
def self.parse: (String string) -> self
|
79
85
|
def self.from_uuidv4: (String uuid) -> ULID
|
80
|
-
def self.from_integer: (Integer integer) ->
|
81
|
-
def self.min: (?moment
|
82
|
-
def self.max: (?moment
|
86
|
+
def self.from_integer: (Integer integer) -> self
|
87
|
+
def self.min: (?moment moment) -> ULID
|
88
|
+
def self.max: (?moment moment) -> ULID
|
89
|
+
def self.sample: (?period: period) -> self
|
90
|
+
| (Integer number, ?period: period) -> Array[self]
|
83
91
|
def self.valid?: (untyped string) -> bool
|
84
|
-
def self.scan: (String string) -> Enumerator[
|
85
|
-
| (String string) { (
|
86
|
-
def self.
|
87
|
-
def self.
|
88
|
-
|
92
|
+
def self.scan: (String string) -> Enumerator[self, singleton(ULID)]
|
93
|
+
| (String string) { (self ulid) -> void } -> singleton(ULID)
|
94
|
+
def self.from_milliseconds_and_entropy: (milliseconds: Integer, entropy: Integer) -> self
|
95
|
+
def self.try_convert: (_ULID) -> self
|
96
|
+
| (untyped) -> nil
|
89
97
|
attr_reader milliseconds: Integer
|
90
98
|
attr_reader entropy: Integer
|
91
|
-
def initialize: (milliseconds: Integer, entropy: Integer) -> void
|
99
|
+
def initialize: (milliseconds: Integer, entropy: Integer, integer: Integer) -> void
|
92
100
|
def to_s: -> String
|
93
101
|
def to_i: -> Integer
|
94
102
|
alias hash to_i
|
95
103
|
def <=>: (ULID other) -> Integer
|
96
|
-
| (untyped other) ->
|
104
|
+
| (untyped other) -> nil
|
97
105
|
def inspect: -> String
|
98
106
|
def eql?: (untyped other) -> bool
|
99
107
|
alias == eql?
|
@@ -101,8 +109,7 @@ class ULID
|
|
101
109
|
def to_time: -> Time
|
102
110
|
def timestamp: -> String
|
103
111
|
def randomness: -> String
|
104
|
-
def
|
105
|
-
def strict_pattern: -> Regexp
|
112
|
+
def patterns: -> Hash[Symbol, Regexp | String]
|
106
113
|
def octets: -> octets
|
107
114
|
def timestamp_octets: -> timestamp_octets
|
108
115
|
def randomness_octets: -> randomness_octets
|
@@ -111,9 +118,14 @@ class ULID
|
|
111
118
|
alias succ next
|
112
119
|
def pred: -> ULID?
|
113
120
|
def freeze: -> self
|
121
|
+
def to_ulid: -> self
|
122
|
+
def dup: -> self
|
123
|
+
# Same as https://github.com/ruby/rbs/blob/4fb4c33b2325d1a73d79ff7aaeb49f21cec1e0e5/core/object.rbs#L79
|
124
|
+
def clone: (?freeze: bool) -> self
|
114
125
|
|
115
126
|
private
|
116
|
-
def self.
|
117
|
-
def
|
127
|
+
def self.reasonable_entropy: -> Integer
|
128
|
+
def self.milliseconds_from_time: (Time time) -> Integer
|
129
|
+
def self.safe_get_class_name: (untyped object) -> String
|
118
130
|
def cache_all_instance_variables: -> void
|
119
131
|
end
|
metadata
CHANGED
@@ -1,35 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-ulid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kenichi Kamiya
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-05-
|
11
|
+
date: 2021-05-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: integer-base
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ">="
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: 0.1.2
|
20
|
-
- - "<"
|
21
|
-
- !ruby/object:Gem::Version
|
22
|
-
version: 0.2.0
|
23
|
-
type: :runtime
|
24
|
-
prerelease: false
|
25
|
-
version_requirements: !ruby/object:Gem::Requirement
|
26
|
-
requirements:
|
27
|
-
- - ">="
|
28
|
-
- !ruby/object:Gem::Version
|
29
|
-
version: 0.1.2
|
30
|
-
- - "<"
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: 0.2.0
|
33
13
|
- !ruby/object:Gem::Dependency
|
34
14
|
name: rbs
|
35
15
|
requirement: !ruby/object:Gem::Requirement
|
@@ -97,7 +77,9 @@ files:
|
|
97
77
|
- LICENSE
|
98
78
|
- README.md
|
99
79
|
- lib/ulid.rb
|
80
|
+
- lib/ulid/crockford_base32.rb
|
100
81
|
- lib/ulid/monotonic_generator.rb
|
82
|
+
- lib/ulid/uuid.rb
|
101
83
|
- lib/ulid/version.rb
|
102
84
|
- sig/ulid.rbs
|
103
85
|
homepage: https://github.com/kachick/ruby-ulid
|