opinionated_http 0.0.5 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +4 -0
- data/Rakefile +2 -0
- data/lib/opinionated_http/client.rb +109 -112
- data/lib/opinionated_http/request.rb +77 -0
- data/lib/opinionated_http/response.rb +55 -0
- data/lib/opinionated_http/version.rb +1 -1
- data/lib/opinionated_http.rb +2 -0
- data/test/client_test.rb +72 -107
- data/test/config/application.yml +1 -1
- data/test/request_test.rb +95 -0
- data/test/test_helper.rb +1 -1
- metadata +11 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 70883413762ba2277b74061e20f9fd5faa73fccee8a762595a40793a93ed2689
|
|
4
|
+
data.tar.gz: 45bf3291006a3577a29f5fd603ad27c34b1d6948c54e67f71efb20862c0af613
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eec7f0a191186d29a970284e04dc571a2c8329587a414fa41576852e59d08503928594e14fc8e65f9ebea1570f90e15ad5e878a7b26259eae4adc605e7e9063f
|
|
7
|
+
data.tar.gz: ac1ce6b6f65d720cf7ddac4d93621c608f609ad1e113ead056e710e0cfe37df70792459940cdd3ca9effe0d8a123e60df11e89cf2a713ab7855c1148ea29e86d
|
data/README.md
CHANGED
|
@@ -36,6 +36,10 @@ Parameters:
|
|
|
36
36
|
Required.
|
|
37
37
|
logger:
|
|
38
38
|
Default: SemanticLogger[OpinionatedHTTP]
|
|
39
|
+
format: [:json | nil]
|
|
40
|
+
Optionally set the format for http requests.
|
|
41
|
+
Currently only supports `:json`, which will format the request and response to/from JSON.
|
|
42
|
+
It will also set the `Content-Type` http header to `application/json`.
|
|
39
43
|
Other options as supported by PersistentHTTP
|
|
40
44
|
#TODO: Expand PersistentHTTP options here
|
|
41
45
|
|
data/Rakefile
CHANGED
|
@@ -10,18 +10,19 @@ module OpinionatedHTTP
|
|
|
10
10
|
# 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout
|
|
11
11
|
HTTP_RETRY_CODES = %w[502 503 504].freeze
|
|
12
12
|
|
|
13
|
-
attr_reader :secret_config_prefix, :logger, :metric_prefix, :error_class, :driver,
|
|
13
|
+
attr_reader :secret_config_prefix, :logger, :format, :metric_prefix, :error_class, :driver, :format,
|
|
14
14
|
:retry_count, :retry_interval, :retry_multiplier, :http_retry_codes,
|
|
15
15
|
:url, :pool_size, :keep_alive, :proxy, :force_retry, :max_redirects,
|
|
16
|
-
:open_timeout, :read_timeout, :idle_timeout, :pool_timeout, :warn_timeout
|
|
16
|
+
:open_timeout, :read_timeout, :idle_timeout, :pool_timeout, :warn_timeout,
|
|
17
|
+
:after_connect
|
|
17
18
|
|
|
18
19
|
# Any option supplied here can be overridden if that corresponding value is set in Secret Config.
|
|
19
|
-
# Except for any values passed directly to Persistent HTTP under `**options`.
|
|
20
20
|
def initialize(
|
|
21
21
|
secret_config_prefix:,
|
|
22
22
|
metric_prefix:,
|
|
23
23
|
error_class:,
|
|
24
24
|
logger: nil,
|
|
25
|
+
format: nil,
|
|
25
26
|
retry_count: 11,
|
|
26
27
|
retry_interval: 0.01,
|
|
27
28
|
retry_multiplier: 1.8,
|
|
@@ -37,93 +38,107 @@ module OpinionatedHTTP
|
|
|
37
38
|
proxy: :ENV,
|
|
38
39
|
force_retry: true,
|
|
39
40
|
max_redirects: 10,
|
|
40
|
-
|
|
41
|
+
after_connect: nil,
|
|
42
|
+
verify_peer: false, # TODO: PersistentHTTP keeps returning cert expired even when it is valid.
|
|
43
|
+
certificate: nil,
|
|
44
|
+
private_key: nil,
|
|
45
|
+
header: nil
|
|
41
46
|
)
|
|
42
|
-
@metric_prefix
|
|
43
|
-
@logger
|
|
44
|
-
@error_class
|
|
45
|
-
@
|
|
46
|
-
@
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
47
|
+
@metric_prefix = metric_prefix
|
|
48
|
+
@logger = logger || SemanticLogger[self]
|
|
49
|
+
@error_class = error_class
|
|
50
|
+
@format = format
|
|
51
|
+
@after_connect = after_connect
|
|
52
|
+
SecretConfig.configure(secret_config_prefix) do |config|
|
|
53
|
+
@retry_count = config.fetch("retry_count", type: :integer, default: retry_count)
|
|
54
|
+
@retry_interval = config.fetch("retry_interval", type: :float, default: retry_interval)
|
|
55
|
+
@retry_multiplier = config.fetch("retry_multiplier", type: :float, default: retry_multiplier)
|
|
56
|
+
@max_redirects = config.fetch("max_redirects", type: :integer, default: max_redirects)
|
|
57
|
+
http_retry_codes = config.fetch("http_retry_codes", type: :string, default: http_retry_codes)
|
|
58
|
+
@http_retry_codes = http_retry_codes.split(",").collect(&:strip)
|
|
59
|
+
|
|
60
|
+
@url = url.nil? ? config.fetch("url") : config.fetch("url", default: url)
|
|
61
|
+
|
|
62
|
+
@pool_size = config.fetch("pool_size", type: :integer, default: pool_size)
|
|
63
|
+
@open_timeout = config.fetch("open_timeout", type: :float, default: open_timeout)
|
|
64
|
+
@read_timeout = config.fetch("read_timeout", type: :float, default: read_timeout)
|
|
65
|
+
@idle_timeout = config.fetch("idle_timeout", type: :float, default: idle_timeout)
|
|
66
|
+
@keep_alive = config.fetch("keep_alive", type: :float, default: keep_alive)
|
|
67
|
+
@pool_timeout = config.fetch("pool_timeout", type: :float, default: pool_timeout)
|
|
68
|
+
@warn_timeout = config.fetch("warn_timeout", type: :float, default: warn_timeout)
|
|
69
|
+
@proxy = config.fetch("proxy", type: :symbol, default: proxy)
|
|
70
|
+
@force_retry = config.fetch("force_retry", type: :boolean, default: force_retry)
|
|
71
|
+
@certificate = config.fetch("certificate", type: :string, default: certificate)
|
|
72
|
+
@private_key = config.fetch("private_key", type: :string, default: private_key)
|
|
73
|
+
@verify_peer = config.fetch("verify_peer", type: :boolean, default: verify_peer)
|
|
74
|
+
end
|
|
63
75
|
|
|
64
76
|
internal_logger = OpinionatedHTTP::Logger.new(@logger)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
@driver = PersistentHTTP.new(
|
|
78
|
+
url: @url,
|
|
79
|
+
logger: internal_logger,
|
|
80
|
+
debug_output: internal_logger,
|
|
81
|
+
name: "",
|
|
82
|
+
pool_size: @pool_size,
|
|
83
|
+
open_timeout: @open_timeout,
|
|
84
|
+
read_timeout: @read_timeout,
|
|
85
|
+
idle_timeout: @idle_timeout,
|
|
86
|
+
keep_alive: @keep_alive,
|
|
87
|
+
pool_timeout: @pool_timeout,
|
|
88
|
+
warn_timeout: @warn_timeout,
|
|
89
|
+
proxy: @proxy,
|
|
90
|
+
force_retry: @force_retry,
|
|
91
|
+
after_connect: @after_connect,
|
|
92
|
+
certificate: @certificate,
|
|
93
|
+
private_key: @private_key,
|
|
94
|
+
verify_mode: @verify_peer ? OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT : OpenSSL::SSL::VERIFY_NONE,
|
|
95
|
+
header: header
|
|
96
|
+
)
|
|
83
97
|
end
|
|
84
98
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
request =
|
|
88
|
-
|
|
89
|
-
|
|
99
|
+
def get(request: nil, **args)
|
|
100
|
+
request ||= Request.new(**args)
|
|
101
|
+
request.verb = "Get"
|
|
102
|
+
http_response = request(request)
|
|
103
|
+
Response.new(http_response, request)
|
|
90
104
|
end
|
|
91
105
|
|
|
92
|
-
def post(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
response = request(action: action, request: request)
|
|
96
|
-
extract_body(response)
|
|
97
|
-
end
|
|
106
|
+
def post(request: nil, json: nil, body: nil, **args)
|
|
107
|
+
raise(ArgumentError, "Either set :json or :body") if json && body
|
|
98
108
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
request ||= Request.new(**args)
|
|
110
|
+
if json
|
|
111
|
+
request.format = :json
|
|
112
|
+
request.body = json
|
|
113
|
+
else
|
|
114
|
+
request.body = body
|
|
102
115
|
end
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
request = Net::HTTP.const_get(verb).new(path, headers)
|
|
116
|
+
request.verb = "Post"
|
|
117
|
+
http_response = request(request)
|
|
118
|
+
Response.new(http_response, request)
|
|
119
|
+
end
|
|
109
120
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
121
|
+
def delete(request: nil, **args)
|
|
122
|
+
request ||= Request.new(**args)
|
|
123
|
+
request.verb = "Delete"
|
|
124
|
+
http_response = request(request)
|
|
125
|
+
Response.new(http_response, request)
|
|
126
|
+
end
|
|
114
127
|
|
|
115
|
-
|
|
116
|
-
request.
|
|
117
|
-
request.
|
|
118
|
-
request
|
|
128
|
+
def patch(request: nil, **args)
|
|
129
|
+
request ||= Request.new(**args)
|
|
130
|
+
request.verb = "Patch"
|
|
131
|
+
http_response = request(request)
|
|
132
|
+
Response.new(http_response, request)
|
|
119
133
|
end
|
|
120
134
|
|
|
121
|
-
# Returns [
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
135
|
+
# Returns [Response] after submitting the [Request]
|
|
136
|
+
def request(request)
|
|
137
|
+
request.metric_prefix ||= metric_prefix
|
|
138
|
+
request.format ||= format
|
|
139
|
+
request.error_class ||= error_class
|
|
140
|
+
request.logger ||= logger
|
|
141
|
+
request_with_retry(action: request.action, request: request.http_request)
|
|
127
142
|
end
|
|
128
143
|
|
|
129
144
|
private
|
|
@@ -134,12 +149,14 @@ module OpinionatedHTTP
|
|
|
134
149
|
begin
|
|
135
150
|
payload = {}
|
|
136
151
|
if logger.trace?
|
|
137
|
-
payload[:parameters] = parameters
|
|
138
|
-
payload[:path]
|
|
152
|
+
# payload[:parameters] = parameters
|
|
153
|
+
payload[:path] = request.path
|
|
139
154
|
end
|
|
140
155
|
message = "HTTP #{http_method}: #{action}" if logger.debug?
|
|
141
156
|
|
|
142
|
-
logger.benchmark_info(message: message, metric: "#{metric_prefix}/#{action}", payload: payload)
|
|
157
|
+
logger.benchmark_info(message: message, metric: "#{metric_prefix}/#{action}", payload: payload) do
|
|
158
|
+
driver.request(request)
|
|
159
|
+
end
|
|
143
160
|
rescue StandardError => e
|
|
144
161
|
message = "HTTP #{http_method}: #{action} Failure: #{e.class.name}: #{e.message}"
|
|
145
162
|
logger.error(message: message, metric: "#{metric_prefix}/exception", exception: e)
|
|
@@ -147,33 +164,19 @@ module OpinionatedHTTP
|
|
|
147
164
|
end
|
|
148
165
|
|
|
149
166
|
# Retry on http 5xx errors except 500 which means internal server error.
|
|
150
|
-
|
|
151
|
-
if try_count < retry_count
|
|
152
|
-
try_count += 1
|
|
153
|
-
duration = retry_sleep_interval(try_count)
|
|
154
|
-
logger.warn(message: "HTTP #{http_method}: #{action} Failure: (#{response.code}) #{response.message}. Retry: #{try_count}", metric: "#{metric_prefix}/retry", duration: duration * 1_000)
|
|
155
|
-
sleep(duration)
|
|
156
|
-
response = request_with_retry(action: action, request: request, try_count: try_count)
|
|
157
|
-
else
|
|
158
|
-
message = "HTTP #{http_method}: #{action} Failure: (#{response.code}) #{response.message}. Retries Exhausted"
|
|
159
|
-
logger.error(message: message, metric: "#{metric_prefix}/exception")
|
|
160
|
-
raise(error_class, message)
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
response
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def extract_body(response)
|
|
168
|
-
return response.body if response.is_a?(Net::HTTPSuccess)
|
|
167
|
+
return response unless http_retry_codes.include?(response.code)
|
|
169
168
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
169
|
+
if try_count >= retry_count
|
|
170
|
+
message = "HTTP #{http_method}: #{action} Failure: (#{response.code}) #{response.message}. Retries Exhausted"
|
|
171
|
+
logger.error(message: message, metric: "#{metric_prefix}/exception")
|
|
172
|
+
raise(error_class, message)
|
|
173
|
+
end
|
|
174
174
|
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
try_count += 1
|
|
176
|
+
duration = retry_sleep_interval(try_count)
|
|
177
|
+
logger.warn(message: "HTTP #{http_method}: #{action} Failure: (#{response.code}) #{response.message}. Retry: #{try_count}", metric: "#{metric_prefix}/retry", duration: duration * 1_000)
|
|
178
|
+
sleep(duration)
|
|
179
|
+
request_with_retry(action: action, request: request, try_count: try_count)
|
|
177
180
|
end
|
|
178
181
|
|
|
179
182
|
# First retry is immediate, next retry is after `retry_interval`,
|
|
@@ -181,13 +184,7 @@ module OpinionatedHTTP
|
|
|
181
184
|
def retry_sleep_interval(retry_count)
|
|
182
185
|
return 0 if retry_count <= 1
|
|
183
186
|
|
|
184
|
-
(retry_multiplier**(retry_count - 1)) * retry_interval
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
def headers_and_form_data_compatible?(headers, form_data)
|
|
188
|
-
return true if headers.nil? || form_data.nil?
|
|
189
|
-
|
|
190
|
-
!headers.keys.map(&:downcase).include?("content-type")
|
|
187
|
+
(retry_multiplier ** (retry_count - 1)) * retry_interval
|
|
191
188
|
end
|
|
192
189
|
end
|
|
193
190
|
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
module OpinionatedHTTP
|
|
2
|
+
# The request object
|
|
3
|
+
class Request
|
|
4
|
+
attr_accessor :action, :verb, :format, :path, :headers, :body, :form_data, :username, :password, :parameters,
|
|
5
|
+
:metric_prefix, :error_class, :logger
|
|
6
|
+
|
|
7
|
+
def initialize(action:, verb: nil, path: nil, format: nil, headers: {}, body: nil, form_data: nil, username: nil, password: nil, parameters: nil)
|
|
8
|
+
@action = action
|
|
9
|
+
@path =
|
|
10
|
+
if path.nil?
|
|
11
|
+
"/#{action}"
|
|
12
|
+
elsif path.start_with?("/")
|
|
13
|
+
path
|
|
14
|
+
else
|
|
15
|
+
"/#{path}"
|
|
16
|
+
end
|
|
17
|
+
@verb = verb
|
|
18
|
+
@format = format
|
|
19
|
+
@headers = headers
|
|
20
|
+
@body = body
|
|
21
|
+
@form_data = form_data
|
|
22
|
+
@username = username
|
|
23
|
+
@password = password
|
|
24
|
+
@parameters = parameters
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def http_request
|
|
28
|
+
unless headers_and_form_data_compatible?(headers, form_data)
|
|
29
|
+
raise(ArgumentError, "Setting form data will overwrite supplied content-type")
|
|
30
|
+
end
|
|
31
|
+
raise(ArgumentError, "Cannot supply both form_data and a body") if body && form_data
|
|
32
|
+
|
|
33
|
+
path_with_params = parameters ? "#{path}?#{URI.encode_www_form(parameters)}" : path
|
|
34
|
+
body = format_body if self.body
|
|
35
|
+
request = Net::HTTP.const_get(verb).new(path_with_params, headers)
|
|
36
|
+
|
|
37
|
+
if body && !request.request_body_permitted?
|
|
38
|
+
raise(ArgumentError, "#{request.class.name} does not support a request body")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if parameters && !request.response_body_permitted?
|
|
42
|
+
raise(ArgumentError, ":parameters cannot be supplied for #{request.class.name}")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
request.body = body if body
|
|
46
|
+
request.set_form_data form_data if form_data
|
|
47
|
+
request.basic_auth(username, password) if username && password
|
|
48
|
+
request
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def format_body
|
|
54
|
+
return if body.nil?
|
|
55
|
+
|
|
56
|
+
case format
|
|
57
|
+
when :json
|
|
58
|
+
headers["Content-Type"] = "application/json"
|
|
59
|
+
body.to_json unless body.is_a?(String) || body.nil?
|
|
60
|
+
when nil
|
|
61
|
+
body
|
|
62
|
+
else
|
|
63
|
+
raise(ArgumentError, "Unknown format: #{format.inspect}")
|
|
64
|
+
end
|
|
65
|
+
rescue StandardError => exc
|
|
66
|
+
message = "Failed to serialize request body. #{exc.class.name}: #{exc.message}"
|
|
67
|
+
logger.error(message: message, metric: "#{metric_prefix}/exception", exception: exc)
|
|
68
|
+
raise(error_class, message)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def headers_and_form_data_compatible?(headers, form_data)
|
|
72
|
+
return true if headers.empty? || form_data.nil?
|
|
73
|
+
|
|
74
|
+
!headers.keys.map(&:downcase).include?("content-type")
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require "forwardable"
|
|
2
|
+
module OpinionatedHTTP
|
|
3
|
+
# The response object
|
|
4
|
+
class Response
|
|
5
|
+
extend Forwardable
|
|
6
|
+
|
|
7
|
+
def_instance_delegators :@http_response, :code, :message
|
|
8
|
+
def_instance_delegators :@request, :verb, :action, :metric_prefix, :format, :path, :error_class, :logger
|
|
9
|
+
|
|
10
|
+
# :action used for logging the action in the error message
|
|
11
|
+
def initialize(http_response, request)
|
|
12
|
+
@http_response = http_response
|
|
13
|
+
@request = request
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def success?
|
|
17
|
+
@http_response.is_a?(Net::HTTPSuccess)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def body
|
|
21
|
+
@body ||= parse_body
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Raises an exception when the HTTP Response is not a success
|
|
25
|
+
def body!
|
|
26
|
+
return body if success?
|
|
27
|
+
|
|
28
|
+
error_message = "HTTP #{verb.upcase}: #{action} Failure: (#{code}) #{message}"
|
|
29
|
+
logger.error(message: error_message, metric: "#{metric_prefix}/exception", payload: {body: body})
|
|
30
|
+
raise(error_class, error_message)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
attr_reader :http_response, :request
|
|
36
|
+
|
|
37
|
+
def parse_body
|
|
38
|
+
return unless http_response.class.body_permitted?
|
|
39
|
+
|
|
40
|
+
case format
|
|
41
|
+
when :json
|
|
42
|
+
JSON.parse(http_response.body)
|
|
43
|
+
when nil
|
|
44
|
+
http_response.body
|
|
45
|
+
else
|
|
46
|
+
raise(ArgumentError, "Unknown format: #{format.inspect}")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
rescue StandardError => exc
|
|
50
|
+
message = "Failed to parse response body. #{exc.class.name}: #{exc.message}"
|
|
51
|
+
logger.error(message: message, metric: "#{metric_prefix}/exception", exception: exc)
|
|
52
|
+
raise(error_class, message)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/opinionated_http.rb
CHANGED
|
@@ -7,6 +7,8 @@ require "opinionated_http/version"
|
|
|
7
7
|
module OpinionatedHTTP
|
|
8
8
|
autoload :Client, "opinionated_http/client"
|
|
9
9
|
autoload :Logger, "opinionated_http/logger"
|
|
10
|
+
autoload :Request, "opinionated_http/request"
|
|
11
|
+
autoload :Response, "opinionated_http/response"
|
|
10
12
|
|
|
11
13
|
#
|
|
12
14
|
# Create a new Opinionated HTTP instance.
|
data/test/client_test.rb
CHANGED
|
@@ -2,135 +2,100 @@ require "net/http"
|
|
|
2
2
|
require "json"
|
|
3
3
|
require_relative "test_helper"
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
end
|
|
5
|
+
class ClientTest < Minitest::Test
|
|
6
|
+
describe OpinionatedHTTP::Client do
|
|
7
|
+
class ServiceError < StandardError
|
|
8
|
+
end
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
end
|
|
10
|
+
let :http do
|
|
11
|
+
OpinionatedHTTP.new(
|
|
12
|
+
secret_config_prefix: "fake_service",
|
|
13
|
+
metric_prefix: "FakeService",
|
|
14
|
+
logger: SemanticLogger["FakeService"],
|
|
15
|
+
error_class: ServiceError
|
|
16
|
+
)
|
|
17
|
+
end
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
assert_equal body, response
|
|
19
|
+
describe "get" do
|
|
20
|
+
it "succeeds" do
|
|
21
|
+
output = {zip: "12345", population: 54_321}
|
|
22
|
+
body = output.to_json
|
|
23
|
+
stub_request(Net::HTTPSuccess, 200, "OK", body) do
|
|
24
|
+
response = http.get(action: "lookup", parameters: {zip: "12345"})
|
|
25
|
+
assert_equal body, response.body!
|
|
29
26
|
end
|
|
30
27
|
end
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
end
|
|
39
|
-
assert_equal body, response
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
it "with form data" do
|
|
43
|
-
output = {zip: "12345", population: 54_321}
|
|
44
|
-
body = output.to_json
|
|
45
|
-
response = stub_request(Net::HTTPSuccess, 200, "OK", body) do
|
|
46
|
-
http.post(action: "lookup", form_data: output)
|
|
29
|
+
it "fails" do
|
|
30
|
+
message = "HTTP GET: lookup Failure: (403) Forbidden"
|
|
31
|
+
error = assert_raises ServiceError do
|
|
32
|
+
stub_request(Net::HTTPForbidden, 403, "Forbidden", "") do
|
|
33
|
+
response = http.get(action: "lookup", parameters: {zip: "12345"})
|
|
34
|
+
response.body!
|
|
47
35
|
end
|
|
48
|
-
assert_equal body, response
|
|
49
36
|
end
|
|
37
|
+
assert_equal message, error.message
|
|
50
38
|
end
|
|
39
|
+
end
|
|
51
40
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
req2 = http.build_request(path: path, verb: get_verb)
|
|
60
|
-
|
|
61
|
-
assert_kind_of Net::HTTP::Post, req
|
|
62
|
-
assert_kind_of Net::HTTP::Get, req2
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
it "returns a request with supplied headers" do
|
|
66
|
-
test_headers = {"test1" => "yes_test_1", "test2" => "yes_test_2"}
|
|
67
|
-
req = http.build_request(path: path, verb: get_verb, headers: test_headers)
|
|
68
|
-
|
|
69
|
-
assert_equal test_headers["test1"], req["test1"]
|
|
70
|
-
assert_equal test_headers["test2"], req["test2"]
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
it "returns a request with supplied body" do
|
|
74
|
-
test_body = "nice bod"
|
|
75
|
-
req = http.build_request(path: path, verb: post_verb, body: test_body)
|
|
76
|
-
|
|
77
|
-
assert_equal test_body, req.body
|
|
41
|
+
describe "post" do
|
|
42
|
+
it "succeeds with body" do
|
|
43
|
+
output = {zip: "12345", population: 54_321}
|
|
44
|
+
body = output.to_json
|
|
45
|
+
stub_request(Net::HTTPSuccess, 200, "OK", body) do
|
|
46
|
+
response = http.post(action: "lookup", body: body)
|
|
47
|
+
assert_equal body, response.body!
|
|
78
48
|
end
|
|
49
|
+
end
|
|
79
50
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
assert_equal
|
|
86
|
-
assert_equal "application/x-www-form-urlencoded", req["Content-Type"]
|
|
51
|
+
it "with form data" do
|
|
52
|
+
output = {zip: "12345", population: 54_321}
|
|
53
|
+
body = output.to_json
|
|
54
|
+
stub_request(Net::HTTPSuccess, 200, "OK", body) do
|
|
55
|
+
response = http.post(action: "lookup", form_data: output)
|
|
56
|
+
assert_equal body, response.body!
|
|
87
57
|
end
|
|
58
|
+
end
|
|
88
59
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
assert_equal req2["authorization"], req["authorization"]
|
|
60
|
+
it "with JSON data" do
|
|
61
|
+
request = {zip: "12345", population: 54_321}
|
|
62
|
+
body = request.to_json
|
|
63
|
+
stub_request(Net::HTTPSuccess, 200, "OK", body) do
|
|
64
|
+
response = http.post(action: "lookup", json: request)
|
|
65
|
+
assert_equal request, response.body!.transform_keys!(&:to_sym)
|
|
97
66
|
end
|
|
67
|
+
end
|
|
98
68
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
assert_raises ArgumentError do
|
|
110
|
-
http.build_request(path: path, verb: post_verb, headers: capitalized_headers, form_data: form_data)
|
|
69
|
+
it "fails with body" do
|
|
70
|
+
message = "HTTP POST: lookup Failure: (403) Forbidden"
|
|
71
|
+
output = {zip: "12345", population: 54_321}
|
|
72
|
+
body = output.to_json
|
|
73
|
+
error = assert_raises ServiceError do
|
|
74
|
+
stub_request(Net::HTTPForbidden, 403, "Forbidden", "") do
|
|
75
|
+
response = http.post(action: "lookup", body: body)
|
|
76
|
+
response.body!
|
|
111
77
|
end
|
|
112
|
-
|
|
113
|
-
assert http.build_request(path: path, verb: post_verb, headers: no_conflict_headers, form_data: form_data)
|
|
114
78
|
end
|
|
79
|
+
assert_equal message, error.message
|
|
80
|
+
end
|
|
115
81
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
http.
|
|
82
|
+
it "fails with form data" do
|
|
83
|
+
output = {zip: "12345", population: 54_321}
|
|
84
|
+
message = "HTTP POST: lookup Failure: (403) Forbidden"
|
|
85
|
+
error = assert_raises ServiceError do
|
|
86
|
+
stub_request(Net::HTTPForbidden, 403, "Forbidden", "") do
|
|
87
|
+
response = http.post(action: "lookup", form_data: output)
|
|
88
|
+
response.body!
|
|
122
89
|
end
|
|
123
|
-
|
|
124
|
-
assert http.build_request(path: path, verb: post_verb, body: body)
|
|
125
|
-
assert http.build_request(path: path, verb: post_verb, form_data: form_data)
|
|
126
90
|
end
|
|
91
|
+
assert_equal message, error.message
|
|
127
92
|
end
|
|
93
|
+
end
|
|
128
94
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
end
|
|
95
|
+
def stub_request(klass, code, msg, body, &block)
|
|
96
|
+
response = klass.new("1.1", code, msg)
|
|
97
|
+
response.stub(:body, body) do
|
|
98
|
+
http.driver.stub(:request, response, &block)
|
|
134
99
|
end
|
|
135
100
|
end
|
|
136
101
|
end
|
data/test/config/application.yml
CHANGED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "json"
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
|
|
5
|
+
class RequestTest < Minitest::Test
|
|
6
|
+
describe OpinionatedHTTP::Request do
|
|
7
|
+
describe "http_request" do
|
|
8
|
+
let(:path) { "/fake_action" }
|
|
9
|
+
let(:post_verb) { "Post" }
|
|
10
|
+
let(:get_verb) { "Get" }
|
|
11
|
+
let(:action) { "fake_action" }
|
|
12
|
+
let :json_request do
|
|
13
|
+
{zip: "12345", population: 54_321}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "creates a request corresponding to the supplied verb" do
|
|
17
|
+
req = OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb).http_request
|
|
18
|
+
req2 = OpinionatedHTTP::Request.new(action: action, path: path, verb: get_verb).http_request
|
|
19
|
+
|
|
20
|
+
assert_kind_of Net::HTTP::Post, req
|
|
21
|
+
assert_kind_of Net::HTTP::Get, req2
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "creates a JSON request" do
|
|
25
|
+
req = OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, format: :json, body: json_request).http_request
|
|
26
|
+
|
|
27
|
+
assert_equal json_request.to_json, req.body
|
|
28
|
+
assert_equal "application/json", req["Content-Type"]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "returns a request with supplied headers" do
|
|
32
|
+
test_headers = {"test1" => "yes_test_1", "test2" => "yes_test_2"}
|
|
33
|
+
req = OpinionatedHTTP::Request.new(action: action, path: path, verb: get_verb, headers: test_headers).http_request
|
|
34
|
+
|
|
35
|
+
assert_equal test_headers["test1"], req["test1"]
|
|
36
|
+
assert_equal test_headers["test2"], req["test2"]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "returns a request with supplied body" do
|
|
40
|
+
test_body = "nice bod"
|
|
41
|
+
req = OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, body: test_body).http_request
|
|
42
|
+
|
|
43
|
+
assert_equal test_body, req.body
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "returns a request with supplied form data in x-www-form-urlencoded Content-Type" do
|
|
47
|
+
test_data = {test1: "yes", test2: "no"}
|
|
48
|
+
expected_string = "test1=yes&test2=no"
|
|
49
|
+
req = OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, form_data: test_data).http_request
|
|
50
|
+
|
|
51
|
+
assert_equal expected_string, req.body
|
|
52
|
+
assert_equal "application/x-www-form-urlencoded", req["Content-Type"]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "add supplied authentication to the request" do
|
|
56
|
+
test_un = "admin"
|
|
57
|
+
test_pw = "hunter2"
|
|
58
|
+
req = OpinionatedHTTP::Request.new(action: action, path: path, verb: get_verb, username: test_un, password: test_pw).http_request
|
|
59
|
+
req2 = Net::HTTP::Get.new(path)
|
|
60
|
+
req2.basic_auth test_un, test_pw
|
|
61
|
+
|
|
62
|
+
assert_equal req2["authorization"], req["authorization"]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "raise an error if supplied content-type header would be overwritten by setting form_data" do
|
|
66
|
+
downcase_headers = {"unimportant" => "blank", "content-type" => "application/json"}
|
|
67
|
+
capitalized_headers = {"Unimportant" => "blank", "Content-Type" => "application/json"}
|
|
68
|
+
no_conflict_headers = {"whatever" => "blank", "irrelevant" => "test"}
|
|
69
|
+
form_data = {thing1: 1, thing2: 2}
|
|
70
|
+
|
|
71
|
+
assert_raises ArgumentError do
|
|
72
|
+
OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, headers: downcase_headers, form_data: form_data).http_request
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
assert_raises ArgumentError do
|
|
76
|
+
OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, headers: capitalized_headers, form_data: form_data).http_request
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
assert OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, headers: no_conflict_headers, form_data: form_data).http_request
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "raise an error if there is a collision between supplied body and form_data" do
|
|
83
|
+
form_data = {thing1: 1, thing2: 2}
|
|
84
|
+
body = "not form data"
|
|
85
|
+
|
|
86
|
+
assert_raises ArgumentError do
|
|
87
|
+
OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, body: body, form_data: form_data).http_request
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
assert OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, body: body).http_request
|
|
91
|
+
assert OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, form_data: form_data).http_request
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: opinionated_http
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Reid Morrison
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2022-03-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: persistent_http
|
|
@@ -55,7 +55,6 @@ dependencies:
|
|
|
55
55
|
description: HTTP Client with retries. Uses PersistentHTTP for http connection pooling,
|
|
56
56
|
Semantic Logger for logging and metrics, and uses Secret Config for its configuration.
|
|
57
57
|
email:
|
|
58
|
-
- reidmo@gmail.com
|
|
59
58
|
executables: []
|
|
60
59
|
extensions: []
|
|
61
60
|
extra_rdoc_files: []
|
|
@@ -65,14 +64,17 @@ files:
|
|
|
65
64
|
- lib/opinionated_http.rb
|
|
66
65
|
- lib/opinionated_http/client.rb
|
|
67
66
|
- lib/opinionated_http/logger.rb
|
|
67
|
+
- lib/opinionated_http/request.rb
|
|
68
|
+
- lib/opinionated_http/response.rb
|
|
68
69
|
- lib/opinionated_http/version.rb
|
|
69
70
|
- test/client_test.rb
|
|
70
71
|
- test/config/application.yml
|
|
72
|
+
- test/request_test.rb
|
|
71
73
|
- test/test_helper.rb
|
|
72
|
-
homepage:
|
|
74
|
+
homepage:
|
|
73
75
|
licenses: []
|
|
74
76
|
metadata: {}
|
|
75
|
-
post_install_message:
|
|
77
|
+
post_install_message:
|
|
76
78
|
rdoc_options: []
|
|
77
79
|
require_paths:
|
|
78
80
|
- lib
|
|
@@ -87,11 +89,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
87
89
|
- !ruby/object:Gem::Version
|
|
88
90
|
version: '0'
|
|
89
91
|
requirements: []
|
|
90
|
-
rubygems_version: 3.
|
|
91
|
-
signing_key:
|
|
92
|
+
rubygems_version: 3.3.7
|
|
93
|
+
signing_key:
|
|
92
94
|
specification_version: 4
|
|
93
95
|
summary: Opinionated HTTP Client
|
|
94
96
|
test_files:
|
|
95
97
|
- test/client_test.rb
|
|
96
98
|
- test/config/application.yml
|
|
99
|
+
- test/request_test.rb
|
|
97
100
|
- test/test_helper.rb
|