Module: RedisClient::Namespace::CommandBuilder

Defined in:
lib/redis_client/namespace/command_builder.rb

Constant Summary collapse

STRATEGIES =

Namespace transformation strategies

{
  # Basic common strategies
  none: ->(cmd, &block) {}, # No transformation
  all: ->(cmd, &block) { cmd.drop(1).each_with_index { |key, i| cmd[i + 1] = block.call(key) } },
  first: ->(cmd, &block) { cmd[1] = block.call(cmd[1]) if cmd[1] },
  second: ->(cmd, &block) { cmd[2] = block.call(cmd[2]) if cmd[2] },
  first_two: lambda { |cmd, &block|
    cmd[1] = block.call(cmd[1]) if cmd[1]
    cmd[2] = block.call(cmd[2]) if cmd[2]
  },
  exclude_first: ->(cmd, &block) { cmd.drop(2).each_with_index { |key, i| cmd[i + 2] = block.call(key) } },
  exclude_last: lambda { |cmd, &block|
    return if cmd.size < 3

    (1...(cmd.size - 1)).each { |i| cmd[i] = block.call(cmd[i]) }
  },
  alternate: lambda { |cmd, &block|
    cmd.drop(1).each_with_index do |item, i|
      cmd[i + 1] = block.call(item) if i.even?
    end
  },

  # Custom strategies used by multiple commands
  eval_style: lambda { |cmd, &block|
    return if cmd.size < 3

    numkeys = cmd[2].to_i
    actual_keys = [numkeys, cmd.size - 3].min
    actual_keys.times { |i| cmd[3 + i] = block.call(cmd[3 + i]) if cmd[3 + i] }
  },

  # Single-command specific strategies
  sort: lambda { |cmd, &block|
    cmd[1] = block.call(cmd[1]) if cmd[1]
    # Handle BY, GET, STORE options
    cmd.each_with_index do |arg, i|
      next if i.zero?

      case arg.to_s.upcase
      when "BY", "STORE"
        cmd[i + 1] = block.call(cmd[i + 1]) if cmd[i + 1]
      when "GET"
        # GET can be "#" or a pattern
        cmd[i + 1] = block.call(cmd[i + 1]) if cmd[i + 1] && cmd[i + 1] != "#"
      end
    end
  },
  georadius_style: lambda { |cmd, &block|
    cmd[1] = block.call(cmd[1]) if cmd[1]
    # Handle STORE, STOREDIST options
    cmd.each_with_index do |arg, i|
      if (arg.to_s.casecmp("STORE").zero? || arg.to_s.casecmp("STOREDIST").zero?) && cmd[i + 1]
        cmd[i + 1] = block.call(cmd[i + 1])
      end
    end
  },
  xread_style: lambda { |cmd, &block|
    # Find STREAMS keyword
    streams_idx = cmd.index { |arg| arg.to_s.casecmp("STREAMS").zero? }
    return unless streams_idx

    # Transform keys after STREAMS
    num_keys = (cmd.size - streams_idx - 1) / 2
    num_keys.times do |i|
      key_idx = streams_idx + 1 + i
      cmd[key_idx] = block.call(cmd[key_idx]) if cmd[key_idx]
    end
  },
  migrate: lambda { |cmd, &block|
    # MIGRATE host port key destination-db timeout [options]
    # MIGRATE host port "" destination-db timeout [COPY | REPLACE] KEYS key [key ...]
    if cmd[3] && cmd[3] != ""
      # Single key format
      cmd[3] = block.call(cmd[3])
    elsif (keys_idx = cmd.index { |arg| arg.to_s.casecmp("KEYS").zero? })
      # Multiple keys format - transform keys after KEYS keyword
      ((keys_idx + 1)...cmd.size).each do |i|
        cmd[i] = block.call(cmd[i]) if cmd[i]
      end
    end
  },
  zinterstore_style: lambda { |cmd, &block|
    # ZINTERSTORE destination numkeys key [key ...]
    return if cmd.size < 3

    cmd[1] = block.call(cmd[1]) if cmd[1] # destination

    numkeys = cmd[2].to_i
    actual_keys = [numkeys, cmd.size - 3].min
    actual_keys.times do |i|
      key_idx = 3 + i
      cmd[key_idx] = block.call(cmd[key_idx]) if cmd[key_idx]
    end
  },
  blmpop_style: lambda { |cmd, &block|
    # BLMPOP timeout numkeys key [key ...] <LEFT | RIGHT> [COUNT count]
    return if cmd.size < 4

    numkeys = cmd[2].to_i
    actual_keys = [numkeys, cmd.size - 3].min
    actual_keys.times do |i|
      key_idx = 3 + i
      cmd[key_idx] = block.call(cmd[key_idx]) if cmd[key_idx]
    end
  },
  lmpop_style: lambda { |cmd, &block|
    # LMPOP numkeys key [key ...] <LEFT | RIGHT> [COUNT count]
    return if cmd.size < 3

    numkeys = cmd[1].to_i
    actual_keys = [numkeys, cmd.size - 2].min
    actual_keys.times do |i|
      key_idx = 2 + i
      cmd[key_idx] = block.call(cmd[key_idx]) if cmd[key_idx]
    end
  },
  scan_cursor_style: lambda { |cmd, &block|
    # SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
    # Only transform MATCH pattern if present and command is SCAN
    if cmd[0].to_s.casecmp("SCAN").zero? && (match_idx = cmd.index do |arg|
      arg.to_s.casecmp("MATCH").zero?
    end) && cmd[match_idx + 1]
      cmd[match_idx + 1] = block.call(cmd[match_idx + 1])
    end
  },
  pubsub_style: lambda { |cmd, &block|
    # PUBSUB CHANNELS [pattern]
    # PUBSUB NUMSUB [channel [channel ...]]
    # PUBSUB SHARDCHANNELS [pattern]
    # PUBSUB SHARDNUMSUB [shardchannel [shardchannel ...]]
    return if cmd.size < 2

    subcommand = cmd[1].to_s.upcase
    case subcommand
    when "CHANNELS", "SHARDCHANNELS"
      # Transform pattern if present
      cmd[2] = block.call(cmd[2]) if cmd[2]
    when "NUMSUB", "SHARDNUMSUB"
      # Transform all channels starting from index 2
      (2...cmd.size).each do |i|
        cmd[i] = block.call(cmd[i]) if cmd[i]
      end
      # NUMPAT has no channels to transform
    end
  },
  scan_style: lambda { |cmd, &block|
    # HSCAN/SSCAN/ZSCAN key cursor [MATCH pattern] [COUNT count]
    # First argument is the key, don't transform MATCH pattern for HSCAN/SSCAN/ZSCAN
    cmd[1] = block.call(cmd[1]) if cmd[1]
  },
  memory_usage: lambda { |cmd, &block|
    # MEMORY USAGE key [SAMPLES samples]
    cmd[2] = block.call(cmd[2]) if cmd.size >= 3 && cmd[1].to_s.casecmp("USAGE").zero? && cmd[2]
  }
}.freeze
COMMANDS =

