Module: Ashid

Extended by:
Ashid
Included in:
Ashid
Defined in:
lib/ashid.rb,
lib/ashid/error.rb,
lib/ashid/encoder.rb,
lib/ashid/version.rb

Defined Under Namespace

Modules: Encoder Classes: Error, InvalidEncodingError, InvalidIdError

Constant Summary collapse

MAX_TIMESTAMP =
35_184_372_088_831
TIMESTAMP_ENCODED_LENGTH =
9
RANDOM_ENCODED_LENGTH =
13
STANDARD_BASE_LENGTH =
22
ASHID4_BASE_LENGTH =
26
VERSION =
"0.1.0"

Instance Method Summary collapse

Instance Method Details

#generate(prefix = nil, time: current_time_ms, random: Encoder.secure_random(bits: 64)) ⇒ Object

Raises:

  • (ArgumentError)


16
17
18
19
20
21
22
23
24
# File 'lib/ashid.rb', line 16

def generate(prefix = nil, time: current_time_ms, random: Encoder.secure_random(bits: 64))
  raise ArgumentError, "time must be non-negative"             if time < 0
  raise ArgumentError, "time must not exceed #{MAX_TIMESTAMP}" if time > MAX_TIMESTAMP
  raise ArgumentError, "random must be non-negative"           if random < 0

  normalized = normalize_prefix(prefix)
  base       = build_base_id(normalized, time, random)
  "#{normalized}#{base}"
end

#generate4(prefix = nil, random1: Encoder.secure_random(bits: 64), random2: Encoder.secure_random(bits: 64)) ⇒ Object

Raises:

  • (ArgumentError)


26
27
28
29
30
31
32
33
# File 'lib/ashid.rb', line 26

def generate4(prefix = nil, random1: Encoder.secure_random(bits: 64), random2: Encoder.secure_random(bits: 64))
  raise ArgumentError, "random1 must be non-negative" if random1 < 0
  raise ArgumentError, "random2 must be non-negative" if random2 < 0

  normalized = normalize_prefix(prefix)
  base       = "#{Encoder.encode(random1, padded: true)}#{Encoder.encode(random2, padded: true)}"
  "#{normalized}#{base}"
end

#normalize(id) ⇒ Object



103
104
105
106
107
108
109
# File 'lib/ashid.rb', line 103

def normalize(id)
  parsed       = parse(id)
  norm_prefix  = parsed[:prefix].empty? ? nil : parsed[:prefix].downcase.chomp("_")
  time_value   = Encoder.decode(parsed[:encoded_timestamp])
  random_value = Encoder.decode(parsed[:encoded_random])
  generate(norm_prefix, time: time_value, random: random_value)
end

#parse(id) ⇒ Object

Raises:



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/ashid.rb', line 35

def parse(id)
  raise InvalidIdError, "id cannot be nil"    if id.nil?
  raise InvalidIdError, "id must be a String" unless id.is_a?(String)
  raise InvalidIdError, "id cannot be empty"  if id.empty?

  prefix_length = 0
  has_delimiter = false

  id.each_char do |ch|
    if ch.match?(/[a-zA-Z]/)
      prefix_length += 1
    elsif (ch == "_" || ch == "-") && prefix_length > 0
      prefix_length += 1
      has_delimiter = true
      break
    else
      prefix_length = 0
      break
    end
  end

  prefix  = has_delimiter ? id[0, prefix_length].sub(/-\z/, "_") : ""
  base_id = has_delimiter ? id[prefix_length..] : id

  raise InvalidIdError, "id must have a base ID" if base_id.nil? || base_id.empty?

  encoded_timestamp, encoded_random = split_base(base_id, has_delimiter)

  { prefix: prefix, encoded_timestamp: encoded_timestamp, encoded_random: encoded_random }
end

#prefix(id) ⇒ Object



66
67
68
# File 'lib/ashid.rb', line 66

def prefix(id)
  parse(id)[:prefix]
end

#random(id) ⇒ Object



80
81
82
# File 'lib/ashid.rb', line 80

def random(id)
  Encoder.decode(parse(id)[:encoded_random])
end

#random_bytes(id) ⇒ Object



84
85
86
# File 'lib/ashid.rb', line 84

def random_bytes(id)
  [random(id).to_s(16).rjust(16, "0")].pack("H*")
end

#time(id) ⇒ Object

Returns a Time. Note: ms-precision via float division to seconds; sub-ms detail is lost but ms is our resolution anyway.



76
77
78
# File 'lib/ashid.rb', line 76

def time(id)
  Time.at(timestamp(id) / 1000.0)
end

#timestamp(id) ⇒ Object



70
71
72
# File 'lib/ashid.rb', line 70

def timestamp(id)
  Encoder.decode(parse(id)[:encoded_timestamp])
end

#valid?(id) ⇒ Boolean

Returns:

  • (Boolean)


88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/ashid.rb', line 88

def valid?(id)
  return false unless id.is_a?(String) && !id.empty?

  parsed = parse(id)
  if !parsed[:prefix].empty? && parsed[:prefix] !~ /\A[a-z0-9]+_\z/
    return false
  end

  Encoder.decode(parsed[:encoded_timestamp])
  Encoder.decode(parsed[:encoded_random])
  true
rescue Error
  false
end