format_parser 2.0.0.pre → 2.0.0.pre.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d7c965b7783ecaea4802f7e585861b4400b2210fee4cb90388757530880fa074
4
- data.tar.gz: fc8b7cc3f00825fa054c948a7ae817b1eee6457ffaec9e5a6b5bdd9a0b92d126
3
+ metadata.gz: 01730fd580c14a2c35b87be1f62384b06f9b2683bd07ba01ed21d21a4211b1f5
4
+ data.tar.gz: c27ba63f09c8251ccaf237040cb6f01c495686f6a025cdb7207aa0bfb2fe0098
5
5
  SHA512:
6
- metadata.gz: 73f774ebe540dfd54e87f89cedecfc0fabf4a97f4e2ef72afcd94edc5e0fbc344c7c67b365942e3bb915dfe76f94f038072671c259c2d366a69d64a73cbde960
7
- data.tar.gz: bc1405329d521487ec4d0738c258fb12c3acdb37b6b8ecebf7451a866d5f1072cfc23774e2ecc3d7d297095ff280320756fb4cd9000de3eac447a105cf87028b
6
+ metadata.gz: 427c1f3652e33592a7bd3a63c0db676a865390b1947732e452905d0fe64f54df82af2b9a339dfdf962d9586c73fe7ac8a2b19da099c502a187efd8215993ddbf
7
+ data.tar.gz: 3652ca2c1ae42ddb5e29811dcae13e8bbb0fcf964c4f47547638e992d21612235bdf489d908b5c4bdf3aa2859768eb0dc98c6fc58527ce5031396ee341207354
@@ -8,7 +8,7 @@ env:
8
8
  jobs:
9
9
  lint:
10
10
  name: Code Style
11
- runs-on: ubuntu-18.04
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-18.04
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
@@ -1,4 +1,4 @@
1
- ## 2.0.0.pre (Prerelease)
1
+ ## 2.0.0.pre.2 (Prerelease)
2
2
  * Drop support for Ruby `<2.7`.
3
3
  * Drop faraday dependencies.
4
4
  * Loosen version constraints on other dependencies.
@@ -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
@@ -1,3 +1,3 @@
1
1
  module FormatParser
2
- VERSION = '2.0.0.pre'
2
+ VERSION = '2.0.0.pre.2'
3
3
  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(uri) if redirects == 0
144
+ raise RedirectLimitReached, uri if redirects == 0
145
145
  location = response['location']
146
146
  if location
