main
  1# frozen_string_literal: true
  2
  3module Net
  4  module Hippie
  5    # A simple client for connecting with http resources.
  6    class Client
  7      DEFAULT_HEADERS = {
  8        'Accept' => 'application/json',
  9        'Content-Type' => 'application/json',
 10        'User-Agent' => "net/hippie #{Net::Hippie::VERSION}"
 11      }.freeze
 12
 13      attr_reader :mapper, :logger, :follow_redirects
 14
 15      def initialize(options = {})
 16        @options = options
 17        @mapper = options.fetch(:mapper, ContentTypeMapper.new)
 18        @logger = options.fetch(:logger, Net::Hippie.logger)
 19        @follow_redirects = options.fetch(:follow_redirects, 0)
 20        @default_headers = options.fetch(:headers, DEFAULT_HEADERS)
 21        @connections = Hash.new do |hash, key|
 22          scheme, host, port = key
 23          hash[key] = Connection.new(scheme, host, port, options)
 24        end
 25      end
 26
 27      def execute(uri, request, limit: follow_redirects, &block)
 28        connection = connection_for(uri)
 29        return execute_with_block(connection, request, &block) if block_given?
 30
 31        response = connection.run(request)
 32        follow_redirect?(response, limit) ? follow_redirect(connection, response, limit) : response
 33      end
 34
 35      def get(uri, headers: {}, body: {}, &block)
 36        run(uri, Net::HTTP::Get, headers, body, &block)
 37      end
 38
 39      def patch(uri, headers: {}, body: {}, &block)
 40        run(uri, Net::HTTP::Patch, headers, body, &block)
 41      end
 42
 43      def post(uri, headers: {}, body: {}, &block)
 44        run(uri, Net::HTTP::Post, headers, body, &block)
 45      end
 46
 47      def put(uri, headers: {}, body: {}, &block)
 48        run(uri, Net::HTTP::Put, headers, body, &block)
 49      end
 50
 51      def delete(uri, headers: {}, body: {}, &block)
 52        run(uri, Net::HTTP::Delete, headers, body, &block)
 53      end
 54
 55      # attempt 1 -> delay 0.1 second
 56      # attempt 2 -> delay 0.2 second
 57      # attempt 3 -> delay 0.4 second
 58      # attempt 4 -> delay 0.8 second
 59      # attempt 5 -> delay 1.6 second
 60      # attempt 6 -> delay 3.2 second
 61      # attempt 7 -> delay 6.4 second
 62      # attempt 8 -> delay 12.8 second
 63      def with_retry(retries: 3)
 64        retries = 0 if retries.nil? || retries.negative?
 65
 66        0.upto(retries) do |n|
 67          attempt(n, retries) do
 68            return yield self
 69          end
 70        end
 71      end
 72
 73      private
 74
 75      attr_reader :default_headers
 76
 77      def execute_with_block(connection, request, &block)
 78        block.arity == 2 ? yield(request, connection.run(request)) : connection.run(request, &block)
 79      end
 80
 81      def follow_redirect?(response, limit)
 82        limit.positive? && response.is_a?(Net::HTTPRedirection)
 83      end
 84
 85      def follow_redirect(connection, response, limit)
 86        url = connection.build_url_for(response['location'])
 87        request = request_for(Net::HTTP::Get, url)
 88        execute(url, request, limit: limit - 1)
 89      end
 90
 91      def attempt(attempt, max)
 92        yield
 93      rescue *CONNECTION_ERRORS => error
 94        raise error if attempt == max
 95
 96        delay = ((2**attempt) * 0.1) + Random.rand(0.05) # delay + jitter
 97        logger&.warn("`#{error.message}` #{attempt + 1}/#{max} Delay: #{delay}s")
 98        sleep delay
 99      end
100
101      def request_for(type, uri, headers: {}, body: {})
102        final_headers = default_headers.merge(headers)
103        type.new(URI.parse(uri.to_s), final_headers).tap do |x|
104          x.body = mapper.map_from(final_headers, body) unless body.empty?
105        end
106      end
107
108      def run(uri, http_method, headers, body, &block)
109        request = request_for(http_method, uri, headers: headers, body: body)
110        execute(uri, request, &block)
111      end
112
113      def connection_for(uri)
114        uri = URI.parse(uri.to_s)
115        @connections[[uri.scheme, uri.host, uri.port]]
116      end
117    end
118  end
119end