berkeley_library-util 0.1.5 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,19 +6,23 @@ require 'berkeley_library/logging'
6
6
  module BerkeleyLibrary
7
7
  module Util
8
8
  module URIs
9
- module Requester
10
- class << self
11
- include BerkeleyLibrary::Logging
9
+ class Requester
10
+ include BerkeleyLibrary::Logging
11
+
12
+ # ------------------------------------------------------------
13
+ # Class methods
12
14
 
15
+ class << self
13
16
  # Performs a GET request and returns the response body as a string.
14
17
  #
15
18
  # @param uri [URI, String] the URI to GET
16
19
  # @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
17
20
  # @param headers [Hash] the request headers.
18
21
  # @return [String] the body as a string.
22
+ # @param log [Boolean] whether to log each request URL and response code
19
23
  # @raise [RestClient::Exception] in the event of an unsuccessful request.
20
- def get(uri, params: {}, headers: {})
21
- resp = make_request(:get, uri, params, headers)
24
+ def get(uri, params: {}, headers: {}, log: true)
25
+ resp = make_request(:get, uri, params, headers, log)
22
26
  resp.body
23
27
  end
24
28
 
@@ -29,9 +33,10 @@ module BerkeleyLibrary
29
33
  # @param uri [URI, String] the URI to HEAD
30
34
  # @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
31
35
  # @param headers [Hash] the request headers.
36
+ # @param log [Boolean] whether to log each request URL and response code
32
37
  # @return [Integer] the response code as an integer.
33
- def head(uri, params: {}, headers: {})
34
- head_response(uri, params: params, headers: headers).code
38
+ def head(uri, params: {}, headers: {}, log: true)
39
+ head_response(uri, params: params, headers: headers, log: log).code
35
40
  end
36
41
 
37
42
  # Performs a GET request and returns the response, even in the event of
@@ -40,9 +45,10 @@ module BerkeleyLibrary
40
45
  # @param uri [URI, String] the URI to GET
41
46
  # @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
42
47
  # @param headers [Hash] the request headers.
48
+ # @param log [Boolean] whether to log each request URL and response code
43
49
  # @return [RestClient::Response] the body as a string.
44
- def get_response(uri, params: {}, headers: {})
45
- make_request(:get, uri, params, headers)
50
+ def get_response(uri, params: {}, headers: {}, log: true)
51
+ make_request(:get, uri, params, headers, log)
46
52
  rescue RestClient::Exception => e
47
53
  e.response
48
54
  end
@@ -53,56 +59,105 @@ module BerkeleyLibrary
53
59
  # @param uri [URI, String] the URI to HEAD
54
60
  # @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
55
61
  # @param headers [Hash] the request headers.
56
- # @return [RestClient::Response] the body as a string.
57
- def head_response(uri, params: {}, headers: {})
58
- make_request(:head, uri, params, headers)
62
+ # @param log [Boolean] whether to log each request URL and response code
63
+ # @return [RestClient::Response] the response
64
+ def head_response(uri, params: {}, headers: {}, log: true)
65
+ make_request(:head, uri, params, headers, log)
59
66
  rescue RestClient::Exception => e
60
67
  e.response
61
68
  end
62
69
 
63
70
  private
64
71
 
65
- # @return [RestClient::Response]
66
- def make_request(method, uri, params, headers)
67
- url_str = url_str_with_params(uri, params)
68
- req_resp_or_raise(method, url_str, headers)
72
+ def make_request(method, url, params, headers, log)
73
+ Requester.new(method, url, params: params, headers: headers, log: log).make_request
69
74
  end
75
+ end
76
+
77
+ # ------------------------------------------------------------
78
+ # Constants
79
+
80
+ SUPPORTED_METHODS = %i[get head].freeze
70
81
 
71
- def url_str_with_params(uri, params)
72
- raise ArgumentError, 'uri cannot be nil' unless (url_str = Validator.url_str_or_nil(uri))
82
+ # ------------------------------------------------------------
83
+ # Attributes
73
84
 
