opinionated_http 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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