etehtsea/oxblood

View on GitHub
lib/oxblood/protocol.rb

Summary

Maintainability
A
2 hrs
Test Coverage
module Oxblood
  module Protocol
    SerializerError = Class.new(RuntimeError)
    ParserError = Class.new(RuntimeError)
    RError = Class.new(RuntimeError)

    SIMPLE_STRING = '+'.freeze
    private_constant :SIMPLE_STRING

    ERROR = '-'.freeze
    private_constant :ERROR

    INTEGER = ':'.freeze
    private_constant :INTEGER

    BULK_STRING = '$'.freeze
    private_constant :BULK_STRING

    ARRAY = '*'.freeze
    private_constant :ARRAY

    TERMINATOR = "\r\n".freeze
    private_constant :TERMINATOR

    EMPTY_ARRAY_RESPONSE = "#{ARRAY}0#{TERMINATOR}".freeze
    private_constant :EMPTY_ARRAY_RESPONSE

    NULL_ARRAY_RESPONSE = "#{ARRAY}-1#{TERMINATOR}".freeze
    private_constant :NULL_ARRAY_RESPONSE

    EMPTY_BULK_STRING_RESPONSE = "#{BULK_STRING}0#{TERMINATOR}#{TERMINATOR}".freeze
    private_constant :EMPTY_BULK_STRING_RESPONSE

    NULL_BULK_STRING_RESPONSE = "#{BULK_STRING}-1#{TERMINATOR}".freeze
    private_constant :NULL_BULK_STRING_RESPONSE

    EMPTY_STRING = ''.freeze
    private_constant :EMPTY_STRING

    EMPTY_ARRAY = [].freeze
    private_constant :EMPTY_ARRAY

    COMMAND_HEADER = [ARRAY, TERMINATOR].join.freeze
    private_constant :COMMAND_HEADER

    class << self
      # Parse redis response
      # @see http://redis.io/topics/protocol
      # @raise [ParserError] if unable to parse response
      # @param [#read, #gets] io IO or IO-like object to read from
      # @return [String, RError, Integer, Array]
      def parse(io)
        line = io.gets(TERMINATOR)

        case line[0]
        when SIMPLE_STRING
          line[1..-3]
        when ERROR
          RError.new(line[1..-3])
        when INTEGER
          line[1..-3].to_i
        when BULK_STRING
          return if line == NULL_BULK_STRING_RESPONSE

          body_length = line[1..-1].to_i

          case body_length
          when -1 then nil
          when 0 then
            # discard CRLF
            io.read(2)
            EMPTY_STRING
          else
            # string length plus CRLF
            body = io.read(body_length + 2)
            body[0..-3]
          end
        when ARRAY
          return if line == NULL_ARRAY_RESPONSE
          return EMPTY_ARRAY if line == EMPTY_ARRAY_RESPONSE

          size = line[1..-1].to_i

          Array.new(size) { parse(io) }
        else
          raise ParserError.new('Unsupported response type')
        end
      end

      # Serialize command to string according to Redis Protocol
      # @note Redis don't support nested arrays
      # @note Written in non-idiomatic ruby without error handling due to
      #       performance reasons
      # @see http://www.redis.io/topics/protocol#sending-commands-to-a-redis-server
      #
      # @raise [SerializerError] if unable to serialize given command
      #
      # @param [#to_s] command name
      # @param [Array] args array consisting of command arguments
      #
      # @return [String] serialized command
      def build_command(command = nil, *args)
        return EMPTY_ARRAY_RESPONSE if command.nil?

        result = append!(command, COMMAND_HEADER.dup)
        size = 1
        args.each do |c|
          if Array === c
            c.each do |e|
              append!(e, result)
              size += 1
            end
          else
            append!(c, result)
            size += 1
          end
        end

        result.insert(1, size.to_s)
      end

      private

      def append!(elem, command)
        elem = elem.to_s
        command << BULK_STRING
        command << elem.bytesize.to_s
        command << TERMINATOR
        command << elem
        command << TERMINATOR
      end
    end
  end
end