74
- elements = [].tap do |ee|
75
- ee << url_str
76
- next if params.empty?
85
+ attr_reader :method, :url_str, :headers, :log
77
86
 
78
- ee << '?' unless url_str.include?('?')
79
- ee << URI.encode_www_form(params)
80
- end
87
+ # ------------------------------------------------------------
88
+ # Initializer
81
89
 
82
- uri = Appender.new(*elements).to_uri
83
- uri.to_s
90
+ # Initializes a new Requester.
91
+ #
92
+ # @param method [:get, :head] the HTTP method to use
93
+ # @param url [String, URI] the URL or URI to request
94
+ # @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
95
+ # @param headers [Hash] the request headers.
96
+ # @param log [Boolean] whether to log each request URL and response code
97
+ # @raise URI::InvalidURIError if the specified URL is invalid
98
+ def initialize(method, url, params: {}, headers: {}, log: true)
99
+ raise ArgumentError, "#{method} not supported" unless SUPPORTED_METHODS.include?(method)
100
+ raise ArgumentError, 'url cannot be nil' unless (uri = Validator.uri_or_nil(url))
101
+
102
+ @method = method
103
+ @url_str = url_str_with_params(uri, params)
104
+ @headers = headers
105
+ @log = log
106
+ end
107
+
108
+ # ------------------------------------------------------------
109
+ # Public instance methods
110
+
111
+ # @return [RestClient::Response]
112
+ def make_request
113
+ execute_request.tap do |resp|
114
+ log_response(resp)
84
115
  end
116
+ rescue RestClient::Exception => e
117
+ log_response(e.response)
118
+ raise
119
+ end
120
+
121
+ # ------------------------------------------------------------
122
+ # Private methods
85
123
 
86
- # @return [RestClient::Response]
87
- def req_resp_or_raise(method, url_str, headers)
88
- resp = RestClient::Request.execute(method: method, url: url_str, headers: headers)
89
- begin
90
- return resp if (status = resp.code) == 200
91
-
92
- raise(exception_for(resp, status))
93
- ensure
94
- # noinspection RubyMismatchedReturnType
95
- logger.info("#{method.to_s.upcase} #{url_str} returned #{status}")
96
- end
124
+ private
125
+
126
+ def log_response(response)
127
+ return unless log
128
+
129
+ logger.info("#{method.to_s.upcase} #{url_str} returned #{response.code}")
130
+ end
131
+
132
+ def url_str_with_params(uri, params)
133
+ elements = [uri]
134
+ if params.any?
135
+ elements << (uri.query ? '&' : '?')
136
+ elements << URI.encode_www_form(params)
137
+ end
138
+
139
+ Appender.new(*elements).to_url_str
140
+ end
141
+
142
+ def execute_request
143
+ RestClient::Request.execute(method: method, url: url_str, headers: headers).tap do |response|
144
+ # Not all failed RestClient requests throw exceptions
145
+ raise(exception_for(response)) unless response.code == 200
97
146
  end
147
+ end
98
148
 
99
- def exception_for(resp, status)
100
- RestClient::RequestFailed.new(resp, status).tap do |ex|
101
- status_message = RestClient::STATUSES[status] || '(Unknown)'
102
- ex.message = "#{status} #{status_message}"
103
- end
149
+ def exception_for(resp)
150
+ status = resp.code
151
+ ex_class_for(status).new(resp, status).tap do |ex|
152
+ status_message = RestClient::STATUSES[status] || '(Unknown)'
153
+ ex.message = "#{status} #{status_message}"
104
154
  end
105
155
  end
156
+
157
+ def ex_class_for(status)
158
+ RestClient::Exceptions::EXCEPTIONS_MAP[status] || RestClient::RequestFailed
159
+ end
160
+
106
161
  end
107
162
  end
108
163
  end
@@ -31,10 +31,11 @@ module BerkeleyLibrary
31
31
  # @param uri [URI, String] the URI to GET
