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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e42aeb5b1a1b2647f48288e3b34b54064b327f72b0cce291ea75a1c0a563eb79
4
- data.tar.gz: 533c7d1bf33a594ebb5b910e25a4ebf0b8420d9e5a24d0aac5f99e83e779fbe4
3
+ metadata.gz: 70883413762ba2277b74061e20f9fd5faa73fccee8a762595a40793a93ed2689
4
+ data.tar.gz: 45bf3291006a3577a29f5fd603ad27c34b1d6948c54e67f71efb20862c0af613
5
5
  SHA512:
6
- metadata.gz: e2555937900d00a7b139a1bee2b76de642065b8b0469aeebb7783934c588bd032dc9b72b211e7ba5b04f06d01f55ece6f7a45d44439f14303c051e71cb4207bc
7
- data.tar.gz: c78cfe4681bfe65cba9d570878dcf78328059d5835e9de0cbff7d416d7ee5954cc4d4fd831f459373027bff9cf37f096ad12f5108ae40fd32db5502fd02ebd12
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
@@ -21,3 +21,5 @@ Rake::TestTask.new(:test) do |t|
21
21
  t.verbose = true
22
22
  t.warning = false
23
23
  end
24
+
25
+ task default: :test
@@ -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
- **options
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 = metric_prefix
43
- @logger = logger || SemanticLogger[self]
44
- @error_class = error_class
45
- @retry_count = SecretConfig.fetch("#{secret_config_prefix}/retry_count", type: :integer, default: retry_count)
46
- @retry_interval = SecretConfig.fetch("#{secret_config_prefix}/retry_interval", type: :float, default: retry_interval)
47
- @retry_multiplier = SecretConfig.fetch("#{secret_config_prefix}/retry_multiplier", type: :float, default: retry_multiplier)
48
- @max_redirects = SecretConfig.fetch("#{secret_config_prefix}/max_redirects", type: :integer, default: max_redirects)
49
- http_retry_codes = SecretConfig.fetch("#{secret_config_prefix}/http_retry_codes", type: :string, default: http_retry_codes)
50
- @http_retry_codes = http_retry_codes.split(",").collect(&:strip)
51
-
52
- @url = url.nil? ? SecretConfig["#{secret_config_prefix}/url"] : SecretConfig.fetch("#{secret_config_prefix}/url", default: url)
53
-
54
- @pool_size = SecretConfig.fetch("#{secret_config_prefix}/pool_size", type: :integer, default: pool_size)
55
- @open_timeout = SecretConfig.fetch("#{secret_config_prefix}/open_timeout", type: :float, default: open_timeout)
56
- @read_timeout = SecretConfig.fetch("#{secret_config_prefix}/read_timeout", type: :float, default: read_timeout)
57
- @idle_timeout = SecretConfig.fetch("#{secret_config_prefix}/idle_timeout", type: :float, default: idle_timeout)
58
- @keep_alive = SecretConfig.fetch("#{secret_config_prefix}/keep_alive", type: :float, default: keep_alive)
59
- @pool_timeout = SecretConfig.fetch("#{secret_config_prefix}/pool_timeout", type: :float, default: pool_timeout)
60
- @warn_timeout = SecretConfig.fetch("#{secret_config_prefix}/warn_timeout", type: :float, default: warn_timeout)
61
- @proxy = SecretConfig.fetch("#{secret_config_prefix}/proxy", type: :symbol, default: proxy)
62
- @force_retry = SecretConfig.fetch("#{secret_config_prefix}/force_retry", type: :boolean, default: force_retry)
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
- new_options = {
66
- logger: internal_logger,
67
- debug_output: internal_logger,
68
- name: "",
69
- pool_size: @pool_size,
70
- open_timeout: @open_timeout,
71
- read_timeout: @read_timeout,
72
- idle_timeout: @idle_timeout,
73
- keep_alive: @keep_alive,
74
- pool_timeout: @pool_timeout,
75
- warn_timeout: @warn_timeout,
76
- proxy: @proxy,
77
- force_retry: @force_retry
78
- }
79
-
80
- url = SecretConfig["#{secret_config_prefix}/url"]
81
- new_options[:url] = url if url
82
- @driver = PersistentHTTP.new(new_options.merge(options))
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
- # Perform an HTTP Get against the supplied path
86
- def get(action:, path: "/#{action}", **args)
87
- request = build_request(path: path, verb: "Get", **args)
88
- response = request(action: action, request: request)
89
- extract_body(response)
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(action:, path: "/#{action}", **args)
93
- request = build_request(path: path, verb: "Post", **args)
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
- def build_request(verb:, path:, headers: nil, body: nil, form_data: nil, username: nil, password: nil, parameters: nil)
100
- unless headers_and_form_data_compatible?(headers, form_data)
101
- raise(ArgumentError, "Setting form data will overwrite supplied content-type")
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
- raise(ArgumentError, "Cannot supply both form_data and a body") if body && form_data
104
-
105
- path = "/#{path}" unless path.start_with?("/")
106
- path = "#{path}?#{URI.encode_www_form(parameters)}" if parameters
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
- raise(ArgumentError, "#{request.class.name} does not support a request body") if body && !request.request_body_permitted?
111
- if parameters && !request.response_body_permitted?
112
- raise(ArgumentError, ":parameters cannot be supplied for #{request.class.name}")
113
- end
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
- request.body = body if body
116
- request.set_form_data form_data if form_data
117
- request.basic_auth(username, password) if username && password
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 [HTTP Response] after submitting the request
122
- #
123
- # Notes:
124
- # - Does not raise an exception when the http response is not an HTTP OK (200.
125
- def request(action:, request:)
126
- request_with_retry(action: action, request: request)
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] = request.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) { driver.request(request) }
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
- if http_retry_codes.include?(response.code)
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
- message = "HTTP #{http_method}: #{action} Failure: (#{response.code}) #{response.message}"
171
- logger.error(message: message, metric: "#{metric_prefix}/exception")
172
- raise(error_class, message)
173
- end
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
- def prefix_path(path)
176
- path.start_with?("/") ? path : "/#{path}"
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
@@ -1,3 +1,3 @@
1
1
  module OpinionatedHTTP
