lib/http/response/parser.rb



# frozen_string_literal: true

require "http-parser"

module HTTP
  class Response
    # @api private
    #
    # NOTE(ixti): This class is a subject of future refactoring, thus don't
    #   expect this class API to be stable until this message disappears and
    #   class is not marked as private anymore.
    class Parser
      attr_reader :headers

      def initialize
        @state  = HttpParser::Parser.new_instance { |i| i.type = :response }
        @parser = HttpParser::Parser.new(self)

        reset
      end

      # @return [self]
      def add(data)
        # XXX(ixti): API doc of HttpParser::Parser is misleading, it says that
        #   it returns boolean true if data was parsed successfully, but instead
        #   it's response tells if there was an error; So when it's `true` that
        #   means parse failed, and `false` means parse was successful.
        #   case of success.
        return self unless @parser.parse(@state, data)

        raise IOError, "Could not parse data"
      end
      alias << add

      def headers?
        @finished[:headers]
      end

      def http_version
        @state.http_version
      end

      def status_code
        @state.http_status
      end

      #
      # HTTP::Parser callbacks
      #

      def on_header_field(_response, field)
        @field = field
      end

      def on_header_value(_response, value)
        @headers.add(@field, value) if @field
      end

      def on_headers_complete(_reposse)
        @finished[:headers] = true
      end

      def on_body(_response, chunk)
        if @chunk
          @chunk << chunk
        else
          @chunk = chunk
        end
      end

      def read(size)
        return if @chunk.nil?

        if @chunk.bytesize <= size
          chunk  = @chunk
          @chunk = nil
        else
          chunk = @chunk.byteslice(0, size)
          @chunk[0, size] = ""
        end

        chunk
      end

      def on_message_complete(_response)
        @finished[:message] = true
      end

      def reset
        @state.reset!

        @finished = Hash.new(false)
        @headers  = HTTP::Headers.new
        @field    = nil
        @chunk    = nil
      end

      def finished?
        @finished[:message]
      end
    end
  end
end