147
- request_range(range, redirect_uri(location, uri), redirects - 1)
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
@@ -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
- it 'returns the partial content when the server supplies a 206 status' do
7
- url = 'https://images.invalid/img.jpg'
8
- response = Net::HTTPPartialContent.new('2', '206', 'Partial Content')
9
- response['Content-Range'] = '10-109/2577'
10
- allow(response).to receive(:body).and_return('Response body')
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
- allow(Net::HTTP).to receive(:start).and_yield(Net::HTTP).and_return(response)
13
- allow(Net::HTTP).to receive(:request_get).and_return(response)
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
- expect(Net::HTTP).to receive(:request_get).with(
16
- an_object_satisfying { |uri| URI::HTTPS === uri && uri.to_s == url },
17
- a_hash_including('range' => 'bytes=10-109')
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
- rio = described_class.new(url)
21
- rio.seek(10)
22
- read_result = rio.read(100)
59
+ rio = described_class.new(url)
60
+ rio.seek(10)
61
+ read_result = rio.read(100)
23
62
 
24
- expect(read_result).to eq(response.body)
25
- end
63
+ expect(read_result).to eq(body)
64
+ expect(stub).to have_been_requested
65
+ end
26
66
 
27
- it 'returns the entire content when the server supplies the Content-Range response but sends a 200 status' do
28
- url = 'https://images.invalid/img.jpg'
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
- allow(Net::HTTP).to receive(:start).and_yield(Net::HTTP).and_return(response)
33
- allow(Net::HTTP).to receive(:request_get).and_return(response)
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
- expect(Net::HTTP).to receive(:request_get).with(
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
- rio = described_class.new(url)
41
- rio.seek(10)
42
- read_result = rio.read(100)
76
+ expect(rio.pos).to eq(0)
43
77
 
44
- expect(read_result).to eq(response.body)
45
- end
78
+ rio.read(1)
46
79
 
47
- it 'raises a specific error for all 4xx responses except 416' do
48
- url = 'https://images.invalid/img.jpg'
49
- response = Net::HTTPForbidden.new('2', '403', 'Forbidden')
80
+ expect(rio.pos).to eq(1)
81
+ expect(stub).to have_been_requested
82
+ end
83
+ end
50
84
 
51
- allow(Net::HTTP).to receive(:start).and_yield(Net::HTTP).and_return(response)
52
- allow(Net::HTTP).to receive(:request_get).and_return(response)
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
- expect(Net::HTTP).to receive(:request_get).with(
55
- an_object_satisfying { |uri| uri.to_s == url },
56
- a_hash_including('range' => 'bytes=100-199')
57
- )
89
+ stub = stub_request(:get, url)
90
+ .with(headers: { 'range' => 'bytes=10-109' })
91
+ .to_return(status: 206)
58
92
 
59
- rio = described_class.new(url)
60
- rio.seek(100)
93
+ rio = described_class.new(url)
94
+ rio.seek(10)
61
95
 
62
- expect { rio.read(100) }.to raise_error(/replied with a 403 and refused/)
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
- it 'returns nil on a 416 response' do
66
- url = 'https://images.invalid/img.jpg'
67
- response = Net::HTTPRangeNotSatisfiable.new('2', '416', 'Range Not Satisfiable')
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
- allow(Net::HTTP).to receive(:start).and_yield(Net::HTTP).and_return(response)
70
- allow(Net::HTTP).to receive(:request_get).and_return(response)
192
+ # 4XX
71
193
 
72
- expect(Net::HTTP).to receive(:request_get).with(
73
- an_object_satisfying { |uri| uri.to_s == url },
74
- a_hash_including('range' => 'bytes=100-199')
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
- rio = described_class.new(url)
78
- rio.seek(100)
198
+ stub = stub_request(:get, url)
199
+ .with(headers: { 'range' => 'bytes=100-199' })
200
+ .to_return(status: 416)
79
201
 
80
- expect(rio.read(100)).to be_nil
81
- end
202
+ rio = described_class.new(url)
203
+ rio.seek(100)
82
204
 
83
- it 'sets the status_code of the exception on a 4xx response from upstream' do
84
- url = 'https://images.invalid/img.jpg'
85
- response = Net::HTTPForbidden.new('2', '403', 'Forbidden')
205
+ expect(rio.read(100)).to be_nil
206
+ expect(stub).to have_been_requested
207
+ end
86
208
 
87
- allow(Net::HTTP).to receive(:start).and_yield(Net::HTTP).and_return(response)
88
- allow(Net::HTTP).to receive(:request_get).and_return(response)
209
+ it 'does not change pos or size' do
210
+ url = 'https://images.invalid/img.jpg'
89
211
 
90
- expect(Net::HTTP).to receive(:request_get).with(
91
- an_object_satisfying { |uri| uri.to_s == url },
92
- a_hash_including('range' => 'bytes=100-199')
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
- rio = described_class.new(url)
96
- rio.seek(100)
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
- it 'returns a nil when the range cannot be satisfied and the response is 416' do
101
- url = 'https://images.invalid/img.jpg'
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
- allow(Net::HTTP).to receive(:start).and_yield(Net::HTTP).and_return(response)
105
- allow(Net::HTTP).to receive(:request_get).and_return(response)
222
+ stub = stub_request(:get, url)
223
+ .with(headers: { 'range' => 'bytes=100-199' })
224
+ .to_return(status: 416)
106
225
 
107
- expect(Net::HTTP).to receive(:request_get).with(
108
- an_object_satisfying { |uri| uri.to_s == url },
109
- a_hash_including('range' => 'bytes=100-199')
110
- )
226
+ rio.seek(100)
227
+ rio.read(100)
111
228
 
112
- rio = described_class.new(url)
113
- rio.seek(100)
114
-
115
- expect(rio.read(100)).to be_nil
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
- it 'raises a specific error for all 5xx responses' do
153
- url = 'https://images.invalid/img.jpg'
154
- response = Net::HTTPBadGateway.new('2', '502', 'Bad Gateway')
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
- allow(Net::HTTP).to receive(:start).and_yield(Net::HTTP).and_return(response)
157
- allow(Net::HTTP).to receive(:request_get).and_return(response)
240
+ stub = stub_request(:get, url)
241
+ .with(headers: { 'range' => 'bytes=100-199' })
242
+ .to_return(status: code)
158
243
 
159
- expect(Net::HTTP).to receive(:request_get).with(
160
- an_object_satisfying { |uri| uri.to_s == url },
161
- a_hash_including('range' => 'bytes=100-199')
162
- )
244
+ rio = described_class.new(url)
245
+ rio.seek(100)
163
246
 
164
- rio = described_class.new(url)
165
- rio.seek(100)
166
-
167
- expect { rio.read(100) }.to raise_error(/replied with a 502 and we might want to retry/)
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
- it 'maintains and exposes #pos' do
171
- url = 'https://images.invalid/img.jpg'
172
- response = Net::HTTPPartialContent.new('2', '206', 'Partial Content')
173
- response['Content-Range'] = 'bytes 0-0/13'
174
- allow(response).to receive(:body).and_return('a')
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
- allow(Net::HTTP).to receive(:start).and_yield(Net::HTTP).and_return(response)
177
- allow(Net::HTTP).to receive(:request_get).and_return(response)
260
+ stub = stub_request(:get, url)
261
+ .with(headers: { 'range' => 'bytes=100-199' })
262
+ .to_return(status: code)
178
263
 
179
- expect(Net::HTTP).to receive(:request_get).with(
180
- an_object_satisfying { |uri| uri.to_s == url },
181
- a_hash_including('range' => 'bytes=0-0')
182
- )
264
+ rio = described_class.new(url)
265
+ rio.seek(100)
183
266
 
184
- rio = described_class.new(url)
185
- expect(rio.pos).to eq(0)
186
- rio.read(1)
187
- expect(rio.pos).to eq(1)
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-13 00:00:00.000000000 Z
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