From 1cd329b92fbaebb0d087997d68806194320ab547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Sun, 16 Jul 2017 21:49:27 +0200 Subject: [PATCH] Refactor helpers into module namespaces --- spec/static_file_handler_spec.cr | 1 + src/kemal.cr | 2 +- src/kemal/base.cr | 6 + src/kemal/config.cr | 6 +- src/kemal/dsl.cr | 1 + .../helpers.cr => dsl/file_helpers.cr} | 89 +-------- src/kemal/dsl/macros.cr | 47 +++++ src/kemal/dsl/templates.cr | 7 + src/kemal/helpers/file_helpers.cr | 136 +++++++++++++ src/kemal/helpers/macros.cr | 185 +++++++++--------- src/kemal/helpers/templates.cr | 54 +++++ 11 files changed, 357 insertions(+), 177 deletions(-) rename src/kemal/{helpers/helpers.cr => dsl/file_helpers.cr} (55%) create mode 100644 src/kemal/dsl/macros.cr create mode 100644 src/kemal/dsl/templates.cr create mode 100644 src/kemal/helpers/file_helpers.cr 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 %> -# ... -# <% end %> -# ``` -# -# Then, you call `yield_content` with that identifier, generally from a -# layout, to render the captured block: -# -# ``` -# # layout.ecr -# <%= yield_content "some_key" %> -# ``` -# -# ## And How Is This Useful? -# -# For example, some of your views might need a few javascript tags and -# stylesheets, but you don't want to force this files in all your pages. -# Then you can put `<%= yield_content :scripts_and_styles %>` on your -# layout, inside the tag, and each view can call `content_for` -# setting the appropriate set of tags that should be added to the layout. -macro content_for(key, file = __FILE__) - %proc = ->() { - __kilt_io__ = IO::Memory.new - {{ yield }} - __kilt_io__.to_s - } + # `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 %> + # ... + # <% end %> + # ``` + # + # Then, you call `yield_content` with that identifier, generally from a + # layout, to render the captured block: + # + # ``` + # # layout.ecr + # <%= yield_content "some_key" %> + # ``` + # + # ## And How Is This Useful? + # + # For example, some of your views might need a few javascript tags and + # stylesheets, but you don't want to force this files in all your pages. + # Then you can put `<%= yield_content :scripts_and_styles %>` on your + # layout, inside the tag, and each view can call `content_for` + # setting the appropriate set of tags that should be added to the layout. + macro content_for(key, file = __FILE__) + %proc = ->() { + __kilt_io__ = IO::Memory.new + {{ yield }} + __kilt_io__.to_s + } - CONTENT_FOR_BLOCKS[{{key}}] = Tuple.new {{file}}, %proc - nil -end + 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] - %proc.call if __content_filename__ == __caller_filename__ + # 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] + %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" + # ``` + macro render(filename, layout) + __content_filename__ = {{filename}} + content = render {{filename}} + render {{layout}} + end + + # Render view with the given filename. + macro render(filename) + Kilt.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 = "") + {{env}}.response.status_code = {{status_code}} + {{env}}.response.print {{response}} + {{env}}.response.close + next + end + + # Extends context storage with user defined types. + # + # ``` + # class User + # property name + # end + # + # add_context_storage_type(User) + # ``` + macro add_context_storage_type(type) + {{ HTTP::Server::Context::STORE_MAPPINGS.push(type) }} end end - -# Render view with a layout as the superview. -# -# ``` -# render "src/views/index.ecr", "src/views/layout.ecr" -# ``` -macro render(filename, layout) - __content_filename__ = {{filename}} - content = render {{filename}} - render {{layout}} -end - -# Render view with the given filename. -macro render(filename) - Kilt.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 = "") - {{env}}.response.status_code = {{status_code}} - {{env}}.response.print {{response}} - {{env}}.response.close - next -end - -# Extends context storage with user defined types. -# -# ``` -# class User -# property name -# end -# -# add_context_storage_type(User) -# ``` -macro add_context_storage_type(type) - {{ HTTP::Server::Context::STORE_MAPPINGS.push(type) }} -end diff --git a/src/kemal/helpers/templates.cr b/src/kemal/helpers/templates.cr index b343fc8..924941f 100644 --- a/src/kemal/helpers/templates.cr +++ b/src/kemal/helpers/templates.cr @@ -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 @@ -32,4 +33,57 @@ def render_500(context, exception, verbosity) context.response.print template context +======= +module Kemal::Templates + def render_404 + template = <<-HTML + + + + + + +

Kemal doesn't know this way.

+ + + + HTML + end + + def render_500(context, backtrace, verbosity) + message = if verbosity + "
#{HTML.escape(backtrace)}
" + else + "

Something wrong with the server :(

" + end + + template = <<-HTML + + + + + + +

Kemal has encountered an error. (500)

+ #{message} + + + HTML + context.response.status_code = 500 + context.response.print template + context + end +>>>>>>> Refactor helpers into module namespaces end