opinionated_http 0.0.4 → 0.1.0

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: 6279db8b72fbf65adbc3e271ea079be89b2c54227a343c5dd252cf79799e7adf
4
- data.tar.gz: 6951e99ac22b9237ed2a2220bc858cb2e1f1c05b0cd15a4793631226b296ced9
3
+ metadata.gz: e0b33ff41de4de2487017dcb16c8af1bd049d8f59b99af148c2782f5aadbf90f
4
+ data.tar.gz: fbb4ddce12a6aabb67827d124fd579e91d4a2b4def78d74c893dc82687519c96
5
5
  SHA512:
6
- metadata.gz: a1139e62e3fcf12a4ed985d96a9d561c4f7a3b9859fb2296699d5b70942aa53448a50fc0a1a7f3fb97cd7a9ee1bff528c9ef9fdc704ae25ec42eee845e31dd39
7
- data.tar.gz: 954fe864fef5772be8658bbf78f20ec43e0fc32ed6d456025c1ed3bffbd0b638dd880c6c4e1efc66e1a9e2034930410b0f25e41952508891b51c909ee1977589
6
+ metadata.gz: a3b31d0083666203057f05a38bdc0e7a5519af9757df935c9c772ca514bb7f8777c9b52a24edb960b201e5816bca356508d9470bfcd42fdcf83ee3bd622ec51b
7
+ data.tar.gz: b6b9ba368403e795fc19b81a759e30cd03de38fd436c52222db7f7e73f79d4ad38fb1054c7ad6a23a2cddf5c6f55654127e334c76b0f5fd5f0ab3deab606163d
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
@@ -1,23 +1,25 @@
1
1
  # Setup bundler to avoid having to run bundle exec all the time.
2
- require 'rubygems'
3
- require 'bundler/setup'
2
+ require "rubygems"
3
+ require "bundler/setup"
4
4
 
5
- require 'rake/testtask'
6
- require_relative 'lib/opinionated_http/version'
5
+ require "rake/testtask"
6
+ require_relative "lib/opinionated_http/version"
7
7
 
8
8
  task :gem do
9
- system 'gem build opinionated_http.gemspec'
9
+ system "gem build opinionated_http.gemspec"
10
10
  end
11
11
 
12
12
  task publish: :gem do
13
13
  system "git tag -a v#{OpinionatedHTTP::VERSION} -m 'Tagging #{OpinionatedHTTP::VERSION}'"
14
- system 'git push --tags'
14
+ system "git push --tags"
15
15
  system "gem push opinionated_http-#{OpinionatedHTTP::VERSION}.gem"
16
16
  system "rm opinionated_http-#{OpinionatedHTTP::VERSION}.gem"
17
17
  end
18
18
 
19
19
  Rake::TestTask.new(:test) do |t|
20
- t.pattern = 'test/**/*_test.rb'
20
+ t.pattern = "test/**/*_test.rb"
21
21
  t.verbose = true
22
22
  t.warning = false
23
23
  end
24
+
25
+ task default: :test
@@ -1,6 +1,6 @@
1
- require 'persistent_http'
2
- require 'secret_config'
3
- require 'semantic_logger'
1
+ require "persistent_http"
2
+ require "secret_config"
3
+ require "semantic_logger"
4
4
  #
5
5
  # Client http implementation
6
6
  #
@@ -8,125 +8,159 @@ require 'semantic_logger'
8
8
  module OpinionatedHTTP
9
9
  class Client
10
10
  # 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout
