berkeley_library-util 0.1.4 → 0.1.6

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