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.5.0'

Class Method Summary collapse

Class Method Details

.cuid2(length: 24) ⇒ Object



87
88
89
# File 'lib/philiprehberger/id_gen.rb', line 87

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

.cuid2_batch(count, length: 24) ⇒ Object



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

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



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

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



179
180
181
182
183
184
185
186
187
188
# File 'lib/philiprehberger/id_gen.rb', line 179

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



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

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

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



112
113
114
# File 'lib/philiprehberger/id_gen.rb', line 112

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



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

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:



192
193
194
195
196
197
198
199
200
201
# File 'lib/philiprehberger/id_gen.rb', line 192

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



133
134
135
136
# File 'lib/philiprehberger/id_gen.rb', line 133

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_decompose(id, epoch: nil) ⇒ Hash{Symbol => Object}

Decompose a snowflake ID into its three components.

Returns a hash with ‘:timestamp` (Time, derived from the optional `epoch:` shift), `:worker_id` (Integer), and `:sequence` (Integer). Useful for debugging hot-spotting (which worker), throughput analytics (sequence saturation), and audit logs.

Parameters:

  • id (Integer)

    the snowflake ID

  • epoch (Time, nil) (defaults to: nil)

    custom epoch used at generation time, if any

Returns:

  • (Hash{Symbol => Object})

    ‘{ timestamp:, worker_id:, sequence: }`



70
71
72
73
74
75
76
77
# File 'lib/philiprehberger/id_gen.rb', line 70

def self.snowflake_decompose(id, epoch: nil)
  if epoch
    epoch_ms = (epoch.to_f * 1000).to_i
    Snowflake.decompose(id, epoch_ms: epoch_ms)
  else
    Snowflake.decompose(id)
  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



118
119
120
121
# File 'lib/philiprehberger/id_gen.rb', line 118

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



79
80
81
# File 'lib/philiprehberger/id_gen.rb', line 79

def self.uuid_v7
  Uuid.generate_v7
end

.uuid_v7_batch(count) ⇒ Object



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

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

.uuid_v7_timestamp(uuid_string) ⇒ Object



83
84
85
# File 'lib/philiprehberger/id_gen.rb', line 83

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

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

Returns:

  • (Boolean)


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

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)


147
148
149
150
151
152
153
# File 'lib/philiprehberger/id_gen.rb', line 147

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)


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

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)


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

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)


155
156
157
# File 'lib/philiprehberger/id_gen.rb', line 155

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