Module: Philiprehberger::IdGen

Defined in:
lib/philiprehberger/id_gen.rb,
lib/philiprehberger/id_gen/ulid.rb,
lib/philiprehberger/id_gen/uuid.rb,
lib/philiprehberger/id_gen/cuid2.rb,
lib/philiprehberger/id_gen/hashid.rb,
lib/philiprehberger/id_gen/nanoid.rb,
lib/philiprehberger/id_gen/encoder.rb,
lib/philiprehberger/id_gen/version.rb,
lib/philiprehberger/id_gen/prefixed.rb,
lib/philiprehberger/id_gen/snowflake.rb

Defined Under Namespace

Modules: Cuid2, Encoder, Hashid, Nanoid, Prefixed, Snowflake, Ulid, Uuid Classes: Error

Constant Summary collapse

VERSION =
'0.4.0'

Class Method Summary collapse

Class Method Details

.cuid2(length: 24) ⇒ Object



68
69
70
# File 'lib/philiprehberger/id_gen.rb', line 68

def self.cuid2(length: 24)
  Cuid2.generate(length)
end

.cuid2_batch(count, length: 24) ⇒ Object



72
73
74
75
# File 'lib/philiprehberger/id_gen.rb', line 72

def self.cuid2_batch(count, length: 24)
  validate_batch_count!(count)
  Array.new(count) { Cuid2.generate(length) }
end

.decode(string, alphabet: Encoder::DEFAULT_ALPHABET) ⇒ Object



89
90
91
# File 'lib/philiprehberger/id_gen.rb', line 89

def self.decode(string, alphabet: Encoder::DEFAULT_ALPHABET)
  Encoder.decode(string, alphabet: alphabet)
end

.detect_format(id) ⇒ Symbol?