32
32
  # @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
33
33
  # @param headers [Hash] the request headers.
34
+ # @param log [Boolean] whether to log each request URL and response code
34
35
  # @return [String] the body as a string.
35
36
  # @raise [RestClient::Exception] in the event of an unsuccessful request.
36
- def get(uri, params: {}, headers: {})
37
- Requester.get(uri, params: params, headers: headers)
37
+ def get(uri, params: {}, headers: {}, log: true)
38
+ Requester.get(uri, params: params, headers: headers, log: log)
38
39
  end
39
40
 
40
41
  # Performs a HEAD request and returns the response status as an integer.
@@ -44,9 +45,10 @@ module BerkeleyLibrary
44
45
  # @param uri [URI, String] the URI to HEAD
45
46
  # @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
46
47
  # @param headers [Hash] the request headers.
48
+ # @param log [Boolean] whether to log each request URL and response code
47
49
  # @return [Integer] the response code as an integer.
48
- def head(uri, params: {}, headers: {})
49
- Requester.head(uri, params: params, headers: headers)
50
+ def head(uri, params: {}, headers: {}, log: true)
51
+ Requester.head(uri, params: params, headers: headers, log: log)
50
52
  end
51
53
 
52
54
  # Performs a GET request and returns the response, even in the event of
@@ -55,9 +57,10 @@ module BerkeleyLibrary
55
57
  # @param uri [URI, String] the URI to GET
56
58
  # @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
57
59
  # @param headers [Hash] the request headers.
58
- # @return [RestClient::Response] the body as a string.
59
- def get_response(uri, params: {}, headers: {})
60
- Requester.get_response(uri, params: params, headers: headers)
60
+ # @param log [Boolean] whether to log each request URL and response code
61
+ # @return [RestClient::Response] the response
62
+ def get_response(uri, params: {}, headers: {}, log: true)
63
+ Requester.get_response(uri, params: params, headers: headers, log: log)
61
64
  end
62
65
 
63
66
  # Performs a HEAD request and returns the response, even in the event of
@@ -66,9 +69,10 @@ module BerkeleyLibrary
66
69
  # @param uri [URI, String] the URI to HEAD
67
70
  # @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
68
71
  # @param headers [Hash] the request headers.
69
- # @return [RestClient::Response] the body as a string.
70
- def head_response(uri, params: {}, headers: {})
71
- Requester.head_response(uri, params: params, headers: headers)
72
+ # @param log [Boolean] whether to log each request URL and response code
73
+ # @return [RestClient::Response] the response
74
+ def head_response(uri, params: {}, headers: {}, log: true)
75
+ Requester.head_response(uri, params: params, headers: headers, log: log)
72
76
  end
73
77
 
74
78
  # Returns the specified URL as a URI, or `nil` if the URL is `nil`.
@@ -115,7 +119,7 @@ module BerkeleyLibrary
115
119
 
116
120
  def should_escape?(b, mode)
117
121
  return false if unreserved?(b)
118
- return false if ALLOWED_BYTES_BY_MODE[mode]&.include?(b)
122
+ return false if ALLOWED_BYTES_BY_MODE[mode].include?(b)
119
123
 
120
124
  true
121
125
  end
@@ -0,0 +1,4 @@
1
+ inherit_from: ../.rubocop.yml
2
+
3
+ require:
4
+ - rubocop-rake
data/spec/.rubocop.yml CHANGED
@@ -1,5 +1,8 @@
1
1
  inherit_from: ../.rubocop.yml
2
2
 
3
+ require:
4
+ - rubocop-rspec
5
+
3
6
  AllCops:
4
7
  # Exclude generated files
5
8
  Exclude:
@@ -38,3 +41,89 @@ Metrics/MethodLength:
38
41
  # Sometimes we're testing the operator
39
42
  Lint/BinaryOperatorWithIdenticalOperands:
40
43
  Enabled: false
