Skip to content

Commit

Permalink
Added Renderable#data method to be able to respond with IO and String…
Browse files Browse the repository at this point in the history
… from an Action (#1220)

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

* Refactored Lucky::DataResponse to accept data as a String
  • Loading branch information
igor-alexandrov authored Jul 21, 2020
1 parent bb92bb2 commit 29fa30c
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 0 deletions.
95 changes: 95 additions & 0 deletions spec/lucky/data_response_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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)

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, 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)

context.response.status_code.should eq 300
end
end

describe "content_length" do
it "calculates from a bytesize of the data" do
context = build_context
data = "Lucky is awesome 🤟"
print_data_response(context, data: data)

context.response.headers["Content-Length"].should eq data.bytesize.to_s
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)

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, 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)

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

it "can be changed to 'inline'" do
context = build_context
print_data_response(context, 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, 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,
data : String = "Lucky is awesome",
content_type : String = "application/octet-stream",
disposition : String = "attachment",
filename : String? = nil,
status : Int32? = nil)
response = Lucky::DataResponse.new(context,
data,
content_type,
disposition: disposition,
filename: filename,
status: status)
response.print
end
75 changes: 75 additions & 0 deletions src/lucky/data_response.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# 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:
#
# * `data` - first argument, _required_. The data that should be sent.
# * `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 Reports::MyReport < ApiAction
# get "/reports/my_report" do
# result = CSV.build do |csv|
# csv.row "one", "two"
# csv.row "three"
# end

# data result, filename: "my_report.csv"
# end
# end
# ```
class Lucky::DataResponse < Lucky::Response
DEFAULT_STATUS = 200

getter context, data, content_type, filename, debug_message, headers

def initialize(@context : HTTP::Server::Context,
@data : String,
@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.print data
end

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

private def set_response_headers : Nil
context.response.content_length = data.bytesize
context.response.content_type = content_type
context.response.status_code = status
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
10 changes: 10 additions & 0 deletions src/lucky/renderable.cr
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,16 @@ 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
Lucky::DataResponse.new(context, data, content_type, disposition, filename, status)
end

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

0 comments on commit 29fa30c

Please sign in to comment.