format_parser 2.0.0.pre → 2.0.0.pre.2
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/main.yml +2 -2
- data/CHANGELOG.md +1 -1
- data/format_parser.gemspec +1 -0
- data/lib/format_parser/version.rb +1 -1
- data/lib/remote_io.rb +6 -3
- data/spec/remote_io_spec.rb +227 -144
- data/spec/spec_helper.rb +4 -0
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 01730fd580c14a2c35b87be1f62384b06f9b2683bd07ba01ed21d21a4211b1f5
|
4
|
+
data.tar.gz: c27ba63f09c8251ccaf237040cb6f01c495686f6a025cdb7207aa0bfb2fe0098
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 427c1f3652e33592a7bd3a63c0db676a865390b1947732e452905d0fe64f54df82af2b9a339dfdf962d9586c73fe7ac8a2b19da099c502a187efd8215993ddbf
|
7
|
+
data.tar.gz: 3652ca2c1ae42ddb5e29811dcae13e8bbb0fcf964c4f47547638e992d21612235bdf489d908b5c4bdf3aa2859768eb0dc98c6fc58527ce5031396ee341207354
|
data/.github/workflows/main.yml
CHANGED
@@ -8,7 +8,7 @@ env:
|
|
8
8
|
jobs:
|
9
9
|
lint:
|
10
10
|
name: Code Style
|
11
|
-
runs-on: ubuntu-
|
11
|
+
runs-on: ubuntu-latest
|
12
12
|
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
|
13
13
|
strategy:
|
14
14
|
matrix:
|
@@ -54,7 +54,7 @@ jobs:
|
|
54
54
|
run: bundle exec rubocop
|
55
55
|
test:
|
56
56
|
name: Specs
|
57
|
-
runs-on: ubuntu-
|
57
|
+
runs-on: ubuntu-latest
|
58
58
|
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
|
59
59
|
strategy:
|
60
60
|
matrix:
|
data/CHANGELOG.md
CHANGED
data/format_parser.gemspec
CHANGED
@@ -39,6 +39,7 @@ Gem::Specification.new do |spec|
|
|
39
39
|
spec.add_development_dependency 'rake'
|
40
40
|
spec.add_development_dependency 'rspec'
|
41
41
|
spec.add_development_dependency 'simplecov'
|
42
|
+
spec.add_development_dependency 'webmock'
|
42
43
|
spec.add_development_dependency 'wetransfer_style', '1.0.0'
|
43
44
|
spec.add_development_dependency 'yard'
|
44
45
|
end
|
data/lib/remote_io.rb
CHANGED
@@ -122,7 +122,7 @@ class FormatParser::RemoteIO
|
|
122
122
|
error_message = [
|
123
123
|
"We requested #{requested_range_size} bytes, but the server sent us more",
|
124
124
|
"(#{response_size} bytes) - it likely has no `Range:` support.",
|
125
|
-
"The error occurred when talking to #{uri}
|
125
|
+
"The error occurred when talking to #{uri}"
|
126
126
|
]
|
127
127
|
raise InvalidRequest.new(response.code, error_message.join("\n"))
|
128
128
|
end
|
@@ -141,10 +141,13 @@ class FormatParser::RemoteIO
|
|
141
141
|
# to be 206
|
142
142
|
[size, response.body]
|
143
143
|
when Net::HTTPMovedPermanently, Net::HTTPFound, Net::HTTPSeeOther, Net::HTTPTemporaryRedirect, Net::HTTPPermanentRedirect
|
144
|
-
raise RedirectLimitReached
|
144
|
+
raise RedirectLimitReached, uri if redirects == 0
|
145
145
|
location = response['location']
|
146
146
|
if location
|
147
|
-
|
147
|
+
new_uri = redirect_uri(location, uri)
|
148
|
+
# Clear the Authorization header if the new URI has a different host.
|
149
|
+
@headers.delete('Authorization') unless [@uri.scheme, @uri.host, @uri.port] == [new_uri.scheme, new_uri.host, new_uri.port]
|
150
|
+
request_range(range, new_uri, redirects - 1)
|
148
151
|
else
|
149
152
|
raise InvalidRequest.new(response.code, "Server at #{uri} replied with a #{response.code}, indicating redirection; however, the location header was empty.")
|
150
153
|
end
|
data/spec/remote_io_spec.rb
CHANGED
@@ -3,187 +3,270 @@ require 'spec_helper'
|
|
3
3
|
describe FormatParser::RemoteIO do
|
4
4
|
it_behaves_like 'an IO object compatible with IOConstraint'
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
6
|
+
# 2XX
|
7
|
+
|
8
|
+
context 'when the response code is 200 (OK)' do
|
9
|
+
context 'when the response size does not exceed the requested range' do
|
10
|
+
it 'returns the entire response body' do
|
11
|
+
url = 'https://images.invalid/img.jpg'
|
12
|
+
body = 'response body'
|
13
|
+
|
14
|
+
stub = stub_request(:get, url)
|
15
|
+
.with(headers: { 'range' => 'bytes=10-109' })
|
16
|
+
.to_return(body: body, status: 200)
|
17
|
+
|
18
|
+
rio = described_class.new(url)
|
19
|
+
rio.seek(10)
|
20
|
+
read_result = rio.read(100)
|
21
|
+
|
22
|
+
expect(read_result).to eq(body)
|
23
|
+
expect(stub).to have_been_requested
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'when the response size exceeds the requested range' do
|
28
|
+
it 'raises an error' do
|
29
|
+
url = 'https://images.invalid/img.jpg'
|
30
|
+
body = 'This response is way longer than 10 bytes.'
|
31
|
+
|
32
|
+
stub = stub_request(:get, url)
|
33
|
+
.with(headers: { 'range' => 'bytes=10-19' })
|
34
|
+
.to_return(body: body, status: 200)
|
35
|
+
|
36
|
+
rio = described_class.new(url)
|
37
|
+
rio.seek(10)
|
38
|
+
|
39
|
+
expect { rio.read(10) }.to raise_error(
|
40
|
+
"We requested 10 bytes, but the server sent us more\n"\
|
41
|
+
"(42 bytes) - it likely has no `Range:` support.\n"\
|
42
|
+
"The error occurred when talking to #{url}"
|
43
|
+
)
|
44
|
+
expect(stub).to have_been_requested
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
11
48
|
|
12
|
-
|
13
|
-
|
49
|
+
context 'when the response status code is 206 (Partial Content)' do
|
50
|
+
context 'when the Content-Range header is present' do
|
51
|
+
it 'returns the response body' do
|
52
|
+
url = 'https://images.invalid/img.jpg'
|
53
|
+
body = 'response body'
|
14
54
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
)
|
55
|
+
stub = stub_request(:get, url)
|
56
|
+
.with(headers: { 'range' => 'bytes=10-109' })
|
57
|
+
.to_return(body: body, headers: { 'Content-Range' => '10-109/2577' }, status: 206)
|
19
58
|
|
20
|
-
|
21
|
-
|
22
|
-
|
59
|
+
rio = described_class.new(url)
|
60
|
+
rio.seek(10)
|
61
|
+
read_result = rio.read(100)
|
23
62
|
|
24
|
-
|
25
|
-
|
63
|
+
expect(read_result).to eq(body)
|
64
|
+
expect(stub).to have_been_requested
|
65
|
+
end
|
26
66
|
|
27
|
-
|
28
|
-
|
29
|
-
response = Net::HTTPOK.new('2', '200', 'OK')
|
30
|
-
allow(response).to receive(:body).and_return('Response body')
|
67
|
+
it 'maintains and exposes pos' do
|
68
|
+
url = 'https://images.invalid/img.jpg'
|
31
69
|
|
32
|
-
|
33
|
-
|
70
|
+
stub = stub_request(:get, url)
|
71
|
+
.with(headers: { 'range' => 'bytes=0-0' })
|
72
|
+
.to_return(body: 'a', headers: { 'Content-Range' => '0-0/13' }, status: 206)
|
34
73
|
|
35
|
-
|
36
|
-
an_object_satisfying { |uri| URI::HTTPS === uri && uri.to_s == url },
|
37
|
-
a_hash_including('range' => 'bytes=10-109')
|
38
|
-
)
|
74
|
+
rio = described_class.new(url)
|
39
75
|
|
40
|
-
|
41
|
-
rio.seek(10)
|
42
|
-
read_result = rio.read(100)
|
76
|
+
expect(rio.pos).to eq(0)
|
43
77
|
|
44
|
-
|
45
|
-
end
|
78
|
+
rio.read(1)
|
46
79
|
|
47
|
-
|
48
|
-
|
49
|
-
|
80
|
+
expect(rio.pos).to eq(1)
|
81
|
+
expect(stub).to have_been_requested
|
82
|
+
end
|
83
|
+
end
|
50
84
|
|
51
|
-
|
52
|
-
|
85
|
+
context 'when the Content-Range header is not present' do
|
86
|
+
it 'raises an error' do
|
87
|
+
url = 'https://images.invalid/img.jpg'
|
53
88
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
)
|
89
|
+
stub = stub_request(:get, url)
|
90
|
+
.with(headers: { 'range' => 'bytes=10-109' })
|
91
|
+
.to_return(status: 206)
|
58
92
|
|
59
|
-
|
60
|
-
|
93
|
+
rio = described_class.new(url)
|
94
|
+
rio.seek(10)
|
61
95
|
|
62
|
-
|
96
|
+
expect { rio.read(100) }.to raise_error("The server replied with 206 status but no Content-Range at #{url}")
|
97
|
+
expect(stub).to have_been_requested
|
98
|
+
end
|
99
|
+
end
|
63
100
|
end
|
64
101
|
|
65
|
-
|
66
|
-
|
67
|
-
|
102
|
+
# 3XX
|
103
|
+
|
104
|
+
[301, 302, 303, 307, 308].each do |code|
|
105
|
+
context "when the response code is #{code}" do
|
106
|
+
context 'when the location header is present and the redirect limit is not exceeded' do
|
107
|
+
context 'when the location is absolute' do
|
108
|
+
it 'redirects to the specified location, without the Authorization header' do
|
109
|
+
redirecting_url = 'https://my_images.invalid/my_image'
|
110
|
+
destination_url = 'https://images.invalid/img.jpg'
|
111
|
+
body = 'response body'
|
112
|
+
|
113
|
+
redirect_stub = stub_request(:get, redirecting_url)
|
114
|
+
.with(headers: { 'Authorization' => 'token', 'range' => 'bytes=10-109' })
|
115
|
+
.to_return(headers: { 'location' => destination_url }, status: code)
|
116
|
+
destination_stub = stub_request(:get, destination_url)
|
117
|
+
.with { |request| request.headers['Range'] == 'bytes=10-109' && !request.headers.key?('Authorization') }
|
118
|
+
.to_return(body: body, status: 200)
|
119
|
+
|
120
|
+
rio = described_class.new(redirecting_url, headers: { 'Authorization' => 'token' })
|
121
|
+
rio.seek(10)
|
122
|
+
read_result = rio.read(100)
|
123
|
+
|
124
|
+
expect(read_result).to eq(body)
|
125
|
+
expect(redirect_stub).to have_been_requested
|
126
|
+
expect(destination_stub).to have_been_requested
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
context 'when the location is relative' do
|
131
|
+
it 'redirects to the specified location under the same host, with the same Authorization header' do
|
132
|
+
host = 'https://images.invalid'
|
133
|
+
redirecting_path = '/my_image'
|
134
|
+
redirecting_url = host + redirecting_path
|
135
|
+
destination_path = '/img.jpg'
|
136
|
+
destination_url = host + destination_path
|
137
|
+
body = 'response body'
|
138
|
+
|
139
|
+
redirect_stub = stub_request(:get, redirecting_url)
|
140
|
+
.with(headers: { 'Authorization' => 'token', 'range' => 'bytes=10-109' })
|
141
|
+
.to_return(headers: { 'location' => destination_path }, status: code)
|
142
|
+
destination_stub = stub_request(:get, destination_url)
|
143
|
+
.with(headers: { 'Authorization' => 'token', 'range' => 'bytes=10-109' })
|
144
|
+
.to_return(body: body, status: 200)
|
145
|
+
|
146
|
+
rio = described_class.new(redirecting_url, headers: { 'Authorization' => 'token' })
|
147
|
+
rio.seek(10)
|
148
|
+
read_result = rio.read(100)
|
149
|
+
|
150
|
+
expect(read_result).to eq(body)
|
151
|
+
expect(redirect_stub).to have_been_requested
|
152
|
+
expect(destination_stub).to have_been_requested
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
context 'when the location header is not present' do
|
158
|
+
it 'raises an error' do
|
159
|
+
url = 'https://images.invalid/my_image'
|
160
|
+
|
161
|
+
stub = stub_request(:get, url)
|
162
|
+
.with(headers: { 'range' => 'bytes=10-109' })
|
163
|
+
.to_return(status: code)
|
164
|
+
|
165
|
+
rio = described_class.new(url)
|
166
|
+
rio.seek(10)
|
167
|
+
|
168
|
+
expect { rio.read(100) }.to raise_error("Server at #{url} replied with a #{code}, indicating redirection; however, the location header was empty.")
|
169
|
+
expect(stub).to have_been_requested
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
context 'when the redirect limit is exceeded' do
|
174
|
+
it 'raises an error' do
|
175
|
+
redirecting_url = 'https://images.invalid/my_image'
|
176
|
+
destination_url = 'https://images.invalid/img.jpg'
|
177
|
+
|
178
|
+
stub = stub_request(:get, /https:\/\/images\.invalid.*/)
|
179
|
+
.with(headers: { 'range' => 'bytes=10-109' })
|
180
|
+
.to_return(headers: { 'location' => destination_url }, status: code)
|
181
|
+
|
182
|
+
rio = described_class.new(redirecting_url)
|
183
|
+
rio.seek(10)
|
184
|
+
|
185
|
+
expect { rio.read(100) }.to raise_error("Too many redirects; last one to: #{destination_url}")
|
186
|
+
expect(stub).to have_been_requested.times(4)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
68
191
|
|
69
|
-
|
70
|
-
allow(Net::HTTP).to receive(:request_get).and_return(response)
|
192
|
+
# 4XX
|
71
193
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
)
|
194
|
+
context 'when the response status code is 416 (Range Not Satisfiable)' do
|
195
|
+
it 'returns nil' do
|
196
|
+
url = 'https://images.invalid/img.jpg'
|
76
197
|
|
77
|
-
|
78
|
-
|
198
|
+
stub = stub_request(:get, url)
|
199
|
+
.with(headers: { 'range' => 'bytes=100-199' })
|
200
|
+
.to_return(status: 416)
|
79
201
|
|
80
|
-
|
81
|
-
|
202
|
+
rio = described_class.new(url)
|
203
|
+
rio.seek(100)
|
82
204
|
|
83
|
-
|
84
|
-
|
85
|
-
|
205
|
+
expect(rio.read(100)).to be_nil
|
206
|
+
expect(stub).to have_been_requested
|
207
|
+
end
|
86
208
|
|
87
|
-
|
88
|
-
|
209
|
+
it 'does not change pos or size' do
|
210
|
+
url = 'https://images.invalid/img.jpg'
|
89
211
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
)
|
212
|
+
stub = stub_request(:get, url)
|
213
|
+
.with(headers: { 'range' => 'bytes=0-0' })
|
214
|
+
.to_return(body: 'response body', headers: { 'Content-Range' => 'bytes 0-0/13' }, status: 206)
|
94
215
|
|
95
|
-
|
96
|
-
|
97
|
-
expect { rio.read(100) }.to(raise_error { |e| expect(e.status_code).to eq(403) })
|
98
|
-
end
|
216
|
+
rio = described_class.new(url)
|
217
|
+
rio.read(1)
|
99
218
|
|
100
|
-
|
101
|
-
|
102
|
-
response = Net::HTTPRangeNotSatisfiable.new('2', '416', 'Range Not Satisfiable')
|
219
|
+
expect(rio.size).to eq(13)
|
220
|
+
expect(stub).to have_been_requested
|
103
221
|
|
104
|
-
|
105
|
-
|
222
|
+
stub = stub_request(:get, url)
|
223
|
+
.with(headers: { 'range' => 'bytes=100-199' })
|
224
|
+
.to_return(status: 416)
|
106
225
|
|
107
|
-
|
108
|
-
|
109
|
-
a_hash_including('range' => 'bytes=100-199')
|
110
|
-
)
|
226
|
+
rio.seek(100)
|
227
|
+
rio.read(100)
|
111
228
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
end
|
117
|
-
|
118
|
-
it 'does not overwrite size when the range cannot be satisfied and the response is 416' do
|
119
|
-
url = 'https://images.invalid/img.jpg'
|
120
|
-
response_1 = Net::HTTPPartialContent.new('2', '206', 'Partial Content')
|
121
|
-
response_1['Content-Range'] = 'bytes 0-0/13'
|
122
|
-
allow(response_1).to receive(:body).and_return('Response body')
|
123
|
-
response_2 = Net::HTTPRangeNotSatisfiable.new('2', '416', 'Range Not Satisfiable')
|
124
|
-
|
125
|
-
allow(Net::HTTP).to receive(:start).and_yield(Net::HTTP).and_return(response_1, response_2)
|
126
|
-
allow(Net::HTTP).to receive(:request_get).and_return(response_1, response_2)
|
127
|
-
|
128
|
-
expect(Net::HTTP).to receive(:request_get)
|
129
|
-
.with(
|
130
|
-
an_object_satisfying { |uri| uri.to_s == url },
|
131
|
-
a_hash_including('range' => 'bytes=0-0')
|
132
|
-
)
|
133
|
-
.ordered
|
134
|
-
expect(Net::HTTP).to receive(:request_get)
|
135
|
-
.with(
|
136
|
-
an_object_satisfying { |uri| uri.to_s == url },
|
137
|
-
a_hash_including('range' => 'bytes=100-199')
|
138
|
-
)
|
139
|
-
.ordered
|
140
|
-
|
141
|
-
rio = described_class.new(url)
|
142
|
-
rio.read(1)
|
143
|
-
|
144
|
-
expect(rio.size).to eq(13)
|
145
|
-
|
146
|
-
rio.seek(100)
|
147
|
-
|
148
|
-
expect(rio.read(100)).to be_nil
|
149
|
-
expect(rio.size).to eq(13)
|
229
|
+
expect(rio.pos).to eq(100)
|
230
|
+
expect(rio.size).to eq(13)
|
231
|
+
expect(stub).to have_been_requested
|
232
|
+
end
|
150
233
|
end
|
151
234
|
|
152
|
-
|
153
|
-
|
154
|
-
|
235
|
+
[*400..415, *417..499].each do |code|
|
236
|
+
context "when the response status code is #{code}" do
|
237
|
+
it 'raises an error' do
|
238
|
+
url = 'https://images.invalid/img.jpg'
|
155
239
|
|
156
|
-
|
157
|
-
|
240
|
+
stub = stub_request(:get, url)
|
241
|
+
.with(headers: { 'range' => 'bytes=100-199' })
|
242
|
+
.to_return(status: code)
|
158
243
|
|
159
|
-
|
160
|
-
|
161
|
-
a_hash_including('range' => 'bytes=100-199')
|
162
|
-
)
|
244
|
+
rio = described_class.new(url)
|
245
|
+
rio.seek(100)
|
163
246
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
247
|
+
expect { rio.read(100) }.to raise_error("Server at #{url} replied with a #{code} and refused our request")
|
248
|
+
expect(stub).to have_been_requested
|
249
|
+
end
|
250
|
+
end
|
168
251
|
end
|
169
252
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
response
|
174
|
-
|
253
|
+
# 5XX
|
254
|
+
|
255
|
+
(500..599).each do |code|
|
256
|
+
context "when the response status code is #{code}" do
|
257
|
+
it 'raises an error' do
|
258
|
+
url = 'https://images.invalid/img.jpg'
|
175
259
|
|
176
|
-
|
177
|
-
|
260
|
+
stub = stub_request(:get, url)
|
261
|
+
.with(headers: { 'range' => 'bytes=100-199' })
|
262
|
+
.to_return(status: code)
|
178
263
|
|
179
|
-
|
180
|
-
|
181
|
-
a_hash_including('range' => 'bytes=0-0')
|
182
|
-
)
|
264
|
+
rio = described_class.new(url)
|
265
|
+
rio.seek(100)
|
183
266
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
267
|
+
expect { rio.read(100) }.to raise_error("Server at #{url} replied with a #{code} and we might want to retry")
|
268
|
+
expect(stub).to have_been_requested
|
269
|
+
end
|
270
|
+
end
|
188
271
|
end
|
189
272
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -7,6 +7,10 @@ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
|
7
7
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
8
8
|
|
9
9
|
require 'rspec'
|
10
|
+
require 'webmock/rspec'
|
11
|
+
|
12
|
+
WebMock.disable_net_connect!(allow_localhost: true)
|
13
|
+
|
10
14
|
require 'format_parser'
|
11
15
|
|
12
16
|
module SpecHelpers
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: format_parser
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.0.pre
|
4
|
+
version: 2.0.0.pre.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Noah Berman
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2022-10-
|
12
|
+
date: 2022-10-31 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: exifr
|
@@ -123,6 +123,20 @@ dependencies:
|
|
123
123
|
- - ">="
|
124
124
|
- !ruby/object:Gem::Version
|
125
125
|
version: '0'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: webmock
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
type: :development
|
134
|
+
prerelease: false
|
135
|
+
version_requirements: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - ">="
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '0'
|
126
140
|
- !ruby/object:Gem::Dependency
|
127
141
|
name: wetransfer_style
|
128
142
|
requirement: !ruby/object:Gem::Requirement
|