Refactor helpers into module namespaces

This commit is contained in:
Johannes Müller 2017-07-16 21:49:27 +02:00 committed by sdogruyol
parent aaa2109837
commit 1cd329b92f
11 changed files with 357 additions and 177 deletions

View file

@ -4,6 +4,7 @@ private def handle(request, fallthrough = true)
io = IO::Memory.new
response = HTTP::Server::Response.new(io)
context = HTTP::Server::Context.new(request, response)
context.app = Kemal.application
handler = Kemal::StaticFileHandler.new "#{__DIR__}/static", fallthrough
handler.call context
response.close

View file

@ -1,3 +1,5 @@
require "./helpers/*"
# Kemal Base
# The DSL currently consists of
# - get post put patch delete options
@ -5,6 +7,10 @@
# - before_*
# - error
class Kemal::Base
include FileHelpers
include Templates
include Macros
HTTP_METHODS = %w(get post put patch delete options)
FILTER_METHODS = %w(get post put patch delete options all)

View file

@ -51,7 +51,11 @@ module Kemal
def serve_static?(key)
config = @serve_static
config.try(&.[key]?) || config == true
(config.is_a?(Hash) && config[key]?) || false
end
def extra_options(&@extra_options : OptionParser ->)
end
end
end

View file

@ -6,6 +6,7 @@
# - WebSocket(ws)
# - before_*
# - error
require "./dsl/*"
{% for method in Kemal::Base::HTTP_METHODS %}
def {{method.id}}(path, &block : HTTP::Server::Context -> _)

View file

@ -26,7 +26,7 @@ end
# Logs the output via `logger`.
# This is the built-in `Kemal::LogHandler` by default which uses STDOUT.
def log(message : String)
Kemal.application.logger.write "#{message}\n"
Kemal.application.log(message)
end
# Enables / Disables logging.
@ -65,7 +65,6 @@ end
# ```
def logger(logger : Kemal::BaseLogHandler)
Kemal.application.logger = logger
Kemal.application.add_handler logger
end
# Enables / Disables static file serving.
@ -94,7 +93,7 @@ end
# end
# ```
def headers(env : HTTP::Server::Context, additional_headers : Hash(String, String))
env.response.headers.merge!(additional_headers)
Kemal.application.headers(env, additional_headers)
end
# Send a file with given path and base the mime-type on the file extension
@ -110,82 +109,7 @@ end
# send_file env, "./path/to/file", "image/jpeg"
# ```
def send_file(env : HTTP::Server::Context, path : String, mime_type : String? = nil)
config = Kemal.config.serve_static
file_path = File.expand_path(path, Dir.current)
mime_type ||= Kemal::Utils.mime_type(file_path)
env.response.content_type = mime_type
env.response.headers["Accept-Ranges"] = "bytes"
env.response.headers["X-Content-Type-Options"] = "nosniff"
minsize = 860 # http://webmasters.stackexchange.com/questions/31750/what-is-recommended-minimum-object-size-for-gzip-performance-benefits ??
request_headers = env.request.headers
filesize = File.size(file_path)
filestat = File.info(file_path)
Kemal.config.static_headers.try(&.call(env.response, file_path, filestat))
File.open(file_path) do |file|
if env.request.method == "GET" && env.request.headers.has_key?("Range")
next multipart(file, env)
end
condition = config.is_a?(Hash) && config["gzip"]? == true && filesize > minsize && Kemal::Utils.zip_types(file_path)
if condition && request_headers.includes_word?("Accept-Encoding", "gzip")
env.response.headers["Content-Encoding"] = "gzip"
Gzip::Writer.open(env.response) do |deflate|
IO.copy(file, deflate)
end
elsif condition && request_headers.includes_word?("Accept-Encoding", "deflate")
env.response.headers["Content-Encoding"] = "deflate"
Flate::Writer.open(env.response) do |deflate|
IO.copy(file, deflate)
end
else
env.response.content_length = filesize
IO.copy(file, env.response)
end
end
return
end
private def multipart(file, env : HTTP::Server::Context)
# See http://httpwg.org/specs/rfc7233.html
fileb = file.size
startb = endb = 0
if match = env.request.headers["Range"].match /bytes=(\d{1,})-(\d{0,})/
startb = match[1].to_i { 0 } if match.size >= 2
endb = match[2].to_i { 0 } if match.size >= 3
end
endb = fileb - 1 if endb == 0
if startb < endb < fileb
content_length = 1 + endb - startb
env.response.status_code = 206
env.response.content_length = content_length
env.response.headers["Accept-Ranges"] = "bytes"
env.response.headers["Content-Range"] = "bytes #{startb}-#{endb}/#{fileb}" # MUST
if startb > 1024
skipped = 0
# file.skip only accepts values less or equal to 1024 (buffer size, undocumented)
until (increase_skipped = skipped + 1024) > startb
file.skip(1024)
skipped = increase_skipped
end
if (skipped_minus_startb = skipped - startb) > 0
file.skip skipped_minus_startb
end
else
file.skip(startb)
end
IO.copy(file, env.response, content_length)
else
env.response.content_length = fileb
env.response.status_code = 200 # Range not satisfable, see 4.4 Note
IO.copy(file, env.response)
end
Kemal.application.send_file(env, path, mime_type)
end
# Send a file with given data and default `application/octet-stream` mime_type.
@ -200,10 +124,7 @@ end
# send_file env, data_slice, "image/jpeg"
# ```
def send_file(env : HTTP::Server::Context, data : Slice(UInt8), mime_type : String? = nil)
mime_type ||= "application/octet-stream"
env.response.content_type = mime_type
env.response.content_length = data.bytesize
env.response.write data
Kemal.application.send_file(env, data, mime_type)
end
# Configures an `HTTP::Server::Response` to compress the response
@ -211,7 +132,7 @@ end
#
# Disabled by default.
def gzip(status : Bool = false)
add_handler HTTP::CompressHandler.new if status
Kemal.application.gzip(status)
end
# Adds headers to `Kemal::StaticFileHandler`. This is especially useful for `CORS`.

