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