opinionated_http 0.0.6 → 0.1.2
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 +107 -116
- data/lib/opinionated_http/request.rb +76 -0
- data/lib/opinionated_http/response.rb +59 -0
- data/lib/opinionated_http/version.rb +1 -1
- data/lib/opinionated_http.rb +2 -0
- data/test/client_test.rb +72 -140
- data/test/config/application.yml +1 -1
- data/test/request_test.rb +96 -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: cbd58bf8946567e33344521051ead26e88ad8d6d083f820c1528ac644c6ac7ae
|
4
|
+
data.tar.gz: 4dd8282607215174ef3b261f262e9bbbd99396a0f5992df1a6dc652bc58b4008
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 37c91ccf2069493d1cea1243d5673d3fbbd7e7caed6542eb3b61678e1f4f2f34fd964b5521b17042f297482f42921983a5019c788b5b3de10d6739a1d9569239
|
7
|
+
data.tar.gz: fd93c9925f575faabc06809cd0ed665469cd8e2531c91b9a96446e00b64fb1be1b50c3a065378ea73697a0c42f563a352c234dc42f9b8ac309e5413e2accc253
|
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, 'POST', action)
|
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
|
@@ -132,14 +147,10 @@ module OpinionatedHTTP
|
|
132
147
|
http_method = request.method.upcase
|
133
148
|
response =
|
134
149
|
begin
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
payload[:path] = request.path
|
150
|
+
message = "HTTP #{http_method}: #{action} #{request.path}"
|
151
|
+
logger.benchmark_info(message: message, metric: "#{metric_prefix}/#{action}") do
|
152
|
+
driver.request(request)
|
139
153
|
end
|
140
|
-
message = "HTTP #{http_method}: #{action}" if logger.debug?
|
141
|
-
|
142
|
-
logger.benchmark_info(message: message, metric: "#{metric_prefix}/#{action}", payload: payload) { driver.request(request) }
|
143
154
|
rescue StandardError => e
|
144
155
|
message = "HTTP #{http_method}: #{action} Failure: #{e.class.name}: #{e.message}"
|
145
156
|
logger.error(message: message, metric: "#{metric_prefix}/exception", exception: e)
|
@@ -147,33 +158,19 @@ module OpinionatedHTTP
|
|
147
158
|
end
|
148
159
|
|
149
160
|
# 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, http_method, action)
|
168
|
-
return response.body if response.is_a?(Net::HTTPSuccess)
|
161
|
+
return response unless http_retry_codes.include?(response.code)
|
169
162
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
163
|
+
if try_count >= retry_count
|
164
|
+
message = "HTTP #{http_method}: #{action} Failure: (#{response.code}) #{response.message}. Retries Exhausted"
|
165
|
+
logger.error(message: message, metric: "#{metric_prefix}/exception")
|
166
|
+
raise(error_class, message)
|
167
|
+
end
|
174
168
|
|
175
|
-
|
176
|
-
|
169
|
+
try_count += 1
|
170
|
+
duration = retry_sleep_interval(try_count)
|
171
|
+
logger.warn(message: "HTTP #{http_method}: #{action} Failure: (#{response.code}) #{response.message}. Retry: #{try_count}", metric: "#{metric_prefix}/retry", duration: duration * 1_000)
|
172
|
+
sleep(duration)
|
173
|
+
request_with_retry(action: action, request: request, try_count: try_count)
|
177
174
|
end
|
178
175
|
|
179
176
|
# First retry is immediate, next retry is after `retry_interval`,
|
@@ -181,13 +178,7 @@ module OpinionatedHTTP
|
|
181
178
|
def retry_sleep_interval(retry_count)
|
182
179
|
return 0 if retry_count <= 1
|
183
180
|
|
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")
|
181
|
+
(retry_multiplier ** (retry_count - 1)) * retry_interval
|
191
182
|
end
|
192
183
|
end
|
193
184
|
end
|
@@ -0,0 +1,76 @@
|
|
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
|
+
case format
|
55
|
+
when :json
|
56
|
+
headers["Content-Type"] = "application/json"
|
57
|
+
headers["Accept"] = "application/json"
|
58
|
+
body.is_a?(String) || body.nil? ? body : body.to_json
|
59
|
+
when nil
|
60
|
+
body
|
61
|
+
else
|
62
|
+
raise(ArgumentError, "Unknown format: #{format.inspect}")
|
63
|
+
end
|
64
|
+
rescue StandardError => exc
|
65
|
+
message = "Failed to serialize request body. #{exc.class.name}: #{exc.message}"
|
66
|
+
logger.error(message: message, metric: "#{metric_prefix}/exception", exception: exc)
|
67
|
+
raise(error_class, message)
|
68
|
+
end
|
69
|
+
|
70
|
+
def headers_and_form_data_compatible?(headers, form_data)
|
71
|
+
return true if headers.empty? || form_data.nil?
|
72
|
+
|
73
|
+
!headers.keys.map(&:downcase).include?("content-type")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,59 @@
|
|
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
|
+
exception!
|
29
|
+
end
|
30
|
+
|
31
|
+
def exception!
|
32
|
+
error_message = "HTTP #{verb.upcase}: #{action} Failure: (#{code}) #{message}"
|
33
|
+
logger.error(message: error_message, metric: "#{metric_prefix}/exception", payload: {body: body})
|
34
|
+
raise(error_class, error_message)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
attr_reader :http_response, :request
|
40
|
+
|
41
|
+
def parse_body
|
42
|
+
return unless http_response.class.body_permitted?
|
43
|
+
|
44
|
+
case format
|
45
|
+
when :json
|
46
|
+
JSON.parse(http_response.body)
|
47
|
+
when nil
|
48
|
+
http_response.body
|
49
|
+
else
|
50
|
+
raise(ArgumentError, "Unknown format: #{format.inspect}")
|
51
|
+
end
|
52
|
+
|
53
|
+
rescue StandardError => exc
|
54
|
+
message = "Failed to parse response body. #{exc.class.name}: #{exc.message}"
|
55
|
+
logger.error(message: message, metric: "#{metric_prefix}/exception", exception: exc)
|
56
|
+
raise(error_class, message)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
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,168 +2,100 @@ require "net/http"
|
|
2
2
|
require "json"
|
3
3
|
require_relative "test_helper"
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
end
|
10
|
-
|
11
|
-
let :http do
|
12
|
-
OpinionatedHTTP.new(
|
13
|
-
secret_config_prefix: "fake_service",
|
14
|
-
metric_prefix: "FakeService",
|
15
|
-
logger: SemanticLogger["FakeService"],
|
16
|
-
error_class: ServiceError,
|
17
|
-
header: {"Content-Type" => "application/json"}
|
18
|
-
)
|
19
|
-
end
|
5
|
+
class ClientTest < Minitest::Test
|
6
|
+
describe OpinionatedHTTP::Client do
|
7
|
+
class ServiceError < StandardError
|
8
|
+
end
|
20
9
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
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
|
30
18
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
assert_equal message, error.message
|
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!
|
39
26
|
end
|
40
27
|
end
|
41
28
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
end
|
49
|
-
assert_equal body, response
|
50
|
-
end
|
51
|
-
|
52
|
-
it "with form data" do
|
53
|
-
output = {zip: "12345", population: 54_321}
|
54
|
-
body = output.to_json
|
55
|
-
response = stub_request(Net::HTTPSuccess, 200, "OK", body) do
|
56
|
-
http.post(action: "lookup", form_data: output)
|
57
|
-
end
|
58
|
-
assert_equal body, response
|
59
|
-
end
|
60
|
-
|
61
|
-
it "fails with body" do
|
62
|
-
message = "HTTP POST: lookup Failure: (403) Forbidden"
|
63
|
-
output = {zip: "12345", population: 54_321}
|
64
|
-
body = output.to_json
|
65
|
-
error = assert_raises ServiceError do
|
66
|
-
stub_request(Net::HTTPForbidden, 403, "Forbidden", "") do
|
67
|
-
http.post(action: "lookup", body: body)
|
68
|
-
end
|
69
|
-
end
|
70
|
-
assert_equal message, error.message
|
71
|
-
end
|
72
|
-
|
73
|
-
it "fails with form data" do
|
74
|
-
output = {zip: "12345", population: 54_321}
|
75
|
-
message = "HTTP POST: lookup Failure: (403) Forbidden"
|
76
|
-
error = assert_raises ServiceError do
|
77
|
-
stub_request(Net::HTTPForbidden, 403, "Forbidden", "") do
|
78
|
-
http.post(action: "lookup", form_data: output)
|
79
|
-
end
|
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!
|
80
35
|
end
|
81
|
-
assert_equal message, error.message
|
82
36
|
end
|
37
|
+
assert_equal message, error.message
|
83
38
|
end
|
39
|
+
end
|
84
40
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
req2 = http.build_request(path: path, verb: get_verb)
|
93
|
-
|
94
|
-
assert_kind_of Net::HTTP::Post, req
|
95
|
-
assert_kind_of Net::HTTP::Get, req2
|
96
|
-
end
|
97
|
-
|
98
|
-
it "returns a request with supplied headers" do
|
99
|
-
test_headers = {"test1" => "yes_test_1", "test2" => "yes_test_2"}
|
100
|
-
req = http.build_request(path: path, verb: get_verb, headers: test_headers)
|
101
|
-
|
102
|
-
assert_equal test_headers["test1"], req["test1"]
|
103
|
-
assert_equal test_headers["test2"], req["test2"]
|
104
|
-
end
|
105
|
-
|
106
|
-
it "returns a request with supplied body" do
|
107
|
-
test_body = "nice bod"
|
108
|
-
req = http.build_request(path: path, verb: post_verb, body: test_body)
|
109
|
-
|
110
|
-
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!
|
111
48
|
end
|
49
|
+
end
|
112
50
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
assert_equal
|
119
|
-
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!
|
120
57
|
end
|
58
|
+
end
|
121
59
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
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)
|
130
66
|
end
|
67
|
+
end
|
131
68
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
end
|
141
|
-
|
142
|
-
assert_raises ArgumentError do
|
143
|
-
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!
|
144
77
|
end
|
145
|
-
|
146
|
-
assert http.build_request(path: path, verb: post_verb, headers: no_conflict_headers, form_data: form_data)
|
147
78
|
end
|
79
|
+
assert_equal message, error.message
|
80
|
+
end
|
148
81
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
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!
|
155
89
|
end
|
156
|
-
|
157
|
-
assert http.build_request(path: path, verb: post_verb, body: body)
|
158
|
-
assert http.build_request(path: path, verb: post_verb, form_data: form_data)
|
159
90
|
end
|
91
|
+
assert_equal message, error.message
|
160
92
|
end
|
93
|
+
end
|
161
94
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
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)
|
167
99
|
end
|
168
100
|
end
|
169
101
|
end
|
data/test/config/application.yml
CHANGED
@@ -0,0 +1,96 @@
|
|
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"], -> { req.to_hash.ai }
|
29
|
+
assert_equal "application/json", req["accept"], -> { req.to_hash.ai }
|
30
|
+
end
|
31
|
+
|
32
|
+
it "returns a request with supplied headers" do
|
33
|
+
test_headers = {"test1" => "yes_test_1", "test2" => "yes_test_2"}
|
34
|
+
req = OpinionatedHTTP::Request.new(action: action, path: path, verb: get_verb, headers: test_headers).http_request
|
35
|
+
|
36
|
+
assert_equal test_headers["test1"], req["test1"]
|
37
|
+
assert_equal test_headers["test2"], req["test2"]
|
38
|
+
end
|
39
|
+
|
40
|
+
it "returns a request with supplied body" do
|
41
|
+
test_body = "nice bod"
|
42
|
+
req = OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, body: test_body).http_request
|
43
|
+
|
44
|
+
assert_equal test_body, req.body
|
45
|
+
end
|
46
|
+
|
47
|
+
it "returns a request with supplied form data in x-www-form-urlencoded Content-Type" do
|
48
|
+
test_data = {test1: "yes", test2: "no"}
|
49
|
+
expected_string = "test1=yes&test2=no"
|
50
|
+
req = OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, form_data: test_data).http_request
|
51
|
+
|
52
|
+
assert_equal expected_string, req.body
|
53
|
+
assert_equal "application/x-www-form-urlencoded", req["Content-Type"]
|
54
|
+
end
|
55
|
+
|
56
|
+
it "add supplied authentication to the request" do
|
57
|
+
test_un = "admin"
|
58
|
+
test_pw = "hunter2"
|
59
|
+
req = OpinionatedHTTP::Request.new(action: action, path: path, verb: get_verb, username: test_un, password: test_pw).http_request
|
60
|
+
req2 = Net::HTTP::Get.new(path)
|
61
|
+
req2.basic_auth test_un, test_pw
|
62
|
+
|
63
|
+
assert_equal req2["authorization"], req["authorization"]
|
64
|
+
end
|
65
|
+
|
66
|
+
it "raise an error if supplied content-type header would be overwritten by setting form_data" do
|
67
|
+
downcase_headers = {"unimportant" => "blank", "content-type" => "application/json"}
|
68
|
+
capitalized_headers = {"Unimportant" => "blank", "Content-Type" => "application/json"}
|
69
|
+
no_conflict_headers = {"whatever" => "blank", "irrelevant" => "test"}
|
70
|
+
form_data = {thing1: 1, thing2: 2}
|
71
|
+
|
72
|
+
assert_raises ArgumentError do
|
73
|
+
OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, headers: downcase_headers, form_data: form_data).http_request
|
74
|
+
end
|
75
|
+
|
76
|
+
assert_raises ArgumentError do
|
77
|
+
OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, headers: capitalized_headers, form_data: form_data).http_request
|
78
|
+
end
|
79
|
+
|
80
|
+
assert OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, headers: no_conflict_headers, form_data: form_data).http_request
|
81
|
+
end
|
82
|
+
|
83
|
+
it "raise an error if there is a collision between supplied body and form_data" do
|
84
|
+
form_data = {thing1: 1, thing2: 2}
|
85
|
+
body = "not form data"
|
86
|
+
|
87
|
+
assert_raises ArgumentError do
|
88
|
+
OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, body: body, form_data: form_data).http_request
|
89
|
+
end
|
90
|
+
|
91
|
+
assert OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, body: body).http_request
|
92
|
+
assert OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, form_data: form_data).http_request
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
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.2
|
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-24 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
|