berkeley_library-util 0.1.4 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -16,39 +16,66 @@ module BerkeleyLibrary
16
16
  # @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
17
17
  # @param headers [Hash] the request headers.
18
18
  # @return [String] the body as a string.
19
- # @raise [RestClient::Exception] in the event of an error.
19
+ # @raise [RestClient::Exception] in the event of an unsuccessful request.
20
20
  def get(uri, params: {}, headers: {})
21
- resp = make_get_request(uri, params, headers)
21
+ resp = make_request(:get, uri, params, headers)
22
22
  resp.body
23
23
  end
24
24
 
25
- # Performs a GET request and returns the response.
25
+ # Performs a HEAD request and returns the response status as an integer.
26
+ # Note that unlike {Requester#get}, this does not raise an error in the
27
+ # event of an unsuccessful request.
28
+ #
29
+ # @param uri [URI, String] the URI to HEAD
30
+ # @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
31
+ # @param headers [Hash] the request headers.
32
+ # @return [Integer] the response code as an integer.
33
+ def head(uri, params: {}, headers: {})
34
+ head_response(uri, params: params, headers: headers).code
35
+ end
36
+
37
+ # Performs a GET request and returns the response, even in the event of
38
+ # a failed request.
26
39
  #
27
40
  # @param uri [URI, String] the URI to GET
28
41
  # @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
29
42
  # @param headers [Hash] the request headers.
30
43
  # @return [RestClient::Response] the body as a string.
31
44
  def get_response(uri, params: {}, headers: {})
32
- make_get_request(uri, params, headers)
45
+ make_request(:get, uri, params, headers)
46
+ rescue RestClient::Exception => e
47
+ e.response
48
+ end
49
+
50
+ # Performs a HEAD request and returns the response, even in the event of
51
+ # a failed request.
52
+ #
53
+ # @param uri [URI, String] the URI to HEAD
54
+ # @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
55
+ # @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)
33
59
  rescue RestClient::Exception => e
34
60
  e.response
35
61
  end
36
62
 
37
63
  private
38
64
 
39
- def make_get_request(uri, params, headers)
65
+ # @return [RestClient::Response]
66
+ def make_request(method, uri, params, headers)
40
67
  url_str = url_str_with_params(uri, params)
41
- get_or_raise(url_str, headers)
68
+ req_resp_or_raise(method, url_str, headers)
42
69
  end
43
70
 
44
- def url_str_with_params(uri, params)
45
- raise ArgumentError, 'uri cannot be nil' unless (url_str = Validator.url_str_or_nil(uri))
71
+ def url_str_with_params(url, params)
72
+ raise ArgumentError, 'url cannot be nil' unless (uri = Validator.uri_or_nil(url))
46
73
 
47
74
  elements = [].tap do |ee|
48
- ee << url_str
75
+ ee << uri
49
76
  next if params.empty?
50
77
 
51
- ee << '?' unless url_str.include?('?')
78
+ ee << (uri.query ? '&' : '?')
52
79
  ee << URI.encode_www_form(params)
53
80
  end
54
81
 
@@ -57,15 +84,15 @@ module BerkeleyLibrary
57
84
  end
58
85
 
59
86
  # @return [RestClient::Response]
60
- def get_or_raise(url_str, headers)
61
- resp = RestClient.get(url_str, headers)
87
+ def req_resp_or_raise(method, url_str, headers)
88
+ resp = RestClient::Request.execute(method: method, url: url_str, headers: headers)
62
89
  begin
63
90
  return resp if (status = resp.code) == 200
64
91
 
65
92
  raise(exception_for(resp, status))
66
93
  ensure
67
94
  # noinspection RubyMismatchedReturnType
68
- logger.info("GET #{url_str} returned #{status}")
95
+ logger.info("#{method.to_s.upcase} #{url_str} returned #{status}")
69
96
  end
70
97
  end
71
98
 
@@ -8,6 +8,8 @@ module BerkeleyLibrary
8
8
  module URIs
9
9
  include BerkeleyLibrary::Logging
10
10
 
11
+ UTF_8 = Encoding::UTF_8
12
+
11
13
  class << self
12
14
  include URIs
13
15
  end
@@ -24,18 +26,31 @@ module BerkeleyLibrary
24
26
  Appender.new(uri, *elements).to_uri
25
27
  end
26
28
 
27
- # Performs a GET request.
29
+ # Performs a GET request and returns the response body as a string.
28
30
  #
29
31
  # @param uri [URI, String] the URI to GET
30
32
  # @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
31
33
  # @param headers [Hash] the request headers.
32
34
  # @return [String] the body as a string.
33
- # @raise [RestClient::Exception] in the event of an error.
35
+ # @raise [RestClient::Exception] in the event of an unsuccessful request.
34
36
  def get(uri, params: {}, headers: {})
35
37
  Requester.get(uri, params: params, headers: headers)
36
38
  end
37
39
 
38
- # Performs a GET request and returns the response.
40
+ # Performs a HEAD request and returns the response status as an integer.
41
+ # Note that unlike {Requester#get}, this does not raise an error in the
42
+ # event of an unsuccessful request.
43
+ #
44
+ # @param uri [URI, String] the URI to HEAD
45
+ # @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
46
+ # @param headers [Hash] the request headers.
47
+ # @return [Integer] the response code as an integer.
48
+ def head(uri, params: {}, headers: {})
49
+ Requester.head(uri, params: params, headers: headers)
50
+ end
51
+
52
+ # Performs a GET request and returns the response, even in the event of
53
+ # a failed request.
39
54
  #
40
55
  # @param uri [URI, String] the URI to GET
41
56
  # @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
@@ -45,6 +60,17 @@ module BerkeleyLibrary
45
60
  Requester.get_response(uri, params: params, headers: headers)
