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