44
+
45
+ ############################################################
46
+ # rubocop-rspec
47
+
48
+ # believe me, it wasn't by choice
49
+ RSpec/AnyInstance:
50
+ Enabled: false
51
+
52
+ # we meant to do that
53
+ RSpec/BeforeAfterAll:
54
+ Enabled: false
55
+
56
+ # more words != more readable
57
+ RSpec/ContextWording:
58
+ Enabled: false
59
+
60
+ # explicit >>> implicit
61
+ RSpec/DescribedClass:
62
+ Enabled: false
63
+
64
+ # more punctuation != more readable
65
+ RSpec/DescribeSymbol:
66
+ Enabled: false
67
+
68
+ # setup cost / time >>> failure granularity
69
+ RSpec/ExampleLength:
70
+ Max: 15
71
+ CountAsOne:
72
+ - array
73
+ - hash
74
+ - heredoc
75
+
76
+ # we meant to do that
77
+ RSpec/ExpectInHook:
78
+ Enabled: false
79
+
80
+ # your naming scheme is not in possession of all the facts
81
+ RSpec/FilePath:
82
+ Enabled: false
83
+
84
+ # explicit >>> implicit
85
+ RSpec/InstanceVariable:
86
+ Enabled: false
87
+
88
+ # maybe when 'all' has a corresponding 'none' matcher
89
+ RSpec/IteratedExpectation:
90
+ Enabled: false
91
+
92
+ # we meant to do that
93
+ RSpec/MessageSpies:
94
+ Enabled: false
95
+
96
+ # too late now
97
+ RSpec/MultipleMemoizedHelpers:
98
+ Enabled: false
99
+
100
+ # setup cost / time >>> failure granularity
101
+ RSpec/MultipleExpectations:
102
+ Enabled: false
103
+
104
+ # cure is worse than the disease
105
+ RSpec/NestedGroups:
106
+ Enabled: false
107
+
108
+ # more quotation marks != more readable
109
+ RSpec/SharedExamples:
110
+ Enabled: false
111
+
112
+ # we meant to do that
113
+ RSpec/StubbedMock:
114
+ Enabled: false
115
+
116
+ # we meant to do that
117
+ RSpec/VerifiedDoubles:
118
+ Enabled: false
119
+
120
+ ############################################################
121
+ # rubocop-rspec
122
+
123
+ # enable newer rubocop-rspec cops
124
+
125
+ RSpec/IdenticalEqualityAssertion: # new in 2.4
126
+ Enabled: true
127
+
128
+ RSpec/Rails/AvoidSetupHook: # new in 2.4
129
+ Enabled: true
@@ -113,12 +113,13 @@ module BerkeleyLibrary::Util
113
113
  ]
114
114
  sources.each do |source|
115
115
  expect(Arrays.find_indices(for_array: source, in_array: target)).to be_nil
116
+ expect(Arrays.find_indices(for_array: source, in_array: target) { |s, t| t == s.to_s }).to be_nil
116
117
  end
117
118
  end
118
119
 
119
120
  it 'takes a comparison block' do
120
121
  sub = %i[a c e]
121
- expect(Arrays.find_indices(for_array: sub, in_array: target) { |source, target| target == source.to_s }).to eq([0, 2, 4])
122
+ expect(Arrays.find_indices(for_array: sub, in_array: target) { |s, t| t == s.to_s }).to eq([0, 2, 4])
122
123
  end
123
124
  end
124
125
 
@@ -145,6 +146,10 @@ module BerkeleyLibrary::Util
145
146
  expect(Arrays.find_index(in_array: arr, start_index: 2) { |x| x < 4 }).to be_nil
146
147
  end
147
148
 
149
+ it 'raises ArgumentError if given extra arguments' do
150
+ expect { Arrays.find_index(1, 2, 3, in_array: [1, 2, 3]) }.to raise_error(ArgumentError)
151
+ end
152
+
148
153
  # rubocop:disable Lint/Void