46
61
  end
47
62
 
63
+ # Performs a HEAD request and returns the response, even in the event of
64
+ # a failed request.
65
+ #
66
+ # @param uri [URI, String] the URI to HEAD
67
+ # @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
68
+ # @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
+ end
73
+
48
74
  # Returns the specified URL as a URI, or `nil` if the URL is `nil`.
49
75
  # @param url [String, URI, nil] the URL.
50
76
  # @return [URI] the URI, or `nil`.
@@ -54,6 +80,19 @@ module BerkeleyLibrary
54
80
  Validator.uri_or_nil(url)
55
81
  end
56
82
 
83
+ # Escapes the specified string so that it can be used as a URL path segment,
84
+ # replacing disallowed characters (including /) with percent-encodings as needed.
85
+ def path_escape(s)
86
+ raise ArgumentError, "Can't escape #{s.inspect}: not a string" unless s.respond_to?(:encoding)
87
+ raise ArgumentError, "Can't escape #{s.inspect}: expected #{UTF_8}, was #{s.encoding}" unless s.encoding == UTF_8
88
+
89
+ ''.tap do |escaped|
90
+ s.bytes.each do |b|
91
+ escaped << (should_escape?(b, :path_segment) ? '%%%02X' % b : b.chr)
92
+ end
93
+ end
94
+ end
95
+
57
96
  # Returns the specified URL as a URI, or `nil` if the URL cannot
58
97
  # be parsed.
59
98
  # @param url [Object, nil] the URL.
@@ -65,6 +104,32 @@ module BerkeleyLibrary
65
104
  logger.warn("Error parsing URL #{url.inspect}", e)
66
105
  nil
67
106
  end
107
+
108
+ private
109
+
110
+ # TODO: extend to cover other modes - host, zone, path, password, query, fragment
111
+ # cf. https://github.com/golang/go/blob/master/src/net/url/url.go
112
+ ALLOWED_BYTES_BY_MODE = {
113
+ path_segment: [0x24, 0x26, 0x2b, 0x3a, 0x3d, 0x40] # @ & = + $
114
+ }.freeze
115
+
116
+ def should_escape?(b, mode)
117
+ return false if unreserved?(b)
118
+ return false if ALLOWED_BYTES_BY_MODE[mode].include?(b)
119
+
120
+ true
121
+ end
122
+
123
+ # rubocop:disable Metrics/CyclomaticComplexity
124
+ def unreserved?(byte)
125
+ return true if byte >= 0x41 && byte <= 0x5a # A-Z
126
+ return true if byte >= 0x61 && byte <= 0x7a # a-z
127
+ return true if byte >= 0x30 && byte <= 0x39 # 0-9
128
+ return true if [0x2d, 0x2e, 0x5f, 0x7e].include?(byte) # - . _ ~
129
+
130
+ false
131
+ end
132
+ # rubocop:enable Metrics/CyclomaticComplexity
68
133
  end
69
134
  end
70
135
  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
@@ -66,6 +66,85 @@ module BerkeleyLibrary
66
66
  expect(result).to eq(expected_body)
67
67
  end
68
68
  end
69
+
70
+ describe :head do
71
+ it 'returns an HTTP status code for a URL string' do
72
+ url = 'https://example.org/'
73
+ expected_status = 203
74
+ stub_request(:head, url).to_return(status: expected_status)
75
+
76
+ result = Requester.head(url)
77
+ expect(result).to eq(expected_status)
78
+ end
79
+
80
+ it 'returns an HTTP response body for a URI' do
81
+ uri = URI.parse('https://example.org/')
82
+ expected_status = 203
83
+ stub_request(:head, uri).to_return(status: expected_status)
84
+
85
+ result = Requester.head(uri)
86
+ expect(result).to eq(expected_status)
87
+ end
88
+
89
+ it 'appends query parameters' do
90
+ url = 'https://example.org/'
91
+ params = { p1: 1, p2: 2 }
92
+ url_with_query = "#{url}?#{URI.encode_www_form(params)}"
93
+ expected_status = 203
94
+ stub_request(:head, url_with_query).to_return(status: expected_status)
95
+
96
+ result = Requester.head(url, params: params)
97
+ expect(result).to eq(expected_status)
98
+ end
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
+
111
+ it 'sends request headers' do
112
+ url = 'https://example.org/'
113
+ headers = { 'X-help' => 'I am trapped in a unit test' }
114
+ expected_status = 203
115
+ stub_request(:head, url).with(headers: headers).to_return(status: expected_status)
116
+
117
+ result = Requester.head(url, headers: headers)
118
+ expect(result).to eq(expected_status)
119
+ end
120
+
121
+ it 'returns the status even for unsuccessful requests' do
122
+ aggregate_failures 'responses' do
123
+ [207, 400, 401, 403, 404, 405, 418, 451, 500, 503].each do |expected_status|
124
+ url = "http://example.edu/#{expected_status}"
125
+ stub_request(:head, url).to_return(status: expected_status)
126
+
127
+ result = Requester.head(url)
128
+ expect(result).to eq(expected_status)
129
+ end
130
+ end
131
+ end
132
+
133
+ it 'handles redirects' do
134
+ url1 = 'https://example.org/'
135
+ url2 = 'https://example.edu/'
136
+ stub_request(:head, url1).to_return(status: 302, headers: { 'Location' => url2 })
137
+ expected_status = 203
138
+ stub_request(:head, url2).to_return(status: expected_status)
139
+
140
+ result = Requester.head(url1)
141
+ expect(result).to eq(expected_status)
142
+ end
143
+
144
+ it 'rejects a nil URI' do
145
+ expect { Requester.head(nil) }.to raise_error(ArgumentError)
146
+ end
147
+ end
69
148
  end
70
149
  end
71
150
  end