11
- HTTP_RETRY_CODES = %w[502 503 504]
12
-
13
- attr_reader :secret_config_prefix, :logger, :metric_prefix, :error_class, :driver,
14
- :retry_count, :retry_interval, :retry_multiplier, :http_retry_codes
15
-
16
- def initialize(secret_config_prefix:, logger: nil, metric_prefix:, error_class:, **options)
17
- @metric_prefix = metric_prefix
18
- @logger = logger || SemanticLogger[self]
19
- @error_class = error_class
20
- @retry_count = SecretConfig.fetch("#{secret_config_prefix}/retry_count", type: :integer, default: 11)
21
- @retry_interval = SecretConfig.fetch("#{secret_config_prefix}/retry_interval", type: :float, default: 0.01)
22
- @retry_multiplier = SecretConfig.fetch("#{secret_config_prefix}/retry_multiplier", type: :float, default: 1.8)
23
- http_retry_codes = SecretConfig.fetch("#{secret_config_prefix}/http_retry_codes", type: :string, default: HTTP_RETRY_CODES.join(","))
24
- @http_retry_codes = http_retry_codes.split(",").collect { |str| str.strip }
11
+ HTTP_RETRY_CODES = %w[502 503 504].freeze
12
+
13
+ attr_reader :secret_config_prefix, :logger, :format, :metric_prefix, :error_class, :driver, :format,
14
+ :retry_count, :retry_interval, :retry_multiplier, :http_retry_codes,
15
+ :url, :pool_size, :keep_alive, :proxy, :force_retry, :max_redirects,
16
+ :open_timeout, :read_timeout, :idle_timeout, :pool_timeout, :warn_timeout,
17
+ :after_connect
18
+
19
+ # Any option supplied here can be overridden if that corresponding value is set in Secret Config.
20
+ def initialize(
21
+ secret_config_prefix:,
22
+ metric_prefix:,
23
+ error_class:,
24
+ logger: nil,
25
+ format: nil,
26
+ retry_count: 11,
27
+ retry_interval: 0.01,
28
+ retry_multiplier: 1.8,
29
+ http_retry_codes: HTTP_RETRY_CODES.join(","),
30
+ url: nil,
31
+ pool_size: 100,
32
+ open_timeout: 10,
33
+ read_timeout: 10,
34
+ idle_timeout: 300,
35
+ keep_alive: 300,
36
+ pool_timeout: 5,
37
+ warn_timeout: 0.25,
38
+ proxy: :ENV,
39
+ force_retry: true,
40
+ max_redirects: 10,
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
+ )
46
+ @metric_prefix = metric_prefix
47
+ @logger = logger || SemanticLogger[self]
48
+ @error_class = error_class
49
+ @format = format
50
+ @after_connect = after_connect
51
+ SecretConfig.configure(secret_config_prefix) do |config|
52
+ @retry_count = config.fetch("retry_count", type: :integer, default: retry_count)
53
+ @retry_interval = config.fetch("retry_interval", type: :float, default: retry_interval)
54
+ @retry_multiplier = config.fetch("retry_multiplier", type: :float, default: retry_multiplier)
55
+ @max_redirects = config.fetch("max_redirects", type: :integer, default: max_redirects)
56
+ http_retry_codes = config.fetch("http_retry_codes", type: :string, default: http_retry_codes)
57
+ @http_retry_codes = http_retry_codes.split(",").collect(&:strip)
58
+
59
+ @url = url.nil? ? config.fetch("url") : config.fetch("url", default: url)
60
+
61
+ @pool_size = config.fetch("pool_size", type: :integer, default: pool_size)
62
+ @open_timeout = config.fetch("open_timeout", type: :float, default: open_timeout)
63
+ @read_timeout = config.fetch("read_timeout", type: :float, default: read_timeout)
64
+ @idle_timeout = config.fetch("idle_timeout", type: :float, default: idle_timeout)
65
+ @keep_alive = config.fetch("keep_alive", type: :float, default: keep_alive)
66
+ @pool_timeout = config.fetch("pool_timeout", type: :float, default: pool_timeout)
67
+ @warn_timeout = config.fetch("warn_timeout", type: :float, default: warn_timeout)
68
+ @proxy = config.fetch("proxy", type: :symbol, default: proxy)
69
+ @force_retry = config.fetch("force_retry", type: :boolean, default: force_retry)
70
+ @certificate = config.fetch("certificate", type: :string, default: certificate)
71
+ @private_key = config.fetch("private_key", type: :string, default: private_key)
72
+ @verify_peer = config.fetch("verify_peer", type: :boolean, default: verify_peer)
73
+ end
25
74
 
26
75
  internal_logger = OpinionatedHTTP::Logger.new(@logger)