149
154
  it 'returns an enumerator if given no arguments' do
150
155
  e = Arrays.find_index(in_array: arr)
@@ -327,6 +332,10 @@ module BerkeleyLibrary::Util
327
332
  expect(Arrays.invert([0, 2, 3])).to eq([0, nil, 1, 2])
328
333
  end
329
334
 
335
+ it 'returns nil if given nil' do
336
+ expect(Arrays.invert(nil)).to be_nil
337
+ end
338
+
330
339
  it 'fails if values are not ints' do
331
340
  # noinspection RubyYardParamTypeMatch
332
341
  expect { Arrays.invert(%i[a b c]) }.to raise_error(TypeError)
@@ -8,6 +8,7 @@ module BerkeleyLibrary
8
8
  attr_reader :tmpdir
9
9
 
10
10
  before { @tmpdir = Dir.mktmpdir(basename) }
11
+
11
12
  after { FileUtils.remove_entry(tmpdir) }
12
13
 
13
14
  describe :file_exists? do
@@ -16,6 +16,13 @@ module BerkeleyLibrary
16
16
  end
17
17
  end
18
18
 
19
+ it 'reads from the end if given a negative index' do
20
+ bytes.reverse.each_with_index do |b, i|
21
+ end_offset = i + 1
22
+ expect(StringIOs.getbyte(sio, -end_offset)).to eq(b)
23
+ end
24
+ end
25
+
19
26
  it 'resets the current offset' do
20
27
  StringIOs.getbyte(sio, bytes.size / 2)
21
28
  expect(sio.pos).to eq(0)
@@ -41,6 +41,10 @@ module BerkeleyLibrary
41
41
  expect(Strings.diff_index(s, s)).to be_nil
42
42
  end
43
43
 
44
+ it 'returns nil for non-strings' do
45
+ expect(Strings.diff_index(2, ['2'])).to be_nil
46
+ end
47
+
44
48
  it 'returns the index for different strings' do
45
49
  s1 = 'elvis aaron presley'
46
50
  s2 = 'elvis nikita presley'
@@ -33,6 +33,10 @@ module BerkeleyLibrary
33
33
  # noinspection RubyYardParamTypeMatch
34
34
  expect { Times.ensure_utc(Object.new) }.to raise_error(ArgumentError)
35
35
  end
36
+
37
+ it 'returns nil for nil' do
38
+ expect(Times.ensure_utc(nil)).to be_nil
39
+ end
36
40
  end
37
41
  end
38
42
  end
@@ -97,6 +97,17 @@ module BerkeleyLibrary
97
97
  expect(result).to eq(expected_status)
98
98
  end
99
99
 
100
+ it 'appends query parameters to URL with existing params' do
101
+ url = 'https://example.org/endpoint?foo=bar'
102
+ params = { p1: 1, p2: 2 }
103
+ url_with_query = "#{url}&#{URI.encode_www_form(params)}"
104
+ expected_status = 203
105
+ stub_request(:head, url_with_query).to_return(status: expected_status)
106
+
107
+ result = Requester.head(url, params: params)
108
+ expect(result).to eq(expected_status)
109
+ end
110
+
100
111
  it 'sends request headers' do
101
112
  url = 'https://example.org/'
102
113
  headers = { 'X-help' => 'I am trapped in a unit test' }
@@ -129,6 +140,116 @@ module BerkeleyLibrary
129
140
  result = Requester.head(url1)
130
141
  expect(result).to eq(expected_status)
131
142
  end
