Skip to content

Commit c7da23b

Browse files
committed
un-deprecate stream-like objects
- this splits the purpose of "stream" and "file" inside_route methods. - file will now always setup headers for streaming. - stream is purely for stream-like objects. This allows you to have streaming responses of generated data
1 parent 2bf2c1a commit c7da23b

File tree

11 files changed

+170
-51
lines changed

11 files changed

+170
-51
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#### Features
44

5+
* [#1520](https://github.com/ruby-grape/grape/pull/1520): Un-deprecate stream-like objects - [@urkle](https://github.com/urkle).
56
* [#2060](https://github.com/ruby-grape/grape/pull/2060): Drop support for Ruby 2.4 - [@dblock](https://github.com/dblock).
67
* [#2060](https://github.com/ruby-grape/grape/pull/2060): Upgraded Rubocop to 0.84.0 - [@dblock](https://github.com/dblock).
78
* Your contribution here.

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3168,7 +3168,7 @@ end
31683168
31693169
Use `body false` to return `204 No Content` without any data or content-type.
31703170
3171-
You can also set the response to a file with `file`.
3171+
You can also set the response to a file with `file` and it will be streamed using Rack::Chunked.
31723172
31733173
```ruby
31743174
class API < Grape::API
@@ -3178,12 +3178,22 @@ class API < Grape::API
31783178
end
31793179
```
31803180
3181-
If you want a file to be streamed using Rack::Chunked, use `stream`.
3181+
If you want to stream non-file data you use the stream method and a Stream object.
3182+
This is simply an object that responds to each and yields for each chunk to send to the client.
3183+
Each chunk will be sent as it is yielded instead of waiting for all of the content to be available.
31823184
31833185
```ruby
3186+
class MyStream
3187+
def each
3188+
yield 'part 1'
3189+
yield 'part 2'
3190+
yield 'part 3'
3191+
end
3192+
end
3193+
31843194
class API < Grape::API
31853195
get '/' do
3186-
stream '/path/to/file'
3196+
stream MyStream.new
31873197
end
31883198
end
31893199
```

UPGRADING.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,42 @@
11
Upgrading Grape
22
===============
33

4+
### Upgrading to >= 1.3.4
5+
6+
#### Reworking stream and file and un-deprecating stream like-objects
7+
8+
Previously in 0.16 stream-like objects were deprecated. This release restores their functionality for use-cases other than file streaming.
9+
10+
For streaming files, simply use file always.
11+
12+
```ruby
13+
class API < Grape::API
14+
get '/' do
15+
file '/path/to/file'
16+
end
17+
end
18+
```
19+
20+
If you want to stream other kinds of content from a streamer object you may.
21+
An example would be a streamer class that fetches several pages of data from a database and streams the formatted responses back.
22+
23+
```ruby
24+
class MyObject
25+
def each
26+
yield '['
27+
# maybe do some paginated DB fetches and return each page
28+
yield {}.to_json
29+
yield ']'
30+
end
31+
end
32+
33+
class API < Grape::API
34+
get '/' do
35+
stream MyObject.new
36+
end
37+
end
38+
```
39+
440
### Upgrading to >= 1.3.3
541

642
#### Nil values for structures

lib/grape.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,12 @@ module Presenters
206206
end
207207
end
208208

209-
module ServeFile
209+
module ServeStream
210210
extend ::ActiveSupport::Autoload
211211
eager_autoload do
212-
autoload :FileResponse
213212
autoload :FileBody
214213
autoload :SendfileResponse
214+
autoload :StreamResponse
215215
end
216216
end
217217
end

lib/grape/dsl/inside_route.rb

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -289,13 +289,13 @@ def return_no_content
289289
# GET /file # => "contents of file"
290290
def file(value = nil)
291291
if value.is_a?(String)
292-
file_body = Grape::ServeFile::FileBody.new(value)
293-
@file = Grape::ServeFile::FileResponse.new(file_body)
292+
file_body = Grape::ServeStream::FileBody.new(value)
293+
stream(file_body)
294294
elsif !value.is_a?(NilClass)
295-
warn '[DEPRECATION] Argument as FileStreamer-like object is deprecated. Use path to file instead.'
296-
@file = Grape::ServeFile::FileResponse.new(value)
295+
warn '[DEPRECATION] Argument as FileStreamer-like object is deprecated. Use path to file instead or stream to use a Stream object.'
296+
stream(value)
297297
else
298-
instance_variable_defined?(:@file) ? @file : nil
298+
stream
299299
end
300300
end
301301

@@ -318,7 +318,16 @@ def stream(value = nil)
318318
header 'Content-Length', nil
319319
header 'Transfer-Encoding', nil
320320
header 'Cache-Control', 'no-cache' # Skips ETag generation (reading the response up front)
321-
file(value)
321+
if value.is_a?(String)
322+
warn '[DEPRECATION] Use `file file_path` to stream a file instead.'
323+
file(value)
324+
elsif value.respond_to?(:each)
325+
@stream = Grape::ServeStream::StreamResponse.new(value)
326+
elsif !value.is_a?(NilClass)
327+
raise ArgumentError, 'Stream object must respond to :each.'
328+
else
329+
instance_variable_defined?(:@stream) ? @stream : nil
330+
end
322331
end
323332

324333
# Allows you to make use of Grape Entities by setting

lib/grape/middleware/formatter.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ def after
3636
def build_formatted_response(status, headers, bodies)
3737
headers = ensure_content_type(headers)
3838

39-
if bodies.is_a?(Grape::ServeFile::FileResponse)
40-
Grape::ServeFile::SendfileResponse.new([], status, headers) do |resp|
41-
resp.body = bodies.file
39+
if bodies.is_a?(Grape::ServeStream::StreamResponse)
40+
Grape::ServeStream::SendfileResponse.new([], status, headers) do |resp|
41+
resp.body = bodies.stream
4242
end
4343
else
4444
# Allow content-type to be explicitly overwritten

lib/grape/serve_file/file_body.rb renamed to lib/grape/serve_stream/file_body.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22

33
module Grape
4-
module ServeFile
4+
module ServeStream
55
CHUNK_SIZE = 16_384
66

77
# Class helps send file through API

lib/grape/serve_file/sendfile_response.rb renamed to lib/grape/serve_stream/sendfile_response.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22

33
module Grape
4-
module ServeFile
4+
module ServeStream
55
# Response should respond to to_path method
66
# for using Rack::SendFile middleware
77
class SendfileResponse < Rack::Response

lib/grape/serve_file/file_response.rb renamed to lib/grape/serve_stream/stream_response.rb

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
# frozen_string_literal: true
22

33
module Grape
4-
module ServeFile
5-
# A simple class used to identify responses which represent files and do not
4+
module ServeStream
5+
# A simple class used to identify responses which represent streams (or files) and do not
66
# need to be formatted or pre-read by Rack::Response
7-
class FileResponse
8-
attr_reader :file
7+
class StreamResponse
8+
attr_reader :stream
99

10-
# @param file [Object]
11-
def initialize(file)
12-
@file = file
10+
# @param stream [Object]
11+
def initialize(stream)
12+
@stream = stream
1313
end
1414

1515
# Equality provided mostly for tests.
1616
#
1717
# @return [Boolean]
1818
def ==(other)
19-
file == other.file
19+
stream == other.stream
2020
end
2121
end
2222
end

spec/grape/dsl/inside_route_spec.rb

Lines changed: 89 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -208,31 +208,55 @@ def initialize
208208
let(:file_path) { '/some/file/path' }
209209

210210
let(:file_response) do
211-
file_body = Grape::ServeFile::FileBody.new(file_path)
212-
Grape::ServeFile::FileResponse.new(file_body)
211+
file_body = Grape::ServeStream::FileBody.new(file_path)
212+
Grape::ServeStream::StreamResponse.new(file_body)
213213
end
214214

215215
before do
216+
subject.header 'Cache-Control', 'cache'
217+
subject.header 'Content-Length', 123
218+
subject.header 'Transfer-Encoding', 'base64'
219+
216220
subject.file file_path
217221
end
218222

219-
it 'returns value wrapped in FileResponse' do
223+
it 'returns value wrapped in StreamResponse' do
220224
expect(subject.file).to eq file_response
221225
end
226+
227+
it 'sets Cache-Control header to no-cache' do
228+
expect(subject.header['Cache-Control']).to eq 'no-cache'
229+
end
230+
231+
it 'sets Content-Length header to nil' do
232+
expect(subject.header['Content-Length']).to eq nil
233+
end
234+
235+
it 'sets Transfer-Encoding header to nil' do
236+
expect(subject.header['Transfer-Encoding']).to eq nil
237+
end
222238
end
223239

224240
context 'as object (backward compatibility)' do
225-
let(:file_object) { Class.new }
241+
let(:file_object) { double('StreamerObject', each: nil) }
226242

227243
let(:file_response) do
228-
Grape::ServeFile::FileResponse.new(file_object)
244+
Grape::ServeStream::StreamResponse.new(file_object)
229245
end
230246

231247
before do
248+
allow(subject).to receive(:warn)
249+
end
250+
251+
it 'emits a warning that a stream object should be sent to the stream method' do
252+
expect(subject).to receive(:warn).with(/Argument as FileStreamer-like/)
253+
232254
subject.file file_object
233255
end
234256

235-
it 'returns value wrapped in FileResponse' do
257+
it 'returns value wrapped in StreamResponse' do
258+
subject.file file_object
259+
236260
expect(subject.file).to eq file_response
237261
end
238262
end
@@ -245,38 +269,77 @@ def initialize
245269

246270
describe '#stream' do
247271
describe 'set' do
248-
let(:file_object) { Class.new }
272+
context 'as a file path (backward compatibility)' do
273+
let(:file_path) { '/some/file/path' }
249274

250-
before do
251-
subject.header 'Cache-Control', 'cache'
252-
subject.header 'Content-Length', 123
253-
subject.header 'Transfer-Encoding', 'base64'
254-
subject.stream file_object
255-
end
275+
let(:file_response) do
276+
file_body = Grape::ServeStream::FileBody.new(file_path)
277+
Grape::ServeStream::StreamResponse.new(file_body)
278+
end
256279

257-
it 'returns value wrapped in FileResponse' do
258-
expect(subject.stream).to eq Grape::ServeFile::FileResponse.new(file_object)
259-
end
280+
before do
281+
allow(subject).to receive(:warn)
282+
end
260283

261-
it 'also sets result of file to value wrapped in FileResponse' do
262-
expect(subject.file).to eq Grape::ServeFile::FileResponse.new(file_object)
263-
end
284+
it 'emits a warning to use file method to stream a file' do
285+
expect(subject).to receive(:warn).with(/file file_path/)
286+
287+
subject.stream file_path
288+
end
289+
290+
it 'returns value wrapped in StreamResponse' do
291+
subject.stream file_path
264292

265-
it 'sets Cache-Control header to no-cache' do
266-
expect(subject.header['Cache-Control']).to eq 'no-cache'
293+
expect(subject.file).to eq file_response
294+
end
267295
end
268296

269-
it 'sets Content-Length header to nil' do
270-
expect(subject.header['Content-Length']).to eq nil
297+
context 'as a stream object' do
298+
let(:stream_object) { double('StreamerObject', each: nil) }
299+
300+
let(:stream_response) do
301+
Grape::ServeStream::StreamResponse.new(stream_object)
302+
end
303+
304+
before do
305+
subject.header 'Cache-Control', 'cache'
306+
subject.header 'Content-Length', 123
307+
subject.header 'Transfer-Encoding', 'base64'
308+
subject.stream stream_object
309+
end
310+
311+
it 'returns value wrapped in StreamResponse' do
312+
expect(subject.stream).to eq stream_response
313+
end
314+
315+
it 'also sets result of file to value wrapped in StreamResponse' do
316+
expect(subject.file).to eq stream_response
317+
end
318+
319+
it 'sets Cache-Control header to no-cache' do
320+
expect(subject.header['Cache-Control']).to eq 'no-cache'
321+
end
322+
323+
it 'sets Content-Length header to nil' do
324+
expect(subject.header['Content-Length']).to eq nil
325+
end
326+
327+
it 'sets Transfer-Encoding header to nil' do
328+
expect(subject.header['Transfer-Encoding']).to eq nil
329+
end
271330
end
272331

273-
it 'sets Transfer-Encoding header to nil' do
274-
expect(subject.header['Transfer-Encoding']).to eq nil
332+
context 'as a non-stream object' do
333+
let(:non_stream_object) { double('NonStreamerObject') }
334+
335+
it 'raises an error that the object must implement :each' do
336+
expect { subject.stream non_stream_object }.to raise_error(ArgumentError, /:each/)
337+
end
275338
end
276339
end
277340

278341
it 'returns default' do
279-
expect(subject.file).to be nil
342+
expect(subject.stream).to be nil
280343
end
281344
end
282345

spec/grape/middleware/formatter_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ def to_xml
380380

381381
context 'send file' do
382382
let(:file) { double(File) }
383-
let(:file_body) { Grape::ServeFile::FileResponse.new(file) }
383+
let(:file_body) { Grape::ServeStream::StreamResponse.new(file) }
384384
let(:app) { ->(_env) { [200, {}, file_body] } }
385385

386386
it 'returns a file response' do

0 commit comments

Comments
 (0)