Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Renderable#data method to be able to respond with IO and String from an Action #1220

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions spec/lucky/data_response_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
require "../spec_helper"

include ContextHelper

describe Lucky::FileResponse do
describe "#print" do
describe "status_code" do
it "uses the default status if none is set" do
context = build_context
print_data_response(context, io: fixture_io)

context.response.status_code.should eq Lucky::TextResponse::DEFAULT_STATUS
end

it "uses the passed in status" do
context = build_context
print_data_response(context, io: fixture_io, status: 300)

context.response.status_code.should eq 300
end

it "uses the response status if it's set, and Lucky::TextResponse status is nil" do
context = build_context
context.response.status_code = 300
print_data_response(context, io: fixture_io)

context.response.status_code.should eq 300
end
end

describe "content_type" do
it "uses the default content_type when no extension is present" do
context = build_context
print_data_response(context, io: fixture_io)

context.response.headers["Content-Type"].should eq "application/octet-stream"
end

it "uses the provided content_type" do
context = build_context
print_data_response(context, io: fixture_io, content_type: "text/plain")

context.response.headers["Content-Type"].should eq "text/plain"
end
end

describe "disposition" do
it "is 'attachment' by default" do
context = build_context
print_data_response(context, io: fixture_io)

context.response.headers["Content-Disposition"].should eq "attachment"
end

it "can be changed to 'inline'" do
context = build_context
print_data_response(context, io: fixture_io, disposition: "inline")

context.response.headers["Content-Disposition"].should eq "inline"
end

it "can set the downloaded file's name" do
context = build_context
print_data_response(context, io: fixture_io, filename: "logo.png")

context.response.headers["Content-Disposition"].should eq %(attachment; filename="logo.png")
end
end
end
end

private def print_data_response(context : HTTP::Server::Context,
io : IO,
content_type : String = "application/octet-stream",
disposition : String = "attachment",
filename : String? = nil,
status : Int32? = nil)
response = Lucky::DataResponse.new(context,
io,
content_type,
disposition: disposition,
filename: filename,
status: status)
response.print
end

private def fixture_io(content : String = "Lucky is awesome")
IO::Memory.new(content)
end
80 changes: 80 additions & 0 deletions src/lucky/data_response.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Return a data for the request.
#
# `data` can be used to return contents of the IO as a file to the browser, or
# render the contents of the IO inline to a web browser. Options for the
# method:
#
# * `io` - first argument, _required_. The IO to the data from.
# * `content_type` - defaults to "application/octet-stream".
# * `disposition` - default "attachment" (downloads file), or "inline"
# (renders file in browser).
# * `filename` - default `nil`. When overridden and paired with
# `disposition: "attachment"` this will download file with the provided
# filename.
# * status - `Int32` - the HTTP status code to
# return with.
#
# Examples:
#
# ```crystal
# class Rendering::Data < Lucky::Action
# get "/foo" do
# data IO::Memory.new("Lucky is awesome")
# end
# end
# ```
#
# `data` can also be used with a `String` first argument.
#
# ```crystal
# class Rendering::Data < Lucky::Action
# get "/foo" do
# data "Lucky is awesome"
# end
# end
# ```
class Lucky::DataResponse < Lucky::Response
DEFAULT_STATUS = 200

getter context, io, content_type, filename, debug_message, headers

def initialize(@context : HTTP::Server::Context,
@io : IO,
@content_type : String = "application/octet-stream",
@disposition : String = "attachment",
@filename : String? = nil,
@status : Int32? = nil,
@debug_message : String? = nil)
end

def print
set_response_headers
context.response.status_code = status
content_length = IO.copy(io, context.response)
context.response.content_length = content_length
end

def status : Int
@status || context.response.status_code || DEFAULT_STATUS
end

private def set_response_headers : Nil
context.response.content_type = content_type
context.response.headers["Accept-Ranges"] = "bytes"
context.response.headers["X-Content-Type-Options"] = "nosniff"
context.response.headers["Content-Transfer-Encoding"] = "binary"
context.response.headers["Content-Disposition"] = disposition
end

private def custom_filename? : Bool
!!filename
end

def disposition : String
if custom_filename?
%(#{@disposition}; filename="#{filename}")
else
@disposition
end
end
end
20 changes: 20 additions & 0 deletions src/lucky/renderable.cr
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,26 @@ module Lucky::Renderable
file(path, content_type, disposition, filename, status.value)
end

def data(
data : String,
content_type : String = "application/octet-stream",
disposition : String = "attachment",
filename : String? = nil,
status : Int32? = nil
) : Lucky::DataResponse
data(IO::Memory.new(data), content_type, disposition, filename, status)
end

def data(
io : IO,
content_type : String = "application/octet-stream",
disposition : String = "attachment",
filename : String? = nil,
status : Int32? = nil
) : Lucky::DataResponse
Lucky::DataResponse.new(context, io.gets_to_end, content_type, disposition, filename, status)
end

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should also add an overload that takes HTTP::Status like the others? Just to keep those consistent.

def send_text_response(
body : String,
content_type : String,
Expand Down