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.
- checksums.yaml +4 -4
- data/.github/workflows/build.yml +17 -5
- data/.idea/inspectionProfiles/Project_Default.xml +1 -0
- data/.idea/util.iml +48 -51
- data/.rubocop.yml +161 -2
- data/.simplecov +5 -6
- data/CHANGES.md +17 -0
- data/Rakefile +2 -2
- data/berkeley_library-util.gemspec +5 -6
- data/lib/berkeley_library/util/arrays.rb +3 -3
- data/lib/berkeley_library/util/files.rb +2 -2
- data/lib/berkeley_library/util/module_info.rb +1 -1
- data/lib/berkeley_library/util/uris/appender.rb +15 -19
- data/lib/berkeley_library/util/uris/requester.rb +40 -13
- data/lib/berkeley_library/util/uris.rb +68 -3
- data/rakelib/.rubocop.yml +4 -0
- data/spec/.rubocop.yml +89 -0
- data/spec/berkeley_library/util/arrays_spec.rb +10 -1
- data/spec/berkeley_library/util/files_spec.rb +1 -0
- data/spec/berkeley_library/util/stringios_spec.rb +7 -0
- data/spec/berkeley_library/util/strings_spec.rb +4 -0
- data/spec/berkeley_library/util/times_spec.rb +4 -0
- data/spec/berkeley_library/util/uris/requester_spec.rb +79 -0
- data/spec/berkeley_library/util/uris_spec.rb +157 -14
- data/spec/spec_helper.rb +2 -2
- metadata +16 -54
- data/rakelib/bundle.rake +0 -8
@@ -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
|
19
|
+
# @raise [RestClient::Exception] in the event of an unsuccessful request.
|
20
20
|
def get(uri, params: {}, headers: {})
|
21
|
-
resp =
|
21
|
+
resp = make_request(:get, uri, params, headers)
|
22
22
|
resp.body
|
23
23
|
end
|
24
24
|
|
25
|
-
# Performs a
|
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
|
-
|
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
|
-
|
65
|
+
# @return [RestClient::Response]
|
66
|
+
def make_request(method, uri, params, headers)
|
40
67
|
url_str = url_str_with_params(uri, params)
|
41
|
-
|
68
|
+
req_resp_or_raise(method, url_str, headers)
|
42
69
|
end
|
43
70
|
|
44
|
-
def url_str_with_params(
|
45
|
-
raise ArgumentError, '
|
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 <<
|
75
|
+
ee << uri
|
49
76
|
next if params.empty?
|
50
77
|
|
51
|
-
ee <<
|
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
|
61
|
-
resp = RestClient.
|
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("
|
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
|
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
|
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
|
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) { |
|
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)
|
@@ -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'
|
@@ -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
|