Detect the format of an ID by probing the format-specific validators. Probing order (most specific first): ULID, UUID v7, Snowflake (Integer or numeric String), CUID2, Nanoid. Returns a Symbol identifying the format (‘:ulid`, `:uuid_v7`, `:snowflake`, `:cuid2`, `:nanoid`) or `nil` when nothing matches.

Nanoid intentionally runs last because its default alphabet overlaps with many others — treat ‘:nanoid` as a fallback identification only.

Parameters:

  • id (String, Integer)

    the candidate ID

Returns:

  • (Symbol, nil)

    detected format, or nil when no format matches



160
161
162
163
164
165
166
167
168
169
# File 'lib/philiprehberger/id_gen.rb', line 160

def self.detect_format(id)
  return :ulid if id.is_a?(String) && valid_ulid?(id)
  return :uuid_v7 if id.is_a?(String) && valid_uuid_v7?(id)
  return :snowflake if valid_snowflake?(id)
  return :snowflake if id.is_a?(String) && id.match?(/\A\d+\z/) && valid_snowflake?(id.to_i)
  return :cuid2 if id.is_a?(String) && valid_cuid2?(id)
  return :nanoid if id.is_a?(String) && valid_nanoid?(id)

  nil
end

.encode(integer, alphabet: Encoder::DEFAULT_ALPHABET) ⇒ Object



85
86
87
# File 'lib/philiprehberger/id_gen.rb', line 85

def self.encode(integer, alphabet: Encoder::DEFAULT_ALPHABET)
  Encoder.encode(integer, alphabet: alphabet)
end

.hashid(integer, salt: '', min_length: 8) ⇒ Object



93
94
95
# File 'lib/philiprehberger/id_gen.rb', line 93

def self.hashid(integer, salt: '', min_length: 8)
  Hashid.encode(integer, salt: salt, min_length: min_length)
end

.nanoid(size = 21, alphabet: Nanoid::DEFAULT_ALPHABET) ⇒ Object



29
30
31
# File 'lib/philiprehberger/id_gen.rb', line 29

def self.nanoid(size = 21, alphabet: Nanoid::DEFAULT_ALPHABET)
  Nanoid.generate(size, alphabet: alphabet)
end

.nanoid_batch(count, size: 21, alphabet: Nanoid::DEFAULT_ALPHABET) ⇒ Object



104
105
106
107
# File 'lib/philiprehberger/id_gen.rb', line 104

def self.nanoid_batch(count, size: 21, alphabet: Nanoid::DEFAULT_ALPHABET)
  validate_batch_count!(count)
  Array.new(count) { Nanoid.generate(size, alphabet: alphabet) }
end

.parse_ulid(string) ⇒ Object

ULID parsing

Raises:



173
174
175
176
177
178
179
180
181
182
# File 'lib/philiprehberger/id_gen.rb', line 173

def self.parse_ulid(string)
  raise Error, 'Invalid ULID format' unless valid_ulid?(string)

  timestamp = Ulid.timestamp(string)
  random_part = string[10, 16]
  random_value = Ulid.send(:decode_crockford, random_part)
  random_hex = format('%020x', random_value)

  { timestamp: timestamp, random: random_hex }
end

.prefixed(prefix) ⇒ Object



33
34
35
# File 'lib/philiprehberger/id_gen.rb', line 33

def self.prefixed(prefix)
  Prefixed.generate(prefix)
end

.prefixed_batch(prefix, count) ⇒ Object



114
115
116
117
# File 'lib/philiprehberger/id_gen.rb', line 114

def self.prefixed_batch(prefix, count)
  validate_batch_count!(count)
  Array.new(count) { Prefixed.generate(prefix) }
end

.snowflake(worker_id: 0, epoch: nil) ⇒ Object



37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/philiprehberger/id_gen.rb', line 37

def self.snowflake(worker_id: 0, epoch: nil)
  if epoch
    epoch_ms = (epoch.to_f * 1000).to_i
    key = [worker_id, epoch_ms]
    @snowflake_custom_generators ||= {}
    @snowflake_custom_generators[key] ||= Snowflake::Generator.new(worker_id: worker_id, epoch: epoch)
    @snowflake_custom_generators[key].generate
  else
    @snowflake_generators ||= {}
    @snowflake_generators[worker_id] ||= Snowflake::Generator.new(worker_id: worker_id)
    @snowflake_generators[worker_id].generate
  end
end

.snowflake_timestamp(id, epoch: nil) ⇒ Object



51
52
53
54
55
56
57
58
# File 'lib/philiprehberger/id_gen.rb', line 51

def self.snowflake_timestamp(id, epoch: nil)
  if epoch
    epoch_ms = (epoch.to_f * 1000).to_i
    Snowflake.timestamp(id, epoch_ms: epoch_ms)
  else
    Snowflake.timestamp(id)
  end
end

.ulidObject



17
18
19
# File 'lib/philiprehberger/id_gen.rb', line 17

def self.ulid
  Ulid.generate
end

.ulid_batch(count) ⇒ Object

Batch generation methods



99
100
101
102
# File 'lib/philiprehberger/id_gen.rb', line 99

def self.ulid_batch(count)
  validate_batch_count!(count)
  Array.new(count) { Ulid.generate }
end

.ulid_monotonicObject



21
22
23
# File 'lib/philiprehberger/id_gen.rb', line 21

def self.ulid_monotonic
  Ulid.monotonic
end

.ulid_timestamp(ulid_string) ⇒ Object



25
26
27
# File 'lib/philiprehberger/id_gen.rb', line 25

def self.ulid_timestamp(ulid_string)
  Ulid.timestamp(ulid_string)
end

.uuid_v7Object



60
61
62
# File 'lib/philiprehberger/id_gen.rb', line 60

def self.uuid_v7
  Uuid.generate_v7
end

.uuid_v7_batch(count) ⇒ Object



109
110
111
112
# File 'lib/philiprehberger/id_gen.rb', line 109

def self.uuid_v7_batch(count)
  validate_batch_count!(count)
  Array.new(count) { Uuid.generate_v7 }
end

.uuid_v7_timestamp(uuid_string) ⇒ Object



64
65
66
# File 'lib/philiprehberger/id_gen.rb', line 64

def self.uuid_v7_timestamp(uuid_string)
  Uuid.timestamp_v7(uuid_string)
end

.valid_cuid2?(string, length: 24) ⇒ Boolean

Returns:

  • (Boolean)


77
78
79
80
81
82
83
# File 'lib/philiprehberger/id_gen.rb', line 77

def self.valid_cuid2?(string, length: 24)
  return false unless string.is_a?(String)
  return false unless string.length == length
  return false unless string[0].match?(/[a-z]/)

  string.match?(/\A[a-z0-9]+\z/)
end

.valid_nanoid?(string, size: 21, alphabet: Nanoid::DEFAULT_ALPHABET) ⇒ Boolean

Returns:

  • (Boolean)


128
129
130
131
132
133
134
# File 'lib/philiprehberger/id_gen.rb', line 128

def self.valid_nanoid?(string, size: 21, alphabet: Nanoid::DEFAULT_ALPHABET)
  return false unless string.is_a?(String)
  return false unless string.length == size

  escaped = Regexp.escape(alphabet)
  string.match?(/\A[#{escaped}]+\z/)
end

.valid_snowflake?(id) ⇒ Boolean

Returns:

  • (Boolean)


140
141
142
143
144
145
146
147
# File 'lib/philiprehberger/id_gen.rb', line 140

def self.valid_snowflake?(id)
  return false unless id.is_a?(Integer)
  return false unless id.positive?

  timestamp_ms = (id >> Snowflake::TIMESTAMP_SHIFT) + Snowflake::CUSTOM_EPOCH
  # Reasonable timestamp: between epoch (2020-01-01) and ~100 years after
  timestamp_ms.positive? && timestamp_ms < (Snowflake::CUSTOM_EPOCH + (100 * 365.25 * 24 * 60 * 60 * 1000).to_i)
end

.valid_ulid?(string) ⇒ Boolean

Validation methods

Returns:

  • (Boolean)


121
122
123
124
125
126
# File 'lib/philiprehberger/id_gen.rb', line 121

def self.valid_ulid?(string)
  return false unless string.is_a?(String)
  return false unless string.length == 26

  string.match?(/\A[0-9A-HJKMNP-TV-Z]+\z/)
end

.valid_uuid_v7?(string) ⇒ Boolean

Returns:

  • (Boolean)


136
137
138
# File 'lib/philiprehberger/id_gen.rb', line 136

def self.valid_uuid_v7?(string)
  Uuid.valid_v7?(string)
end