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.
@@ -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
- # @api private
8
- attr_accessor :latest_milliseconds, :latest_entropy
7
+ # @return [ULID, nil]
8
+ attr_reader :prev
9
+
10
+ undef_method :instance_variable_set
9
11
 
10
12
  def initialize
11
- reset
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 [ArgumentError] if the given moment(milliseconds) is negative number
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
- milliseconds = ULID.milliseconds_from_moment(moment)
20
- raise ArgumentError, "milliseconds should not be negative: given: #{milliseconds}" if milliseconds.negative?
21
-
22
- if @latest_milliseconds < milliseconds
23
- @latest_milliseconds = milliseconds
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
- ULID.new milliseconds: @latest_milliseconds, entropy: @latest_entropy
30
- end
35
+ milliseconds = ULID.milliseconds_from_moment(moment)
31
36
 
32
- # @api private
33
- # @return [void]
34
- def reset
35
- @latest_milliseconds = 0
36
- @latest_entropy = ULID.reasonable_entropy
37
- nil
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
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  class ULID
5
- VERSION = '0.0.16'
5
+ VERSION = '0.1.1'
6
6
  end
data/sig/ulid.rbs CHANGED
@@ -1,10 +1,10 @@
1
1
  # Classes
2
2
  class ULID
3
3
  VERSION: String
4
- ENCODING_CHARS: Array[String]
5
- TIMESTAMP_PART_LENGTH: 10
6
- RANDOMNESS_PART_LENGTH: 16
7
- ENCODED_ID_LENGTH: 26
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
- PATTERN: Regexp
16
- STRICT_PATTERN: Regexp
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 SetupError < ScriptError
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
- attr_accessor latest_milliseconds: Integer
42
- attr_accessor latest_entropy: Integer
53
+ @mutex: Thread::Mutex
54
+ attr_reader prev: ULID | nil
43
55
  def initialize: -> void
44
56
  def generate: (?moment: moment) -> ULID
45
- def reset: -> void
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) -> ULID
71
- def self.monotonic_generate: -> ULID
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: (Range[Time] | Range[nil] time_range) -> Range[ULID]
82
+ def self.range: (period period) -> Range[ULID]
76
83
  def self.floor: (Time time) -> Time
77
- def self.reasonable_entropy: -> Integer
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) -> ULID
81
- def self.min: (?moment: moment) -> ULID
82
- def self.max: (?moment: moment) -> ULID
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[ULID, singleton(ULID)]
85
- | (String string) { (ULID ulid) -> void } -> singleton(ULID)
86
- def self.octets_from_integer: (Integer integer, length: Integer) -> Array[Integer]
87
- def self.inverse_of_digits: (Array[Integer] reversed_digits) -> Integer
88
- def self.convert_crockford_base32_to_n32: (String) -> String
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) -> Integer?
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 pattern: -> Regexp
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.argument_error_for_range_building: (untyped argument) -> ArgumentError
117
- def matchdata: -> MatchData
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.0.16
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-05 00:00:00.000000000 Z
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