Command to strategy mapping (inspired by redis-namespace)

{
  # Generic
  "DEL" => :all,
  "EXISTS" => :all,
  "EXPIRE" => :first,
  "EXPIREAT" => :first,
  "KEYS" => :first,
  "MOVE" => :first,
  "PERSIST" => :first,
  "PEXPIRE" => :first,
  "PEXPIREAT" => :first,
  "PTTL" => :first,
  "RANDOMKEY" => :none,
  "RENAME" => :first_two,
  "RENAMENX" => :first_two,
  "RESTORE" => :first,
  "TTL" => :first,
  "TYPE" => :first,
  "UNLINK" => :all,
  "SCAN" => :scan_cursor_style,
  "DUMP" => :first,
  "COPY" => :first_two,
  "MIGRATE" => :migrate,
  "SORT" => :sort,
  "SORT_RO" => :sort,
  "TOUCH" => :all,
  "WAIT" => :none,
  "WAITAOF" => :none,
  "OBJECT" => :second,
  "RESTORE-ASKING" => :first,
  "EXPIRETIME" => :first,
  "PEXPIRETIME" => :first,

  # Bitmap
  "BITCOUNT" => :first,
  "BITOP" => :exclude_first,
  "BITPOS" => :first,
  "BITFIELD" => :first,
  "BITFIELD_RO" => :first,
  "GETBIT" => :first,
  "SETBIT" => :first,

  # String
  "APPEND" => :first,
  "DECR" => :first,
  "DECRBY" => :first,
  "GET" => :first,
  "GETRANGE" => :first,
  "GETSET" => :first,
  "INCR" => :first,
  "INCRBY" => :first,
  "INCRBYFLOAT" => :first,
  "MGET" => :all,
  "MSET" => :alternate,
  "MSETNX" => :alternate,
  "PSETEX" => :first,
  "SET" => :first,
  "SETEX" => :first,
  "SETNX" => :first,
  "SETRANGE" => :first,
  "STRLEN" => :first,
  "GETDEL" => :first,
  "GETEX" => :first,
  "LCS" => :first_two,
  "SUBSTR" => :first,

  # List
  "BLPOP" => :exclude_last,
  "BRPOP" => :exclude_last,
  "BRPOPLPUSH" => :first_two,
  "LINDEX" => :first,
  "LINSERT" => :first,
  "LLEN" => :first,
  "LPOP" => :first,
  "LPUSH" => :first,
  "LPUSHX" => :first,
  "LRANGE" => :first,
  "LREM" => :first,
  "LSET" => :first,
  "LTRIM" => :first,
  "RPOP" => :first,
  "RPOPLPUSH" => :first_two,
  "RPUSH" => :first,
  "RPUSHX" => :first,
  "LMOVE" => :first_two,
  "BLMOVE" => :first_two,
  "LMPOP" => :lmpop_style,
  "BLMPOP" => :blmpop_style,
  "LPOS" => :first,

  # Set
  "SADD" => :first,
  "SCARD" => :first,
  "SDIFF" => :all,
  "SDIFFSTORE" => :all,
  "SINTER" => :all,
  "SINTERSTORE" => :all,
  "SISMEMBER" => :first,
  "SMEMBERS" => :first,
  "SMISMEMBER" => :first,
  "SMOVE" => :first_two,
  "SPOP" => :first,
  "SRANDMEMBER" => :first,
  "SREM" => :first,
  "SUNION" => :all,
  "SUNIONSTORE" => :all,
  "SSCAN" => :scan_style,
  "SINTERCARD" => :lmpop_style,

  # Sorted-set
  "BZPOPMIN" => :exclude_last,
  "BZPOPMAX" => :exclude_last,
  "ZADD" => :first,
  "ZCARD" => :first,
  "ZCOUNT" => :first,
  "ZINCRBY" => :first,
  "ZINTERSTORE" => :zinterstore_style,
  "ZLEXCOUNT" => :first,
  "ZPOPMAX" => :first,
  "ZPOPMIN" => :first,
  "ZRANGE" => :first,
  "ZRANGEBYLEX" => :first,
  "ZREVRANGEBYLEX" => :first,
  "ZRANGEBYSCORE" => :first,
  "ZRANK" => :first,
  "ZREM" => :first,
  "ZREMRANGEBYLEX" => :first,
  "ZREMRANGEBYRANK" => :first,
  "ZREMRANGEBYSCORE" => :first,
  "ZREVRANGE" => :first,
  "ZREVRANGEBYSCORE" => :first,
  "ZREVRANK" => :first,
  "ZSCORE" => :first,
  "ZUNIONSTORE" => :zinterstore_style,
  "ZMSCORE" => :first,
  "ZSCAN" => :scan_style,
  "ZDIFF" => :lmpop_style,
  "ZDIFFSTORE" => :zinterstore_style,
  "ZINTER" => :lmpop_style,
  "ZUNION" => :lmpop_style,
  "ZRANDMEMBER" => :first,
  "BZMPOP" => :blmpop_style,
  "ZMPOP" => :lmpop_style,
  "ZINTERCARD" => :lmpop_style,
  "ZRANGESTORE" => :first_two,

  # Hash
  "HDEL" => :first,
  "HEXISTS" => :first,
  "HGET" => :first,
  "HGETALL" => :first,
  "HINCRBY" => :first,
  "HINCRBYFLOAT" => :first,
  "HKEYS" => :first,
  "HLEN" => :first,
  "HMGET" => :first,
  "HMSET" => :first,
  "HSET" => :first,
  "HSETNX" => :first,
  "HSTRLEN" => :first,
  "HVALS" => :first,
  "HSCAN" => :scan_style,
  "HRANDFIELD" => :first,
  "HEXPIRE" => :first,
  "HEXPIREAT" => :first,
  "HEXPIRETIME" => :first,
  "HPERSIST" => :first,
  "HPEXPIRE" => :first,
  "HPEXPIREAT" => :first,
  "HPEXPIRETIME" => :first,
  "HTTL" => :first,
  "HPTTL" => :first,
  "HGETF" => :first,
  "HSETF" => :first,

  # Hyperloglog
  "PFADD" => :first,
  "PFCOUNT" => :all,
  "PFMERGE" => :all,
  "PFDEBUG" => :second,

  # Geo
  "GEOADD" => :first,
  "GEODIST" => :first,
  "GEOHASH" => :first,
  "GEOPOS" => :first,
  "GEORADIUS" => :georadius_style,
  "GEORADIUSBYMEMBER" => :georadius_style,
  "GEOSEARCH" => :first,
  "GEOSEARCHSTORE" => :first_two,
  "GEORADIUS_RO" => :georadius_style,
  "GEORADIUSBYMEMBER_RO" => :georadius_style,

  # Stream
  "XADD" => :first,
  "XRANGE" => :first,
  "XREVRANGE" => :first,
  "XLEN" => :first,
  "XREAD" => :xread_style,
  "XREADGROUP" => :xread_style,
  "XGROUP" => :second,
  "XACK" => :first,
  "XCLAIM" => :first,
  "XDEL" => :first,
  "XTRIM" => :first,
  "XPENDING" => :first,
  "XINFO" => :second,
  "XAUTOCLAIM" => :first,
  "XSETID" => :first,

  # Pubsub
  "PSUBSCRIBE" => :all,
  "PUBLISH" => :first,
  "PUNSUBSCRIBE" => :all,
  "SUBSCRIBE" => :all,
  "UNSUBSCRIBE" => :all,
  "PUBSUB" => :pubsub_style,
  "SPUBLISH" => :none,
  "SSUBSCRIBE" => :none,
  "SUNSUBSCRIBE" => :none,

  # Transactions
  "DISCARD" => :none,
  "EXEC" => :none,
  "MULTI" => :none,
  "UNWATCH" => :none,
  "WATCH" => :all,

  # Scripting
  "EVAL" => :eval_style,
  "EVALSHA" => :eval_style,
  "SCRIPT" => :none,
  "EVAL_RO" => :eval_style,
  "EVALSHA_RO" => :eval_style,
  "FCALL" => :eval_style,
  "FCALL_RO" => :eval_style,
  "FUNCTION" => :none,

  # Connection
  "AUTH" => :none,
  "ECHO" => :none,
  "PING" => :none,
  "QUIT" => :none,
  "SELECT" => :none,
  "SWAPDB" => :none,
  "RESET" => :none,

  # Server
  "BGREWRITEAOF" => :none,
  "BGSAVE" => :none,
  "CLIENT" => :none,
  "COMMAND" => :none,
  "CONFIG" => :none,
  "DBSIZE" => :none,
  "DEBUG" => :none,
  "FLUSHALL" => :none,
  "FLUSHDB" => :none,
  "INFO" => :none,
  "LASTSAVE" => :none,
  "MEMORY" => :memory_usage,
  "MONITOR" => :none,
  "SAVE" => :none,
  "SHUTDOWN" => :none,
  "SLAVEOF" => :none,
  "SLOWLOG" => :none,
  "SYNC" => :none,
  "TIME" => :none,
  "LATENCY" => :none,
  "LOLWUT" => :none,
  "ACL" => :none,
  "MODULE" => :none,
  "CLUSTER" => :none,
  "HELLO" => :none,
  "FAILOVER" => :none,
  "REPLICAOF" => :none,
  "PSYNC" => :none

}.freeze

