berkeley_library-util 0.1.5 → 0.1.7

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.
@@ -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