143
+
144
+ it 'rejects a nil URI' do
145
+ expect { Requester.head(nil) }.to raise_error(ArgumentError)
146
+ end
147
+ end
148
+
149
+ describe 'logging' do
150
+ attr_reader :logger
151
+
152
+ before do
153
+ @logger = instance_double(BerkeleyLibrary::Logging::Logger)
154
+ allow(BerkeleyLibrary::Logging).to receive(:logger).and_return(logger)
155
+ end
156
+
157
+ context 'GET' do
158
+ it 'logs request URLs and response codes for successful GET requests' do
159
+ url = 'https://example.org/'
160
+ expected_body = 'Help! I am trapped in a unit test'
161
+ stub_request(:get, url).to_return(body: expected_body)
162
+
163
+ expect(logger).to receive(:info).with(/#{url}.*200/)
164
+ Requester.send(:get, url)
165
+ end
166
+
167
+ it 'can suppress logging for successful GET requests' do
168
+ url = 'https://example.org/'
169
+ expected_body = 'Help! I am trapped in a unit test'
170
+ stub_request(:get, url).to_return(body: expected_body)
171
+
172
+ expect(logger).not_to receive(:info)
173
+ Requester.send(:get, url, log: false)
174
+ end
175
+
176
+ it 'logs request URLs and response codes for failed GET requests' do
177
+ url = 'https://example.org/'
178
+ status = 500
179
+ stub_request(:get, url).to_return(status: status)
180
+
181
+ expect(logger).to receive(:info).with(/#{url}.*#{status}/)
182
+ expect { Requester.send(:get, url) }.to raise_error(RestClient::InternalServerError)
183
+ end
184
+
185
+ it 'can suppress logging for failed GET requests' do
186
+ url = 'https://example.org/'
187
+ stub_request(:get, url).to_return(status: 500)
188
+
189
+ expect(logger).not_to receive(:info)
190
+ expect { Requester.send(:get, url, log: false) }.to raise_error(RestClient::InternalServerError)
191
+ end
192
+ end
193
+
194
+ context 'HEAD' do
195
+ it 'logs request URLs and response codes for successful HEAD requests' do
196
+ url = 'https://example.org/'
197
+ expected_body = 'Help! I am trapped in a unit test'
198
+ stub_request(:head, url).to_return(body: expected_body)
199
+
200
+ expect(logger).to receive(:info).with(/#{url}.*200/)
201
+ Requester.send(:head, url)
202
+ end
203
+
204
+ it 'can suppress logging for successful HEAD requests' do
205
+ url = 'https://example.org/'
206
+ expected_body = 'Help! I am trapped in a unit test'
207
+ stub_request(:head, url).to_return(body: expected_body)
208
+
209
+ expect(logger).not_to receive(:info)
210
+ Requester.send(:head, url, log: false)
211
+ end
212
+
213
+ it 'logs request URLs and response codes for failed HEAD requests' do
214
+ url = 'https://example.org/'
215
+ status = 500
216
+ stub_request(:head, url).to_return(status: status)
217
+
218
+ expect(logger).to receive(:info).with(/#{url}.*#{status}/)
219
+ expect(Requester.send(:head, url)).to eq(status)
220
+ end
221
+
222
+ it 'can suppress logging for failed HEAD requests' do
223
+ url = 'https://example.org/'
224
+ status = 500
225
+ stub_request(:head, url).to_return(status: status)
226
+
227
+ expect(logger).not_to receive(:info)
228
+ expect(Requester.send(:head, url, log: false)).to eq(status)
229
+ end
230
+ end
231
+ end
232
+
233
+ describe :new do
234
+ it 'rejects invalid URIs' do
235
+ url = 'not a uri'
236
+ Requester::SUPPORTED_METHODS.each do |method|
237
+ expect { Requester.new(method, url) }.to raise_error(URI::InvalidURIError)
238
+ end
239
+ end
240
+
241
+ it 'rejects nil URIs' do
242
+ Requester::SUPPORTED_METHODS.each do |method|
243
+ expect { Requester.new(method, nil) }.to raise_error(ArgumentError)
244
+ end
245
+ end
246
+
247
+ it 'rejects unsupported methods' do
248
+ url = 'https://example.org/'
249
+ %i[put patch post].each do |method|
250
+ expect { Requester.new(method, url) }.to raise_error(ArgumentError)
251
+ end
252
+ end
132
253
  end
133
254
  end
134
255
  end