27
- new_options = {
28
- logger: internal_logger,
29
- debug_output: internal_logger,
30
- name: "",
31
- pool_size: SecretConfig.fetch("#{secret_config_prefix}/pool_size", type: :integer, default: 100),
32
- open_timeout: SecretConfig.fetch("#{secret_config_prefix}/open_timeout", type: :float, default: 10),
33
- read_timeout: SecretConfig.fetch("#{secret_config_prefix}/read_timeout", type: :float, default: 10),
34
- idle_timeout: SecretConfig.fetch("#{secret_config_prefix}/idle_timeout", type: :float, default: 300),
35
- keep_alive: SecretConfig.fetch("#{secret_config_prefix}/keep_alive", type: :float, default: 300),
36
- pool_timeout: SecretConfig.fetch("#{secret_config_prefix}/pool_timeout", type: :float, default: 5),
37
- warn_timeout: SecretConfig.fetch("#{secret_config_prefix}/warn_timeout", type: :float, default: 0.25),
38
- proxy: SecretConfig.fetch("#{secret_config_prefix}/proxy", type: :symbol, default: :ENV),
39
- force_retry: SecretConfig.fetch("#{secret_config_prefix}/force_retry", type: :boolean, default: true),
40
- }
41
-
42
- url = SecretConfig["#{secret_config_prefix}/url"]
43
- new_options[:url] = url if url
44
- @driver = PersistentHTTP.new(new_options.merge(options))
76
+ @driver = PersistentHTTP.new(
77
+ url: @url,
78
+ logger: internal_logger,
79
+ debug_output: internal_logger,
80
+ name: "",
81
+ pool_size: @pool_size,
82
+ open_timeout: @open_timeout,
83
+ read_timeout: @read_timeout,
84
+ idle_timeout: @idle_timeout,
85
+ keep_alive: @keep_alive,
86
+ pool_timeout: @pool_timeout,
87
+ warn_timeout: @warn_timeout,
88
+ proxy: @proxy,
89
+ force_retry: @force_retry,
90
+ after_connect: @after_connect,
91
+ certificate: @certificate,
92
+ private_key: @private_key,
93
+ verify_mode: @verify_peer ? OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT : OpenSSL::SSL::VERIFY_NONE
94
+ )
45
95
  end
46
96
 
47
- # Perform an HTTP Get against the supplied path
48
- def get(action:, path: "/#{action}", parameters: nil)
49
- path = "/#{path}" unless path.start_with?("/")
50
- path = "#{path}?#{URI.encode_www_form(parameters)}" if parameters
51
-
52
- request = generic_request(path: path, verb: 'Get')
53
- response = request_with_retry(action: action, path: path, request: request)
54
-
55
- response.body
97
+ def get(request: nil, **args)
98
+ request ||= Request.new(**args)
99
+ request.verb = "Get"
100
+ http_response = request(request)
101
+ Response.new(http_response, request)
56
102
  end
57
103
 
58
- def post(action:, path: "/#{action}", headers: nil, body: nil, form_data: nil, username: nil, password: nil)
59
- path = "/#{path}" unless path.start_with?("/")
60
- request = generic_request(path: path, verb: 'Post', headers: headers, body: body, form_data: form_data, auth: auth)
61
-
62
- response = request_with_retry(action: action, path: path, request: request)
63
-
64
- response.body
104
+ def post(request: nil, **args)
105
+ request ||= Request.new(**args)
106
+ request.verb = "Post"
107
+ http_response = request(request)
108
+ Response.new(http_response, request)
65
109
  end
66
110
 
67
- def generic_request(path:, verb:, headers: nil, body: nil, form_data: nil, username: nil, password: nil)
68
- raise(ArgumentError, 'setting form data will overwrite supplied content-type') unless headers_and_form_data_compatible? headers, form_data
69
- raise(ArgumentError, 'setting form data will overwrite supplied body') if body && form_data
70
-
71
- request = Net::HTTP.const_get(verb).new(path, headers)
72
- request.body = body if body
73
- request.set_form_data form_data if form_data
74
- request.basic_auth(username, password) if username && password
75
- request
111
+ # Returns [Response] after submitting the [Request]
112
+ def request(request)
113
+ request.metric_prefix ||= metric_prefix
114
+ request.format ||= format
115
+ request.error_class ||= error_class
116
+ request.logger ||= logger
117
+ request_with_retry(action: request.action, request: request.http_request)
76
118
  end