2
- VERSION = "0.0.5".freeze
2
+ VERSION = "0.1.1".freeze
3
3
  end
@@ -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
- module OpinionatedHTTP
6
- class ClientTest < Minitest::Test
7
- describe OpinionatedHTTP::Client do
8
- class ServiceError < StandardError
9
- end
5
+ class ClientTest < Minitest::Test
6
+ describe OpinionatedHTTP::Client do
7
+ class ServiceError < StandardError
8
+ end
10
9
 
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
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
- describe "get" do
22
- it "succeeds" do
23
- output = {zip: "12345", population: 54_321}
24
- body = output.to_json
25
- response = stub_request(Net::HTTPSuccess, 200, "OK", body) do
26
- http.get(action: "lookup", parameters: {zip: "12345"})
27
- end
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
- describe "post" do
33
- it "succeeds with body" do
34
- output = {zip: "12345", population: 54_321}
35
- body = output.to_json
36
- response = stub_request(Net::HTTPSuccess, 200, "OK", body) do
37
- http.post(action: "lookup", body: body)
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
- describe "build_request" do
53
- let(:path) { "/fake_action" }
54
- let(:post_verb) { "Post" }
55
- let(:get_verb) { "Get" }
56
-
57
- it "creates a request corresponding to the supplied verb" do
58
- req = http.build_request(path: path, verb: post_verb)
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
- it "returns a request with supplied form data in x-www-form-urlencoded Content-Type" do
81
- test_data = {test1: "yes", test2: "no"}
82
- expected_string = "test1=yes&test2=no"
83
- req = http.build_request(path: path, verb: post_verb, form_data: test_data)
84
-
85
- assert_equal expected_string, req.body
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
- it "add supplied authentication to the request" do
90
- test_un = "admin"
91
- test_pw = "hunter2"
92
- req = http.build_request(path: path, verb: get_verb, username: test_un, password: test_pw)
93
- req2 = Net::HTTP::Get.new(path)
94
- req2.basic_auth test_un, test_pw
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
- it "raise an error if supplied content-type header would be overwritten by setting form_data" do
100
- downcase_headers = {"unimportant" => "blank", "content-type" => "application/json"}
101
- capitalized_headers = {"Unimportant" => "blank", "Content-Type" => "application/json"}
102
- no_conflict_headers = {"whatever" => "blank", "irrelevant" => "test"}
103
- form_data = {thing1: 1, thing2: 2}
104
-
105
- assert_raises ArgumentError do
106
- http.build_request(path: path, verb: post_verb, headers: downcase_headers, form_data: form_data)
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
- it "raise an error if there is a collision between supplied body and form_data" do
117
- form_data = {thing1: 1, thing2: 2}
118
- body = "not form data"
119
-
120
- assert_raises ArgumentError do
121
- http.build_request(path: path, verb: post_verb, body: body, form_data: form_data)
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
- def stub_request(klass, code, msg, body, &block)
130
- response = klass.new("1.1", code, msg)
131
- response.stub(:body, body) do
132
- http.driver.stub(:request, response, &block)
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
@@ -1,4 +1,4 @@
1
1
  test:
2
- fake_supplier:
2
+ fake_service:
3
3
  url: https://example.org
4
4
 
@@ -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
@@ -3,7 +3,7 @@ ENV["TZ"] = "America/New_York"
3
3
 
4
4
  require "yaml"
5
5
  require "minitest/autorun"
6
- require "awesome_print"
6
+ require "amazing_print"
7
7
  require "secret_config"
8
8
  require "semantic_logger"
9
9
  require "opinionated_http"
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.0.5
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: 2020-04-29 00:00:00.000000000 Z
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.0.8
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