47
src/kemal/dsl/macros.cr Normal file
View file

@ -0,0 +1,47 @@
def content_for_blocks
Kemal.application.content_for_blocks
end
macro content_for(key, file = __FILE__)
Kemal::Macros.content_for({{key}}, {{file}}) do
{{yield}}
end
end
# Yields content for the given key if a `content_for` block exists for that key.
macro yield_content(key)
Kemal::Macros.yield_content({{key}})
end
# Render view with a layout as the superview.
#
# render "src/views/index.ecr", "src/views/layout.ecr"
#
macro render(filename, layout)
Kemal::Macros.render({{filename}}, {{layout}})
end
# Render view with the given filename.
macro render(filename)
Kemal::Macros.render({{filename}})
end
# Halt execution with the current context.
# Returns 200 and an empty response by default.
#
# halt env, status_code: 403, response: "Forbidden"
macro halt(env, status_code = 200, response = "")
Kemal::Macros.halt({{env}}, {{status_code}}, {{response}})
end
# Extends context storage with user defined types.
#
# class User
# property name
# end
#
# add_context_storage_type(User)
#
macro add_context_storage_type(type)
Kemal::Macros.add_context_storage_type({{type}})
end

View file

@ -0,0 +1,7 @@
def render_404
Kemal.application.render_404
end
def render_500(context, backtrace, verbosity)
Kemal.application.render_500(context, backtrace, verbosity)
end

View file

@ -0,0 +1,136 @@
module Kemal::FileHelpers
def log(message)
logger.write "#{message}\n"
end
# Send a file with given path and base the mime-type on the file extension
# or default `application/octet-stream` mime_type.
#
# ```
# send_file env, "./path/to/file"
# ```
#
# Optionally you can override the mime_type
#
# ```
# send_file env, "./path/to/file", "image/jpeg"
# ```
def send_file(env : HTTP::Server::Context, path : String, mime_type : String? = nil)
config = env.app.config
file_path = File.expand_path(path, Dir.current)
mime_type ||= Kemal::Utils.mime_type(file_path)
env.response.content_type = mime_type
env.response.headers["Accept-Ranges"] = "bytes"
env.response.headers["X-Content-Type-Options"] = "nosniff"
minsize = 860 # http://webmasters.stackexchange.com/questions/31750/what-is-recommended-minimum-object-size-for-gzip-performance-benefits ??
request_headers = env.request.headers
filesize = File.size(file_path)
filestat = File.stat(file_path)
config.static_headers.try(&.call(env.response, file_path, filestat))
File.open(file_path) do |file|
if env.request.method == "GET" && env.request.headers.has_key?("Range")
next multipart(file, env)
end
if request_headers.includes_word?("Accept-Encoding", "gzip") && config.serve_static?("gzip") && filesize > minsize && Kemal::Utils.zip_types(file_path)
env.response.headers["Content-Encoding"] = "gzip"
Gzip::Writer.open(env.response) do |deflate|
IO.copy(file, deflate)
end
elsif request_headers.includes_word?("Accept-Encoding", "deflate") && config.serve_static?("gzip") && filesize > minsize && Kemal::Utils.zip_types(file_path)
env.response.headers["Content-Encoding"] = "deflate"
Flate::Writer.open(env.response) do |deflate|
IO.copy(file, deflate)
end
else
env.response.content_length = filesize
IO.copy(file, env.response)
end
end
return
end
private def multipart(file, env : HTTP::Server::Context)
# See http://httpwg.org/specs/rfc7233.html
fileb = file.size
range = env.request.headers["Range"]
match = range.match(/bytes=(\d{1,})-(\d{0,})/)
startb = 0
endb = 0
if match
if match.size >= 2
startb = match[1].to_i { 0 }
end
if match.size >= 3
endb = match[2].to_i { 0 }
end
end
if endb == 0
endb = fileb - 1
end
if startb < endb && endb < fileb
content_length = 1 + endb - startb
env.response.status_code = 206
env.response.content_length = content_length
env.response.headers["Accept-Ranges"] = "bytes"
env.response.headers["Content-Range"] = "bytes #{startb}-#{endb}/#{fileb}" # MUST
if startb > 1024
skipped = 0
# file.skip only accepts values less or equal to 1024 (buffer size, undocumented)
until skipped + 1024 > startb
file.skip(1024)
skipped += 1024
end
if skipped - startb > 0
file.skip(skipped - startb)
end
else
file.skip(startb)
end
IO.copy(file, env.response, content_length)
else
env.response.content_length = fileb
env.response.status_code = 200 # Range not satisfable, see 4.4 Note
IO.copy(file, env.response)
end
end
def headers(env, additional_headers)
env.response.headers.merge!(additional_headers)
end
# Send a file with given data and default `application/octet-stream` mime_type.
#
# ```
# send_file env, data_slice
# ```
#
# Optionally you can override the mime_type
#
# ```
# send_file env, data_slice, "image/jpeg"
# ```
def send_file(env : HTTP::Server::Context, data : Slice(UInt8), mime_type : String? = nil)
mime_type ||= "application/octet-stream"
env.response.content_type = mime_type
env.response.content_length = data.bytesize
env.response.write data
end
# Configures an `HTTP::Server::Response` to compress the response
# output, either using gzip or deflate, depending on the `Accept-Encoding` request header.
#
# Disabled by default.
def gzip(status : Bool = false)
add_handler HTTP::CompressHandler.new if status
end
end

