Module: Valkey::Utils

Included in:
Valkey
Defined in:
lib/valkey/utils.rb

Overview

Valkey Utils module

This module provides utility functions for transforming and processing data structures commonly used in Valkey commands.

It includes methods for converting values to boolean, hash, or float, as well as methods for handling specific Valkey command responses.

Constant Summary collapse

Boolify =
lambda { |value|
  return value if value.is_a?(TrueClass) || value.is_a?(FalseClass)

  value != 0 unless value.nil?
}
BoolifySet =
lambda { |value|
  case value
  when "OK"
    true
  when nil
    false
  else
    value
  end
}
Hashify =
lambda { |value|
  if value.is_a?(Hash)
    value
  elsif value.respond_to?(:each_slice)
    if value.first.is_a?(Array)
      value.to_h
    else
      value.each_slice(2).to_h
    end
  else
    value
  end
}
Pairify =
lambda { |value|
  if value.respond_to?(:each_slice)
    if value.first.is_a?(Array)
      value
    else
      value.each_slice(2).to_a
    end
  else
    value
  end
}
Floatify =
lambda { |value|
  case value
  when "inf"
    Float::INFINITY
  when "-inf"
    -Float::INFINITY
  when String
    Float(value)
  else
    value
  end
}
FloatifyPair =
lambda { |(first, score)|
  [first, Floatify.call(score)]
}
FloatifyPairs =
lambda { |value|
  return value unless value.respond_to?(:each_slice)

  if value.first.is_a?(Array)
    value.map(&FloatifyPair)
  else
    value.each_slice(2).map(&FloatifyPair)
  end
}
HashifyInfo =
lambda { |reply|
  lines = reply.split("\r\n").grep_v(/^(#|$)/)
  lines.map! { |line| line.split(':', 2) }
  lines.compact!
  lines.to_h
}
HashifyStreams =
lambda { |reply|
  case reply
  when nil
    {}
  else
    reply.transform_values { |entries| HashifyStreamEntries.call(entries) }
  end
}
HashifyStreamEntries =

Normalizes the many shapes that stream-read replies can arrive in (Hash / [[id, pairs], ...] / flat [id, pairs, id, pairs] outer; Hash / [[k, v], ...] / flat [k, v, k, v] inner) into redis-rb's [[id, => value, ...], ...] shape. Duplicate field names in one entry collapse to last-write-wins, same as redis-rb.

lambda { |reply|
  entries =
    case reply
    when Hash
      reply
    when Array
      reply.first.is_a?(Array) ? reply : reply.each_slice(2)
    else
      return []
    end

  entries.map { |entry_id, pairs| [entry_id, Hashify.call(pairs || [])] }
}
HashifyStreamAutoclaim =
lambda { |reply|
  {
    'next' => reply[0],
    'entries' => HashifyStreamEntries.call(reply[1])
  }
}
HashifyStreamAutoclaimJustId =
lambda { |reply|
  {
    'next' => reply[0],
    'entries' => reply[1]
  }
}
HashifyStreamPendings =
lambda { |reply|
  {
    'size' => reply[0],
    'min_entry_id' => reply[1],
    'max_entry_id' => reply[2],
    'consumers' => reply[3].nil? ? {} : reply[3].to_h
  }
}
HashifyStreamPendingDetails =
lambda { |reply|
  reply.map do |arr|
    {
      'entry_id' => arr[0],
      'consumer' => arr[1],
      'elapsed' => arr[2],
      'count' => arr[3]
    }
  end
}
HashifyClusterNodeInfo =
lambda { |str|
  arr = str.split
  {
    'node_id' => arr[0],
    'ip_port' => arr[1],
    'flags' => arr[2].split(','),
    'master_node_id' => arr[3],
    'ping_sent' => arr[4],
    'pong_recv' => arr[5],
    'config_epoch' => arr[6],
    'link_state' => arr[7],
    'slots' => arr[8].nil? ? nil : Range.new(*arr[8].split('-'))
  }
}
HashifyClusterSlots =
lambda { |reply|
  reply.map do |arr|
    first_slot, last_slot = arr[0..1]
    master = { 'ip' => arr[2][0], 'port' => arr[2][1], 'node_id' => arr[2][2] }
    replicas = arr[3..].map { |r| { 'ip' => r[0], 'port' => r[1], 'node_id' => r[2] } }
    {
      'start_slot' => first_slot,
      'end_slot' => last_slot,
      'master' => master,
      'replicas' => replicas
    }
  end
}
HashifyClusterNodes =
lambda { |reply|
  reply.split(/[\r\n]+/).map { |str| HashifyClusterNodeInfo.call(str) }
}
HashifyClusterSlaves =
lambda { |reply|
  reply.map { |str| HashifyClusterNodeInfo.call(str) }
}
Noop =
->(reply) { reply }

Class Method Summary collapse

Class Method Details

.parse_redis_url(url) ⇒ Object

Raises:

  • (ArgumentError)


217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/valkey/utils.rb', line 217

def self.parse_redis_url(url)
  return {} unless url.is_a?(String) && !url.empty?

  # Redact userinfo before it ends up in error messages, logs, or trackers.
  # The regex greedily consumes up to the LAST `@` after `://`, so passwords
  # containing `/` or `@` are still fully redacted (RFC 3986 allows both in
  # percent-decoded form; either can appear raw in a user-supplied URL).
  redacted = url.sub(%r{(\A[^:]+://).*@}, '\1[REDACTED]@').inspect

  uri = begin
    URI.parse(url)
  rescue URI::InvalidURIError
    raise ArgumentError, "Invalid Valkey URL #{redacted}: URI could not be parsed"
  end

  unless ALLOWED_URL_SCHEMES.include?(uri.scheme)
    raise ArgumentError,
          "Invalid Valkey URL #{redacted}: scheme must be one of " \
          "#{ALLOWED_URL_SCHEMES.join(', ')}, got #{uri.scheme.inspect}"
  end

  host = uri.hostname
  raise ArgumentError, "Invalid Valkey URL #{redacted}: missing host" if host.to_s.empty?

  result = {
    host: host,
    port: uri.port || 6379,
    ssl: TLS_URL_SCHEMES.include?(uri.scheme)
  }

  db_segment = uri.path.to_s.delete_prefix('/')
  unless db_segment.empty?
    unless db_segment.match?(/\A\d+\z/)
      raise ArgumentError, "Invalid Valkey URL #{redacted}: database must be a non-negative integer"
    end

    result[:db] = db_segment.to_i
  end

  # `URI.parse` returns userinfo *percent-encoded* (RFC 3986 §3.2.1); we
  # want the raw credential value. Callers (`Valkey#initialize`) will
  # re-encode with `URI::RFC2396_PARSER.escape` when composing the URI
  # they hand to the FFI, so leaving the encoded form here would
  # double-encode (`p%40ss` → `p%2540ss`). See valkey-glide/issues/6659.
  result[:username] = URI::RFC2396_PARSER.unescape(uri.user) if uri.user && !uri.user.empty?
  result[:password] = URI::RFC2396_PARSER.unescape(uri.password) if uri.password && !uri.password.empty?

  result
end