estum/console_utils

View on GitHub
lib/console_utils/request_utils/remo.rb

Summary

Maintainability
A
2 hrs
Test Coverage
require 'uri'
require 'open3'
require 'shellwords'
require 'console_utils/request_utils/requester'

module ConsoleUtils::RequestUtils #:nodoc:
  class Remo < Requester
    INSPECT_FORMAT  = "<Remote: %s in %s ms>".freeze
    INSPECT_NOTHING = "<Remote: nothing>".freeze

    attr_reader :request_method

    ConsoleUtils.request_methods.each do |request_method|
      class_eval <<~RUBY, __FILE__, __LINE__ + 1
        def #{request_method}(url, *args)
          @_args = args
          @request_method = "#{request_method.to_s.upcase}"
          @request_params = normalize_args
          @url = urlify(url, @request_params.params)
          perform
        end
      RUBY
    end

    def inspect
      if @url && @_time
        format(INSPECT_FORMAT, @url, @_time)
      else
        INSPECT_NOTHING
      end
    end

    def to_s
      @_body
    end

    attr_reader :_result

    protected

    def perform
      data = @request_params.params.to_json unless params_to_query?
      Curl.(request_method, url, data: data, headers: @request_params.headers) do |result, payload|
        @_result = result
        set_payload!(payload)
      end
    end

    private

    def set_payload!((*body_lines, code, time, size))
      @_body = body_lines.join
      @_code = code.to_i
      @_size = size.to_f
      @_time = time.tr(?,, ?.).to_f
      self
    end

    def urlify(path, options = nil)
      URI.join(ConsoleUtils.remote_endpoint, path).
        tap { |uri| uri.query = options.to_query if options && params_to_query? }.to_s
    end

    def params_to_query?
       ["GET", "HEAD"].include?(@request_method) || @request_params.headers["Content-Type"] != "application/json"
    end

    class Curl
      OUT_FORMAT = '\n%{http_code}\n%{time_total}\n%{size_download}'.freeze
      HEADER_JOIN_PROC = proc { |*kv| ["-H", kv.flatten.join(": ")] }

      def self.call(*args)
        result = new(*args)
        yield(result.to_h, result.payload)
      end

      attr_reader :request, :response, :payload

      def initialize(request_method, url, data: nil, headers: nil)
        cmd = %W(#{ConsoleUtils.curl_bin} --silent -v -g)
        cmd.push("-X#{request_method}")
        cmd.push(url)

        cmd.concat(headers.flat_map(&HEADER_JOIN_PROC)) if headers.present?
        cmd.push("-d", data) if data.present?

        cmd_line = Shellwords.join(cmd)
        cmd_line << %( --write-out "#{OUT_FORMAT}")

        puts "$ #{cmd_line}" if verbose?

        @response = {}
        @request  = {}
        @payload  = []

        Open3.popen3(cmd_line) do |stdin, stdout, stderr, thr|
          # stdin.close
          { stderr: stderr, stdout: stdout }.each do |key, io|
            Thread.new do
              begin
                until (line = io.gets).nil? do
                  key == :stderr ? process_stderr(line) : @payload << line
                end
              rescue => e
                warn e
              end
            end
          end
          thr.join
        end
      end

      KEY_MAP = { ">" => :request, "<" => :response }

      def process_stderr(line)
        # warn(line)
        if type = KEY_MAP[line.chr]
          line = line[2, line.size-1].strip

          return if line.size == 0

          case type
          when :request; set_request(line)
          when :response; set_response(line)
          end
        end
      end

      def set_request(line)
        # warn("Request: #{line}")
        if !@request.key?(:http_version) && line =~ /^(GET|POST|PUT|PATCH|HEAD|OPTION|DELETE) (.+?) HTTP\/(.+)$/
          @request.merge!(method: $1, url: $2, http_version: $3)
        else
          header, value = line.split(": ", 2)
          @request[header] = value
        end
      end

      def set_response(line)
        # warn("Response: #{line}")
        if !@response.key?(:http_version) && line =~ /^HTTP\/(.+) (\d+?) (.+)$/
          @response.merge!(http_version: $1, http_code: $2.to_i, http_status: $3)
        else
          header, value = line.split(": ", 2)
          @response[header] = value
        end
      end

      def to_h
        { response: @response, request: @request }
      end

      def verbose?
        !ConsoleUtils.curl_silence
      end
    end
  end
end