77
119
 
78
120
  private
79
121
 
80
- def request_with_retry(action:, path: "/#{action}", request:, try_count: 0)
122
+ def request_with_retry(action:, request:, try_count: 0)
81
123
  http_method = request.method.upcase
82
124
  response =
83
125
  begin
84
126
  payload = {}
85
127
  if logger.trace?
86
- payload[:parameters] = parameters
87
- payload[:path] = path
128
+ # payload[:parameters] = parameters
129
+ payload[:path] = request.path
88
130
  end
89
131
  message = "HTTP #{http_method}: #{action}" if logger.debug?
90
132
 
91
- logger.benchmark_info(message: message, metric: "#{metric_prefix}/#{action}", payload: payload) { driver.request(request) }
92
- rescue StandardError => exc
93
- message = "HTTP #{http_method}: #{action} Failure: #{exc.class.name}: #{exc.message}"
94
- logger.error(message: message, metric: "#{metric_prefix}/exception", exception: exc)
133
+ logger.benchmark_info(message: message, metric: "#{metric_prefix}/#{action}", payload: payload) do
134
+ driver.request(request)
135
+ end
136
+ rescue StandardError => e
137
+ message = "HTTP #{http_method}: #{action} Failure: #{e.class.name}: #{e.message}"
138
+ logger.error(message: message, metric: "#{metric_prefix}/exception", exception: e)
95
139
  raise(error_class, message)
96
140
  end
97
141
 
98
142
  # Retry on http 5xx errors except 500 which means internal server error.
99
- if http_retry_codes.include?(response.code)
100
- if try_count < retry_count
101
- try_count = try_count + 1
102
- duration = retry_sleep_interval(try_count)
103
- logger.warn(message: "HTTP #{http_method}: #{action} Failure: (#{response.code}) #{response.message}. Retry: #{try_count}", metric: "#{metric_prefix}/retry", duration: duration * 1_000)
104
- sleep(duration)
105
- response = request_with_retry(action: action, path: path, request: request, try_count: try_count)
106
- else
107
- message = "HTTP #{http_method}: #{action} Failure: (#{response.code}) #{response.message}. Retries Exhausted"
108
- logger.error(message: message, metric: "#{metric_prefix}/exception")
109
- raise(error_class, message)
110
- end
111
- elsif !response.is_a?(Net::HTTPSuccess)
112
- message = "HTTP #{http_method}: #{action} Failure: (#{response.code}) #{response.message}"
143
+ return response unless http_retry_codes.include?(response.code)
144
+
145
+ if try_count >= retry_count
146
+ message = "HTTP #{http_method}: #{action} Failure: (#{response.code}) #{response.message}. Retries Exhausted"
113
147
  logger.error(message: message, metric: "#{metric_prefix}/exception")
114
148
  raise(error_class, message)
115
149
  end
116
150
 
117
- response
151
+ try_count += 1
152
+ duration = retry_sleep_interval(try_count)
153
+ logger.warn(message: "HTTP #{http_method}: #{action} Failure: (#{response.code}) #{response.message}. Retry: #{try_count}", metric: "#{metric_prefix}/retry", duration: duration * 1_000)
154
+ sleep(duration)
155
+ request_with_retry(action: action, request: request, try_count: try_count)
118
156
  end
119
157
 
120
158
  # First retry is immediate, next retry is after `retry_interval`,
121
159
  # each subsequent retry interval is 100% longer than the prior interval.
122
160
  def retry_sleep_interval(retry_count)
123
161
  return 0 if retry_count <= 1
124
- (retry_multiplier ** (retry_count - 1)) * retry_interval
125
- end
126
162
 
127
- def headers_and_form_data_compatible?(headers, form_data)
128
- return true if headers.nil? || form_data.nil?
129
- !headers.keys.map(&:downcase).include? 'content-type'
163
+ (retry_multiplier ** (retry_count - 1)) * retry_interval
130
164
  end