Class Method Summary collapse

Class Method Details

.namespaced_command(command, namespace: nil, separator: ":") ⇒ Object



443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# File 'lib/redis_client/namespace/command_builder.rb', line 443

def self.namespaced_command(command, namespace: nil, separator: ":")
  return command if namespace.nil? || namespace.empty? || command.size < 2

  cmd_name = command[0].to_s.upcase
  strategy = COMMANDS[cmd_name]

  # Raise error for unknown commands to maintain compatibility with redis-namespace
  unless strategy
    warn("RedisClient::Namespace does not know how to handle '#{cmd_name}'.")
    return command
  end

  prefix = "#{namespace}#{separator}"
  STRATEGIES[strategy].call(command) { |key| key.start_with?(prefix) ? key : "#{prefix}#{key}" }

  command
end

.trimed_result(command, result, namespace: nil, separator: ":") ⇒ Object



461
462
463
464
465
466
467
468
469
470
471
472
473
# File 'lib/redis_client/namespace/command_builder.rb', line 461

def self.trimed_result(command, result, namespace: nil, separator: ":")
  return command if namespace.nil? || namespace.empty? || command.size < 2

  prefix = "#{namespace}#{separator}"
  case command[0].to_s.upcase
  when "SCAN"
    result[1].map { |r| r.delete_prefix!(prefix) } if result.size > 1
  when "KEYS"
    result.map { |r| r.delete_prefix!(prefix) }
  when "BLPOP", "BRPOP"
    result[0].delete_prefix!(prefix) unless result.nil? || result.empty?
  end
end