Skip to content

Commit

Permalink
un-deprecate stream-like objects
Browse files Browse the repository at this point in the history
- deprecate file and replace with sendfile
- update stream to not deprecate stream-like objects
  • Loading branch information
urkle committed Jun 9, 2020
1 parent 2bf2c1a commit 3db3b0a
Show file tree
Hide file tree
Showing 14 changed files with 329 additions and 74 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
### 1.3.4 (Next)
### 1.4.0 (Next)

#### Features

* [#1520](https://github.com/ruby-grape/grape/pull/1520): Un-deprecate stream-like objects - [@urkle](https://github.com/urkle).
* [#2060](https://github.com/ruby-grape/grape/pull/2060): Drop support for Ruby 2.4 - [@dblock](https://github.com/dblock).
* [#2060](https://github.com/ruby-grape/grape/pull/2060): Upgraded Rubocop to 0.84.0 - [@dblock](https://github.com/dblock).
* Your contribution here.
Expand Down
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3168,17 +3168,19 @@ end
Use `body false` to return `204 No Content` without any data or content-type.
You can also set the response to a file with `file`.
You can also set the response to a file with `sendfile`. This works with the
[Rack::Sendfile](https://www.rubydoc.info/gems/rack/Rack/Sendfile) middleware to optimally send
the file through your web server software.
```ruby
class API < Grape::API
get '/' do
file '/path/to/file'
sendfile '/path/to/file'
end
end
```
If you want a file to be streamed using Rack::Chunked, use `stream`.
To stream a file in chunks use `stream`
```ruby
class API < Grape::API
Expand All @@ -3188,6 +3190,26 @@ class API < Grape::API
end
```
If you want to stream non-file data use the `stream` method and a `Stream` object.
This is an object that responds to `each` and yields for each chunk to send to the client.
Each chunk will be sent as it is yielded instead of waiting for all of the content to be available.
```ruby
class MyStream
def each
yield 'part 1'
yield 'part 2'
yield 'part 3'
end
end
class API < Grape::API
get '/' do
stream MyStream.new
end
end
```
## Authentication
### Basic and Digest Auth
Expand Down
64 changes: 64 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,70 @@
Upgrading Grape
===============

### Upgrading to >= 1.4.0

#### Reworking stream and file and un-deprecating stream like-objects

Previously in 0.16 stream-like objects were deprecated. This release restores their functionality for use-cases other than file streaming.

This release deprecated `file` in favor of `sendfile` to better document its purpose.

To deliver a file via the Sendfile support in your web server and have the Rack::Sendfile middleware enabled. See [`Rack::Sendfile`](https://www.rubydoc.info/gems/rack/Rack/Sendfile).
```ruby
class API < Grape::API
get '/' do
sendfile '/path/to/file'
end
end
```

Use `stream` to stream file content in chunks.

```ruby
class API < Grape::API
get '/' do
stream '/path/to/file'
end
end
```

Or use `stream` to stream other kinds of content. In the following example a streamer class
streams paginated data from a database.

```ruby
class MyObject
attr_accessor :result

def initialize(query)
@result = query
end

def each
yield '['
# Do paginated DB fetches and return each page formatted
first = false
result.find_in_batches do |records|
yield process_records(records, first)
first = false
end
yield ']'
end

def process_records(records, first)
buffer = +''
buffer << ',' unless first
buffer << records.map(&:to_json).join(',')
buffer
end
end

class API < Grape::API
get '/' do
stream MyObject.new(Sprocket.all)
end
end
```

### Upgrading to >= 1.3.3

#### Nil values for structures
Expand Down
4 changes: 2 additions & 2 deletions lib/grape.rb
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,12 @@ module Presenters
end
end

module ServeFile
module ServeStream
extend ::ActiveSupport::Autoload
eager_autoload do
autoload :FileResponse
autoload :FileBody
autoload :SendfileResponse
autoload :StreamResponse
end
end
end
Expand Down
40 changes: 31 additions & 9 deletions lib/grape/dsl/inside_route.rb
Original file line number Diff line number Diff line change
Expand Up @@ -279,23 +279,36 @@ def return_no_content
body false
end

# Allows you to define the response as a file-like object.
# Deprecated method to send files to the client. Use `sendfile` or `stream`
def file(value = nil)
if value.is_a?(String)
warn '[DEPRECATION] Use sendfile or stream to send files.'
sendfile(value)
elsif !value.is_a?(NilClass)
warn '[DEPRECATION] Use stream to use a Stream object.'
stream(value)
else
warn '[DEPRECATION] Use sendfile or stream to send files.'
sendfile
end
end

# Allows you to send a file to the client via sendfile.
#
# @example
# get '/file' do
# file FileStreamer.new(...)
# sendfile FileStreamer.new(...)
# end
#
# GET /file # => "contents of file"
def file(value = nil)
def sendfile(value = nil)
if value.is_a?(String)
file_body = Grape::ServeFile::FileBody.new(value)
@file = Grape::ServeFile::FileResponse.new(file_body)
file_body = Grape::ServeStream::FileBody.new(value)
@stream = Grape::ServeStream::StreamResponse.new(file_body)
elsif !value.is_a?(NilClass)
warn '[DEPRECATION] Argument as FileStreamer-like object is deprecated. Use path to file instead.'
@file = Grape::ServeFile::FileResponse.new(value)
raise ArgumentError, 'Argument must be a file path'
else
instance_variable_defined?(:@file) ? @file : nil
stream
end
end

Expand All @@ -318,7 +331,16 @@ def stream(value = nil)
header 'Content-Length', nil
header 'Transfer-Encoding', nil
header 'Cache-Control', 'no-cache' # Skips ETag generation (reading the response up front)
file(value)
if value.is_a?(String)
file_body = Grape::ServeStream::FileBody.new(value)
@stream = Grape::ServeStream::StreamResponse.new(file_body)
elsif value.respond_to?(:each)
@stream = Grape::ServeStream::StreamResponse.new(value)
elsif !value.is_a?(NilClass)
raise ArgumentError, 'Stream object must respond to :each.'
else
instance_variable_defined?(:@stream) ? @stream : nil
end
end

# Allows you to make use of Grape Entities by setting
Expand Down
6 changes: 3 additions & 3 deletions lib/grape/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def initialize(new_settings, options = {}, &block)
@block = nil

@status = nil
@file = nil
@stream = nil
@body = nil
@proc = nil

Expand Down Expand Up @@ -271,8 +271,8 @@ def run
# status verifies body presence when DELETE
@body ||= response_object

# The body commonly is an Array of Strings, the application instance itself, or a File-like object
response_object = file || [body]
# The body commonly is an Array of Strings, the application instance itself, or a Stream-like object
response_object = stream || [body]

[status, header, response_object]
ensure
Expand Down
6 changes: 3 additions & 3 deletions lib/grape/middleware/formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ def after
def build_formatted_response(status, headers, bodies)
headers = ensure_content_type(headers)

if bodies.is_a?(Grape::ServeFile::FileResponse)
Grape::ServeFile::SendfileResponse.new([], status, headers) do |resp|
resp.body = bodies.file
if bodies.is_a?(Grape::ServeStream::StreamResponse)
Grape::ServeStream::SendfileResponse.new([], status, headers) do |resp|
resp.body = bodies.stream
end
else
# Allow content-type to be explicitly overwritten
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

module Grape
module ServeFile
module ServeStream
CHUNK_SIZE = 16_384

# Class helps send file through API
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

module Grape
module ServeFile
module ServeStream
# Response should respond to to_path method
# for using Rack::SendFile middleware
class SendfileResponse < Rack::Response
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
# frozen_string_literal: true

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

# @param file [Object]
def initialize(file)
@file = file
# @param stream [Object]
def initialize(stream)
@stream = stream
end

# Equality provided mostly for tests.
#
# @return [Boolean]
def ==(other)
file == other.file
stream == other.stream
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/grape/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

module Grape
# The current version of Grape.
VERSION = '1.3.4'
VERSION = '1.4.0'
end
Loading

0 comments on commit 3db3b0a

Please sign in to comment.