diff --git a/spec/static_file_handler_spec.cr b/spec/static_file_handler_spec.cr
index 1aac161..54293b4 100644
--- a/spec/static_file_handler_spec.cr
+++ b/spec/static_file_handler_spec.cr
@@ -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
diff --git a/src/kemal.cr b/src/kemal.cr
index 782826c..11f47e7 100644
--- a/src/kemal.cr
+++ b/src/kemal.cr
@@ -1,2 +1,2 @@
require "./kemal/base"
-require "./kemal/dsl"
\ No newline at end of file
+require "./kemal/dsl"
diff --git a/src/kemal/base.cr b/src/kemal/base.cr
index 23a2e45..cf118ae 100644
--- a/src/kemal/base.cr
+++ b/src/kemal/base.cr
@@ -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)
diff --git a/src/kemal/config.cr b/src/kemal/config.cr
index 99b1f9d..b129c65 100644
--- a/src/kemal/config.cr
+++ b/src/kemal/config.cr
@@ -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
diff --git a/src/kemal/dsl.cr b/src/kemal/dsl.cr
index ba1c021..0d94283 100644
--- a/src/kemal/dsl.cr
+++ b/src/kemal/dsl.cr
@@ -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 -> _)
diff --git a/src/kemal/helpers/helpers.cr b/src/kemal/dsl/file_helpers.cr
similarity index 55%
rename from src/kemal/helpers/helpers.cr
rename to src/kemal/dsl/file_helpers.cr
index e6d1d13..457b303 100644
--- a/src/kemal/helpers/helpers.cr
+++ b/src/kemal/dsl/file_helpers.cr
@@ -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`.
diff --git a/src/kemal/dsl/macros.cr b/src/kemal/dsl/macros.cr
new file mode 100644
index 0000000..094e5d3
--- /dev/null
+++ b/src/kemal/dsl/macros.cr
@@ -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
diff --git a/src/kemal/dsl/templates.cr b/src/kemal/dsl/templates.cr
new file mode 100644
index 0000000..90c6e9d
--- /dev/null
+++ b/src/kemal/dsl/templates.cr
@@ -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
diff --git a/src/kemal/helpers/file_helpers.cr b/src/kemal/helpers/file_helpers.cr
new file mode 100644
index 0000000..c30d0e9
--- /dev/null
+++ b/src/kemal/helpers/file_helpers.cr
@@ -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
diff --git a/src/kemal/helpers/macros.cr b/src/kemal/helpers/macros.cr
index 4b5e309..1cfffde 100644
--- a/src/kemal/helpers/macros.cr
+++ b/src/kemal/helpers/macros.cr
@@ -1,98 +1,101 @@
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
-# common use is to populate different parts of your layout from your view.
-#
-# The currently supported engines are: ecr and slang.
-#
-# ## Usage
-#
-# You call `content_for`, generally from a view, to capture a block of markup
-# giving it an identifier:
-#
-# ```
-# # index.ecr
-# <% content_for "some_key" do %>
-#
#{HTML.escape(backtrace)}" + else + "
Something wrong with the server :(
" + end + + template = <<-HTML + + + + + + +