131
165
  end
132
166
  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")
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.4".freeze
2
+ VERSION = "0.1.0".freeze
3
3
  end
@@ -1,12 +1,14 @@
1
- require 'opinionated_http/version'
1
+ require "opinionated_http/version"
2
2
  #
3
3
  # Opinionated HTTP
4
4
  #
5
5
  # An opinionated HTTP Client library using convention over configuration.
6
6
  #
7
7
  module OpinionatedHTTP
8
- autoload :Client, 'opinionated_http/client'
9
- autoload :Logger, 'opinionated_http/logger'
8
+ autoload :Client, "opinionated_http/client"
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
@@ -1,109 +1,93 @@
1
- require 'net/http'
2
- require 'json'
3
- require_relative 'test_helper'
1
+ require "net/http"
2
+ require "json"
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
+ header: {"Content-Type" => "application/json"}
17
+ )
18
+ end
20
19
 
21
- describe "get" do
22
- it 'success' do
23
- # output = {zip: '12345', population: 54321}
24
- # body = output.to_json
25
- # response = Net::HTTPSuccess.new(200, 'OK', body)
26
- # http.driver.stub(:request, response) do
27
- # http.get(action: 'lookup', parameters: {zip: '12345'})
28
- # end
20
+ describe "get" do
21
+ it "succeeds" do
22
+ output = {zip: "12345", population: 54_321}
23
+ body = output.to_json
24
+ stub_request(Net::HTTPSuccess, 200, "OK", body) do
25
+ response = http.get(action: "lookup", parameters: {zip: "12345"})
26
+ assert_equal body, response.body!
29
27
  end
30
28
  end
31
29
 
32
- describe "generic_request" do
33
- let(:path) { '/fake_action' }
34
- let(:post_verb) { 'Post' }
35
- let(:get_verb) { 'Get' }
36
-
37
- it 'creates a request corresponding to the supplied verb' do
38
- req = http.generic_request(path: path, verb: post_verb)
39
- req2 = http.generic_request(path: path, verb: get_verb)
40
-
41
- assert_kind_of Net::HTTP::Post, req
42
- assert_kind_of Net::HTTP::Get, req2
43
- end
44
-
45
- it 'returns a request with supplied headers' do
46
- test_headers = {'test1' => 'yes_test_1', 'test2' => 'yes_test_2'}
47
- req = http.generic_request(path: path, verb: get_verb, headers: test_headers)
48
-
49
- assert_equal test_headers['test1'], req['test1']
50
- assert_equal test_headers['test2'], req['test2']
51
- end
52
-
53
- it 'returns a request with supplied body' do
54
- test_body = "nice bod"
55
- req = http.generic_request(path: path, verb: get_verb, body: test_body)
56
-
57
- assert_equal test_body, req.body
30
+ it "fails" do
31
+ message = "HTTP GET: lookup Failure: (403) Forbidden"
32
+ error = assert_raises ServiceError do
33
+ stub_request(Net::HTTPForbidden, 403, "Forbidden", "") do
34
+ response = http.get(action: "lookup", parameters: {zip: "12345"})
35
+ response.body!
36
+ end
58
37
  end
38
+ assert_equal message, error.message
39
+ end
40
+ end
59
41
 
60
- it 'returns a request with supplied form data in x-www-form-urlencoded Content-Type' do
61
- test_data = {test1: 'yes', test2: 'no'}
62
- expected_string = "test1=yes&test2=no"
63
- req = http.generic_request(path: path, verb: post_verb, form_data: test_data)
64
-
65
- assert_equal expected_string, req.body
66
- assert_equal 'application/x-www-form-urlencoded', req['Content-Type']
42
+ describe "post" do
43
+ it "succeeds with body" do
44
+ output = {zip: "12345", population: 54_321}
45
+ body = output.to_json
46
+ stub_request(Net::HTTPSuccess, 200, "OK", body) do
47
+ response = http.post(action: "lookup", body: body)
48
+ assert_equal body, response.body!
67
49
  end
50
+ end
68
51
 
