ruby-ulid 0.0.16 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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