View file

@ -1,6 +1,9 @@
require "kilt"
CONTENT_FOR_BLOCKS = Hash(String, Tuple(String, Proc(String))).new
module Kemal::Macros
def content_for_blocks
@content_for_blocks ||= Hash(String, Tuple(String, Proc(String))).new
end
# `content_for` is a set of helpers that allows you to capture
# blocks inside views to be rendered later during the request. The most
@ -42,21 +45,20 @@ macro content_for(key, file = __FILE__)
__kilt_io__.to_s
}
CONTENT_FOR_BLOCKS[{{key}}] = Tuple.new {{file}}, %proc
content_for_blocks[{{key}}] = Tuple.new {{file}}, %proc
nil
end
# Yields content for the given key if a `content_for` block exists for that key.
macro yield_content(key)
if CONTENT_FOR_BLOCKS.has_key?({{key}})
__caller_filename__ = CONTENT_FOR_BLOCKS[{{key}}][0]
%proc = CONTENT_FOR_BLOCKS[{{key}}][1]
if content_for_blocks.has_key?({{key}})
__caller_filename__ = content_for_blocks[{{key}}][0]
%proc = content_for_blocks[{{key}}][1]
%proc.call if __content_filename__ == __caller_filename__
end
end
# Render view with a layout as the superview.
#
# ```
# render "src/views/index.ecr", "src/views/layout.ecr"
# ```
@ -96,3 +98,4 @@ end
macro add_context_storage_type(type)
{{ HTTP::Server::Context::STORE_MAPPINGS.push(type) }}
end
end

View file

@ -1,6 +1,7 @@
# This file contains the built-in view templates that Kemal uses.
# Currently it contains templates for 404 and 500 error codes.
<<<<<<< HEAD
def render_404
<<-HTML
<!DOCTYPE html>
@ -32,4 +33,57 @@ def render_500(context, exception, verbosity)
context.response.print template
context
=======
module Kemal::Templates
def render_404
template = <<-HTML
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
body { text-align:center;font-family:helvetica,arial;font-size:22px;
color:#888;margin:20px}
img { max-width: 579px; width: 100%; }
#c {margin:0 auto;width:500px;text-align:left}
</style>
</head>
<body>
<h2>Kemal doesn't know this way.</h2>
<img src="/__kemal__/404.png">
</body>
</html>
HTML
end
def render_500(context, backtrace, verbosity)
message = if verbosity
"<pre>#{HTML.escape(backtrace)}</pre>"
else
"<p>Something wrong with the server :(</p>"
end
template = <<-HTML
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
body { text-align:center;font-family:helvetica,arial;font-size:22px;
color:#888;margin:20px}
#c {margin:0 auto;width:500px;text-align:left}
pre {text-align:left;font-size:14px;color:#fff;background-color:#222;
font-family:Operator,"Source Code Pro",Menlo,Monaco,Inconsolata,monospace;
line-height:1.5;padding:10px;border-radius:2px;overflow:scroll}
</style>
</head>
<body>
<h2>Kemal has encountered an error. (500)</h2>
#{message}
</body>
</html>
HTML
context.response.status_code = 500
context.response.print template
context
end
>>>>>>> Refactor helpers into module namespaces
end