69
- it 'add supplied authentication to the request' do
70
- test_un = 'admin'
71
- test_pw = 'hunter2'
72
- req = http.generic_request(path: path, verb: get_verb, username: test_un, password: test_pw)
73
- req2 = Net::HTTP::Get.new(path)
74
- req2.basic_auth test_un, test_pw
75
-
76
- assert_equal req2['authorization'], req['authorization']
52
+ it "with form data" do
53
+ output = {zip: "12345", population: 54_321}
54
+ body = output.to_json
55
+ stub_request(Net::HTTPSuccess, 200, "OK", body) do
56
+ response = http.post(action: "lookup", form_data: output)
57
+ assert_equal body, response.body!
77
58
  end
59
+ end
78
60
 
79
- it 'raise an error if supplied content-type header would be overwritten by setting form_data' do
80
- downcase_headers = {'unimportant' => 'blank', 'content-type' => 'application/json'}
81
- capitalized_headers = {'Unimportant' => 'blank', 'Content-Type' => 'application/json'}
82
- no_conflict_headers = {'whatever' => 'blank', 'irrelevant' => 'test'}
83
- form_data = {thing1: 1, thing2: 2}
84
-
85
- assert_raises ArgumentError do
86
- http.generic_request(path: path, verb: post_verb, headers: downcase_headers, form_data: form_data)
87
- end
88
-
89
- assert_raises ArgumentError do
90
- http.generic_request(path: path, verb: post_verb, headers: capitalized_headers, form_data: form_data)
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
+ response = http.post(action: "lookup", body: body)
68
+ response.body!
91
69
  end
92
-
93
- assert http.generic_request(path: path, verb: post_verb, headers: no_conflict_headers, form_data: form_data)
94
70
  end
71
+ assert_equal message, error.message
72
+ end
95
73
 
96
- it 'raise an error if there is a collision between supplied body and form_data' do
97
- form_data = {thing1: 1, thing2: 2}
98
- body = "not form data"
99
-
100
- assert_raises ArgumentError do
101
- http.generic_request(path: path, verb: post_verb, body: body, form_data: form_data)
74
+ it "fails with form data" do
75
+ output = {zip: "12345", population: 54_321}
76
+ message = "HTTP POST: lookup Failure: (403) Forbidden"
77
+ error = assert_raises ServiceError do
78
+ stub_request(Net::HTTPForbidden, 403, "Forbidden", "") do
79
+ response = http.post(action: "lookup", form_data: output)
80
+ response.body!
102
81
  end
103
-
104
- assert http.generic_request(path: path, verb: post_verb, body: body)
105
- assert http.generic_request(path: path, verb: post_verb, form_data: form_data)
106
82
  end
83
+ assert_equal message, error.message
84
+ end
85
+ end
86
+
87
+ def stub_request(klass, code, msg, body, &block)
88
+ response = klass.new("1.1", code, msg)
89
+ response.stub(:body, body) do
90
+ http.driver.stub(:request, response, &block)
107
91
  end
108
92
  end
109
93
  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
@@ -1,14 +1,14 @@
1
- $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
- ENV['TZ'] = 'America/New_York'
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + "/../lib"
2
+ ENV["TZ"] = "America/New_York"
3
3
 
4
- require 'yaml'
5
- require 'minitest/autorun'
6
- require 'awesome_print'
7
- require 'secret_config'
8
- require 'semantic_logger'
9
- require 'opinionated_http'
4
+ require "yaml"
5
+ require "minitest/autorun"
6
+ require "amazing_print"
7
+ require "secret_config"
8
+ require "semantic_logger"
9
+ require "opinionated_http"
10
10
 
11
- SemanticLogger.add_appender(file_name: 'test.log', formatter: :color)
11
+ SemanticLogger.add_appender(file_name: "test.log", formatter: :color)
12
12
  SemanticLogger.default_level = :debug
13
13
 
14
- SecretConfig.use :file, path: 'test', file_name: File.expand_path('config/application.yml', __dir__)
14
+ SecretConfig.use :file, path: "test", file_name: File.expand_path("config/application.yml", __dir__)
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.4
4
+ version: 0.1.0
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-16 00:00:00.000000000 Z
11
+ date: 2022-03-11 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