From aaa2109837eefcb381996985424de07fe2f23949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Sun, 16 Jul 2017 19:16:12 +0200 Subject: [PATCH 01/18] Refactor global namespace DSL into OOP Kemal::Base --- .gitignore | 3 +- spec/config_spec.cr | 29 ++-- spec/context_spec.cr | 25 ++-- spec/exception_handler_spec.cr | 24 ++-- spec/handler_spec.cr | 15 ++- spec/helpers_spec.cr | 22 ++-- spec/init_handler_spec.cr | 8 +- spec/middleware/filters_spec.cr | 14 +- spec/param_parser_spec.cr | 4 +- spec/run_spec.cr | 35 ++--- spec/spec_helper.cr | 17 +-- spec/websocket_handler_spec.cr | 20 +-- src/kemal.cr | 100 +------------- src/kemal/base.cr | 227 ++++++++++++++++++++++++++++++++ src/kemal/base_log_handler.cr | 2 + src/kemal/cli.cr | 3 +- src/kemal/config.cr | 167 ++++------------------- src/kemal/dsl.cr | 24 ++-- src/kemal/exception_handler.cr | 29 ++-- src/kemal/ext/context.cr | 6 +- src/kemal/filter_handler.cr | 6 +- src/kemal/helpers/helpers.cr | 12 +- src/kemal/init_handler.cr | 6 +- src/kemal/route_handler.cr | 6 +- src/kemal/websocket_handler.cr | 5 +- 25 files changed, 421 insertions(+), 388 deletions(-) create mode 100644 src/kemal/base.cr diff --git a/.gitignore b/.gitignore index 5a0d7aa7..5411668f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ +/doc/ +/bin/ /lib/ -/.crystal/ /.shards/ *.log diff --git a/spec/config_spec.cr b/spec/config_spec.cr index 31a84380..48fc31d0 100644 --- a/spec/config_spec.cr +++ b/spec/config_spec.cr @@ -2,15 +2,17 @@ require "./spec_helper" describe "Config" do it "sets default port to 3000" do - Kemal::Config.new.port.should eq 3000 + config = Kemal::Config.new + config.port.should eq 3000 end it "sets default environment to development" do - Kemal::Config.new.env.should eq "development" + config = Kemal::Config.new + config.env.should eq "development" end it "sets environment to production" do - config = Kemal.config + config = Kemal::Config.new config.env = "production" config.env.should eq "production" end @@ -20,28 +22,28 @@ describe "Config" do end it "sets host binding" do - config = Kemal.config + config = Kemal::Config.new config.host_binding = "127.0.0.1" config.host_binding.should eq "127.0.0.1" end it "adds a custom handler" do - config = Kemal.config - config.add_handler CustomTestHandler.new - Kemal.config.setup - config.handlers.size.should eq(7) + application = Kemal::Base.new + application.add_handler CustomTestHandler.new + application.setup + application.handlers.size.should eq(8) end it "toggles the shutdown message" do - config = Kemal.config + config = Kemal::Config.new config.shutdown_message = false - config.shutdown_message.should eq false + config.shutdown_message?.should be_false config.shutdown_message = true - config.shutdown_message.should eq true + config.shutdown_message?.should be_true end it "adds custom options" do - config = Kemal.config + config = Kemal::Config.new ARGV.push("--test") ARGV.push("FOOBAR") test_option = nil @@ -51,7 +53,8 @@ describe "Config" do test_option = opt end end - Kemal::CLI.new ARGV + + Kemal::CLI.new(ARGV, config) test_option.should eq("FOOBAR") end diff --git a/spec/context_spec.cr b/spec/context_spec.cr index c9729266..0148d431 100644 --- a/spec/context_spec.cr +++ b/spec/context_spec.cr @@ -90,18 +90,17 @@ describe "Context" do context.get("another_context_test").as(AnotherContextStorageType).name.should eq "kemal-context" end - it "fetches non-existent keys from store with get?" do - get "/" { } - - request = HTTP::Request.new("GET", "/") - io = IO::Memory.new - response = HTTP::Server::Response.new(io) - context = HTTP::Server::Context.new(request, response) - Kemal::FilterHandler::INSTANCE.call(context) - Kemal::RouteHandler::INSTANCE.call(context) - - context.get?("non_existent_key").should be_nil - context.get?("another_non_existent_key").should be_nil - end + request = HTTP::Request.new("GET", "/") + io = IO::Memory.new + response = HTTP::Server::Response.new(io) + context = HTTP::Server::Context.new(request, response) + context.app = Kemal.application + Kemal.application.filter_handler.call(context) + Kemal.application.route_handler.call(context) + context.store["key"].should eq "value" + context.store["before_get"].should eq "Kemal" + context.store["before_get_int"].should eq 123 + context.store["before_get_float"].should eq 3.5 + context.store["before_get_context_test"].as(TestContextStorageType).id.should eq 32 end end diff --git a/spec/exception_handler_spec.cr b/spec/exception_handler_spec.cr index 78da4263..bfa875d8 100644 --- a/spec/exception_handler_spec.cr +++ b/spec/exception_handler_spec.cr @@ -1,5 +1,7 @@ require "./spec_helper" +private INSTANCE = Kemal::ExceptionHandler.new + describe "Kemal::ExceptionHandler" do it "renders 404 on route not found" do get "/" do @@ -10,7 +12,7 @@ describe "Kemal::ExceptionHandler" do io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) - Kemal::ExceptionHandler::INSTANCE.call(context) + INSTANCE.call(context) response.close io.rewind response = HTTP::Client::Response.from_io(io, decompress: false) @@ -28,8 +30,9 @@ describe "Kemal::ExceptionHandler" do io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) - Kemal::ExceptionHandler::INSTANCE.next = Kemal::RouteHandler::INSTANCE - Kemal::ExceptionHandler::INSTANCE.call(context) + context.app = Kemal.application + INSTANCE.next = Kemal::RouteHandler.new + INSTANCE.call(context) response.close io.rewind response = HTTP::Client::Response.from_io(io, decompress: false) @@ -49,8 +52,9 @@ describe "Kemal::ExceptionHandler" do io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) - Kemal::ExceptionHandler::INSTANCE.next = Kemal::RouteHandler::INSTANCE - Kemal::ExceptionHandler::INSTANCE.call(context) + context.app = Kemal.application + INSTANCE.next = Kemal::RouteHandler.new + INSTANCE.call(context) response.close io.rewind response = HTTP::Client::Response.from_io(io, decompress: false) @@ -71,8 +75,9 @@ describe "Kemal::ExceptionHandler" do io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) - Kemal::ExceptionHandler::INSTANCE.next = Kemal::RouteHandler::INSTANCE - Kemal::ExceptionHandler::INSTANCE.call(context) + context.app = Kemal.application + INSTANCE.next = Kemal::RouteHandler.new + INSTANCE.call(context) response.close io.rewind response = HTTP::Client::Response.from_io(io, decompress: false) @@ -93,8 +98,9 @@ describe "Kemal::ExceptionHandler" do io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) - Kemal::ExceptionHandler::INSTANCE.next = Kemal::RouteHandler::INSTANCE - Kemal::ExceptionHandler::INSTANCE.call(context) + context.app = Kemal.application + INSTANCE.next = Kemal::RouteHandler.new + INSTANCE.call(context) response.close io.rewind response = HTTP::Client::Response.from_io(io, decompress: false) diff --git a/spec/handler_spec.cr b/spec/handler_spec.cr index 9b1019fc..e256fa7e 100644 --- a/spec/handler_spec.cr +++ b/spec/handler_spec.cr @@ -70,6 +70,7 @@ end describe "Handler" do it "adds custom handler before before_*" do filter_middleware = Kemal::FilterHandler.new + Kemal.application.add_filter_handler filter_middleware filter_middleware._add_route_filter("GET", "/", :before) do |env| env.response << " is" end @@ -77,6 +78,8 @@ describe "Handler" do filter_middleware._add_route_filter("GET", "/", :before) do |env| env.response << " so" end + Kemal.application.add_filter_handler filter_middleware + add_handler CustomTestHandler.new get "/" do @@ -141,21 +144,21 @@ describe "Handler" do it "adds a handler at given position" do post_handler = PostOnlyHandler.new add_handler post_handler, 1 - Kemal.config.setup - Kemal.config.handlers[1].should eq post_handler + Kemal.application.setup + Kemal.application.handlers[1].should eq post_handler end it "assigns custom handlers" do post_only_handler = PostOnlyHandler.new post_exclude_handler = PostExcludeHandler.new - Kemal.config.handlers = [post_only_handler, post_exclude_handler] - Kemal.config.handlers.should eq [post_only_handler, post_exclude_handler] + Kemal.application.handlers = [post_only_handler, post_exclude_handler] + Kemal.application.handlers.should eq [post_only_handler, post_exclude_handler] end it "is able to use %w in macros" do post_only_handler = PostOnlyHandlerPercentW.new exclude_handler = ExcludeHandlerPercentW.new - Kemal.config.handlers = [post_only_handler, exclude_handler] - Kemal.config.handlers.should eq [post_only_handler, exclude_handler] + Kemal.application.handlers = [post_only_handler, exclude_handler] + Kemal.application.handlers.should eq [post_only_handler, exclude_handler] end end diff --git a/spec/helpers_spec.cr b/spec/helpers_spec.cr index 21c0cdf8..21272eda 100644 --- a/spec/helpers_spec.cr +++ b/spec/helpers_spec.cr @@ -11,21 +11,21 @@ describe "Macros" do describe "#add_handler" do it "adds a custom handler" do add_handler CustomTestHandler.new - Kemal.config.setup - Kemal.config.handlers.size.should eq 7 + Kemal.application.setup + Kemal.application.handlers.size.should eq 7 end end describe "#logging" do it "sets logging status" do logging false - Kemal.config.logging.should eq false + Kemal.config.logging?.should be_false end it "sets a custom logger" do - config = Kemal::Config::INSTANCE + config = Kemal.config logger CustomLogHandler.new - config.logger.should be_a(CustomLogHandler) + Kemal.application.logger.should be_a(CustomLogHandler) end end @@ -119,24 +119,24 @@ describe "Macros" do describe "#gzip" do it "adds HTTP::CompressHandler to handlers" do gzip true - Kemal.config.setup - Kemal.config.handlers[4].should be_a(HTTP::CompressHandler) + Kemal.application.setup + Kemal.application.handlers[4].should be_a(HTTP::CompressHandler) end end describe "#serve_static" do it "should disable static file hosting" do serve_static false - Kemal.config.serve_static.should eq false + Kemal.config.serve_static.should be_false end it "should disble enable gzip and dir_listing" do serve_static({"gzip" => true, "dir_listing" => true}) conf = Kemal.config.serve_static - conf.is_a?(Hash).should eq true + conf.is_a?(Hash).should be_true # Can't use be_a(Hash) because Hash can't be used as generic argument if conf.is_a?(Hash) - conf["gzip"].should eq true - conf["dir_listing"].should eq true + conf["gzip"].should be_true + conf["dir_listing"].should be_true end end end diff --git a/spec/init_handler_spec.cr b/spec/init_handler_spec.cr index 601bbc1d..43fac041 100644 --- a/spec/init_handler_spec.cr +++ b/spec/init_handler_spec.cr @@ -6,8 +6,9 @@ describe "Kemal::InitHandler" do io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) - Kemal::InitHandler::INSTANCE.next = ->(_context : HTTP::Server::Context) {} - Kemal::InitHandler::INSTANCE.call(context) + init_handler = Kemal::InitHandler.new(Kemal::Base.new) + init_handler.next = ->(context : HTTP::Server::Context) {} + init_handler.call(context) context.response.headers["Content-Type"].should eq "text/html" end @@ -16,7 +17,8 @@ describe "Kemal::InitHandler" do io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) - Kemal::InitHandler::INSTANCE.call(context) + init_handler = Kemal::InitHandler.new(Kemal::Base.new) + init_handler.call(context) context.response.headers["X-Powered-By"].should eq "Kemal" end diff --git a/spec/middleware/filters_spec.cr b/spec/middleware/filters_spec.cr index 9bc2564c..65f1d48e 100644 --- a/spec/middleware/filters_spec.cr +++ b/spec/middleware/filters_spec.cr @@ -8,7 +8,7 @@ describe "Kemal::FilterHandler" do filter_middleware = Kemal::FilterHandler.new filter_middleware._add_route_filter("GET", "/greetings", :before) { test_filter.modified = "true" } - kemal = Kemal::RouteHandler::INSTANCE + kemal = Kemal.application.route_handler kemal.add_route "GET", "/greetings" { test_filter.modified } test_filter.modified.should eq("false") @@ -26,7 +26,7 @@ describe "Kemal::FilterHandler" do filter_middleware = Kemal::FilterHandler.new filter_middleware._add_route_filter("GET", "/greetings", :before) { test_filter.modified = test_filter.modified == "true" ? "false" : "true" } - kemal = Kemal::RouteHandler::INSTANCE + kemal = Kemal.application.route_handler kemal.add_route "GET", "/greetings" { test_filter.modified } kemal.add_route "POST", "/greetings" { test_filter.modified } @@ -54,7 +54,7 @@ describe "Kemal::FilterHandler" do filter_middleware._add_route_filter("GET", "/greetings", :before) { test_filter.modified = test_filter.modified == "true" ? "false" : "true" } filter_middleware._add_route_filter("POST", "/greetings", :before) { test_filter.modified = test_filter.modified == "true" ? "false" : "true" } - kemal = Kemal::RouteHandler::INSTANCE + kemal = Kemal.application.route_handler kemal.add_route "GET", "/greetings" { test_filter.modified } kemal.add_route "POST", "/greetings" { test_filter.modified } @@ -80,7 +80,7 @@ describe "Kemal::FilterHandler" do filter_middleware = Kemal::FilterHandler.new filter_middleware._add_route_filter("GET", "/greetings", :after) { test_filter.modified = "true" } - kemal = Kemal::RouteHandler::INSTANCE + kemal = Kemal.application.route_handler kemal.add_route "GET", "/greetings" { test_filter.modified } test_filter.modified.should eq("false") @@ -98,7 +98,7 @@ describe "Kemal::FilterHandler" do filter_middleware = Kemal::FilterHandler.new filter_middleware._add_route_filter("GET", "/greetings", :after) { test_filter.modified = test_filter.modified == "true" ? "false" : "true" } - kemal = Kemal::RouteHandler::INSTANCE + kemal = Kemal.application.route_handler kemal.add_route "GET", "/greetings" { test_filter.modified } kemal.add_route "POST", "/greetings" { test_filter.modified } @@ -126,7 +126,7 @@ describe "Kemal::FilterHandler" do filter_middleware._add_route_filter("GET", "/greetings", :after) { test_filter.modified = test_filter.modified == "true" ? "false" : "true" } filter_middleware._add_route_filter("POST", "/greetings", :after) { test_filter.modified = test_filter.modified == "true" ? "false" : "true" } - kemal = Kemal::RouteHandler::INSTANCE + kemal = Kemal.application.route_handler kemal.add_route "GET", "/greetings" { test_filter.modified } kemal.add_route "POST", "/greetings" { test_filter.modified } @@ -157,7 +157,7 @@ describe "Kemal::FilterHandler" do filter_middleware._add_route_filter("ALL", "/greetings", :before) { test_filter_second.modified = test_filter_second.modified == "true" ? "false" : "true" } filter_middleware._add_route_filter("ALL", "/greetings", :before) { test_filter_third.modified = test_filter_third.modified == "true" ? "false" : "true" } - kemal = Kemal::RouteHandler::INSTANCE + kemal = Kemal.application.route_handler kemal.add_route "GET", "/greetings" { test_filter.modified } kemal.add_route "POST", "/greetings" { test_filter_second.modified } kemal.add_route "PUT", "/greetings" { test_filter_third.modified } diff --git a/spec/param_parser_spec.cr b/spec/param_parser_spec.cr index d63a2298..f85c94e4 100644 --- a/spec/param_parser_spec.cr +++ b/spec/param_parser_spec.cr @@ -22,7 +22,7 @@ describe "ParamParser" do end it "parses url params" do - kemal = Kemal::RouteHandler::INSTANCE + kemal = Kemal.application.route_handler kemal.add_route "POST", "/hello/:hasan" do |env| "hello #{env.params.url["hasan"]}" end @@ -34,7 +34,7 @@ describe "ParamParser" do end it "decodes url params" do - kemal = Kemal::RouteHandler::INSTANCE + kemal = Kemal.application.route_handler kemal.add_route "POST", "/hello/:email/:money/:spanish" do |env| email = env.params.url["email"] money = env.params.url["money"] diff --git a/spec/run_spec.cr b/spec/run_spec.cr index 25534552..915fab8c 100644 --- a/spec/run_spec.cr +++ b/spec/run_spec.cr @@ -17,32 +17,19 @@ end describe "Run" do it "runs a code block after starting" do - run(<<-CR).should eq "started\nstopped\n" - Kemal.config.env = "test" - Kemal.run do - puts "started" - Kemal.stop - puts "stopped" - end - CR + Kemal.config.env = "test" + make_me_true = false + Kemal.run do + make_me_true = true + Kemal.stop + end + make_me_true.should be_true end it "runs without a block being specified" do - run(<<-CR).should eq "[test] Kemal is ready to lead at http://0.0.0.0:3000\ntrue\n" - Kemal.config.env = "test" - Kemal.run - puts Kemal.config.running - CR - end - - it "allows custom HTTP::Server bind" do - run(<<-CR).should eq "[test] Kemal is ready to lead at http://127.0.0.1:3000, http://0.0.0.0:3001\n" - Kemal.config.env = "test" - Kemal.run do |config| - server = config.server.not_nil! - server.bind_tcp "127.0.0.1", 3000, reuse_port: true - server.bind_tcp "0.0.0.0", 3001, reuse_port: true - end - CR + Kemal.config.env = "test" + Kemal.run + Kemal.application.running?.should be_true + Kemal.stop end end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 0bc127ad..2e62d70f 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -1,5 +1,7 @@ require "spec" -require "../src/*" +require "../src/**" +require "../src/kemal/base" +require "../src/kemal/dsl" include Kemal @@ -33,6 +35,7 @@ def create_request_and_return_io_and_context(handler, request) io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) + context.app = Kemal.application handler.call(context) response.close io.rewind @@ -43,6 +46,7 @@ def create_ws_request_and_return_io_and_context(handler, request) io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) + context.app = Kemal.application begin handler.call context rescue IO::Error @@ -64,10 +68,10 @@ def call_request_on_app(request) end def build_main_handler - Kemal.config.setup - main_handler = Kemal.config.handlers.first + Kemal.application.setup + main_handler = Kemal.application.handlers.first current_handler = main_handler - Kemal.config.handlers.each do |handler| + Kemal.application.handlers.each_with_index do |handler, index| current_handler.next = handler current_handler = handler end @@ -81,8 +85,5 @@ Spec.before_each do end Spec.after_each do - Kemal.config.clear - Kemal::RouteHandler::INSTANCE.routes = Radix::Tree(Route).new - Kemal::RouteHandler::INSTANCE.cached_routes = Hash(String, Radix::Result(Route)).new - Kemal::WebSocketHandler::INSTANCE.routes = Radix::Tree(WebSocket).new + Kemal.application.clear end diff --git a/spec/websocket_handler_spec.cr b/spec/websocket_handler_spec.cr index bc02d3c2..ab3d58e8 100644 --- a/spec/websocket_handler_spec.cr +++ b/spec/websocket_handler_spec.cr @@ -2,8 +2,8 @@ require "./spec_helper" describe "Kemal::WebSocketHandler" do it "doesn't match on wrong route" do - handler = Kemal::WebSocketHandler::INSTANCE - handler.next = Kemal::RouteHandler::INSTANCE + handler = Kemal::WebSocketHandler.new + handler.next = Kemal::RouteHandler.new ws "/" { } headers = HTTP::Headers{ "Upgrade" => "websocket", @@ -14,6 +14,7 @@ describe "Kemal::WebSocketHandler" do io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) + context.app = Kemal.application expect_raises(Kemal::Exceptions::RouteNotFound) do handler.call context @@ -21,9 +22,9 @@ describe "Kemal::WebSocketHandler" do end it "matches on given route" do - handler = Kemal::WebSocketHandler::INSTANCE - ws "/" { |socket| socket.send("Match") } - ws "/no_match" { |socket| socket.send "No Match" } + handler = Kemal::WebSocketHandler.new + ws "/" { |socket, context| socket.send("Match") } + ws "/no_match" { |socket, context| socket.send "No Match" } headers = HTTP::Headers{ "Upgrade" => "websocket", "Connection" => "Upgrade", @@ -37,8 +38,8 @@ describe "Kemal::WebSocketHandler" do end it "fetches named url parameters" do - handler = Kemal::WebSocketHandler::INSTANCE - ws "/:id" { |_, c| c.ws_route_lookup.params["id"] } + handler = Kemal::WebSocketHandler.new + ws "/:id" { |s, c| c.params.url["id"] } headers = HTTP::Headers{ "Upgrade" => "websocket", "Connection" => "Upgrade", @@ -51,14 +52,15 @@ describe "Kemal::WebSocketHandler" do end it "matches correct verb" do - handler = Kemal::WebSocketHandler::INSTANCE - handler.next = Kemal::RouteHandler::INSTANCE + handler = Kemal::WebSocketHandler.new + handler.next = Kemal::RouteHandler.new ws "/" { } get "/" { "get" } request = HTTP::Request.new("GET", "/") io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) + context.app = Kemal.application handler.call(context) response.close io.rewind diff --git a/src/kemal.cr b/src/kemal.cr index a57330e8..782826c9 100644 --- a/src/kemal.cr +++ b/src/kemal.cr @@ -1,98 +1,2 @@ -require "http" -require "json" -require "uri" -require "./kemal/*" -require "./kemal/ext/*" -require "./kemal/helpers/*" - -module Kemal - # Overload of `self.run` with the default startup logging. - def self.run(port : Int32?, args = ARGV) - self.run(port, args) { } - end - - # Overload of `self.run` without port. - def self.run(args = ARGV) - self.run(nil, args: args) - end - - # Overload of `self.run` to allow just a block. - def self.run(args = ARGV, &block) - self.run(nil, args: args, &block) - end - - # The command to run a `Kemal` application. - # - # If *port* is not given Kemal will use `Kemal::Config#port` - # - # To use custom command line arguments, set args to nil - # - def self.run(port : Int32? = nil, args = ARGV, &block) - Kemal::CLI.new args - config = Kemal.config - config.setup - config.port = port if port - - # Test environment doesn't need to have signal trap and logging. - if config.env != "test" - setup_404 - setup_trap_signal - end - - server = config.server ||= HTTP::Server.new(config.handlers) - - config.running = true - - yield config - - # Abort if block called `Kemal.stop` - return unless config.running - - unless server.each_address { |_| break true } - {% if flag?(:without_openssl) %} - server.bind_tcp(config.host_binding, config.port) - {% else %} - if ssl = config.ssl - server.bind_tls(config.host_binding, config.port, ssl) - else - server.bind_tcp(config.host_binding, config.port) - end - {% end %} - end - - display_startup_message(config, server) - - server.listen unless config.env == "test" - end - - def self.display_startup_message(config, server) - addresses = server.addresses.map { |address| "#{config.scheme}://#{address}" }.join ", " - log "[#{config.env}] Kemal is ready to lead at #{addresses}" - end - - def self.stop - raise "Kemal is already stopped." if !config.running - if server = config.server - server.close unless server.closed? - config.running = false - else - raise "Kemal.config.server is not set. Please use Kemal.run to set the server." - end - end - - private def self.setup_404 - unless Kemal.config.error_handlers.has_key?(404) - error 404 do - render_404 - end - end - end - - private def self.setup_trap_signal - Signal::INT.trap do - log "Kemal is going to take a rest!" if Kemal.config.shutdown_message - Kemal.stop - exit - end - end -end +require "./kemal/base" +require "./kemal/dsl" \ No newline at end of file diff --git a/src/kemal/base.cr b/src/kemal/base.cr new file mode 100644 index 00000000..23a2e45b --- /dev/null +++ b/src/kemal/base.cr @@ -0,0 +1,227 @@ +# Kemal Base +# The DSL currently consists of +# - get post put patch delete options +# - WebSocket(ws) +# - before_* +# - error +class Kemal::Base + HTTP_METHODS = %w(get post put patch delete options) + FILTER_METHODS = %w(get post put patch delete options all) + + getter route_handler = Kemal::RouteHandler.new + getter filter_handler = Kemal::FilterHandler.new + getter websocket_handler = Kemal::WebSocketHandler.new + + getter handlers = [] of HTTP::Handler + getter custom_handlers = [] of Tuple(Nil | Int32, HTTP::Handler) + getter filter_handlers = [] of HTTP::Handler + getter error_handlers = {} of Int32 => HTTP::Server::Context, Exception -> String + @handler_position = 0 + + getter config : Config + + property! logger : Kemal::BaseLogHandler + property! server : HTTP::Server + property? running = false + + def initialize(@config = Config.new) + @logger = if @config.logging? + Kemal::LogHandler.new + else + Kemal::NullLogHandler.new + end + add_filter_handler(filter_handler) + end + + {% for method in HTTP_METHODS %} + def {{method.id}}(path, &block : HTTP::Server::Context -> _) + raise Kemal::Exceptions::InvalidPathStartException.new({{method}}, path) unless Kemal::Utils.path_starts_with_slash?(path) + route_handler.add_route({{method}}.upcase, path, &block) + end + {% end %} + + def ws(path, &block : HTTP::WebSocket, HTTP::Server::Context -> Void) + raise Kemal::Exceptions::InvalidPathStartException.new("ws", path) unless Kemal::Utils.path_starts_with_slash?(path) + websocket_handler.add_route path, &block + end + + def error(status_code, &block : HTTP::Server::Context, Exception -> _) + add_error_handler status_code, &block + end + + # All the helper methods available are: + # - before_all, before_get, before_post, before_put, before_patch, before_delete, before_options + # - after_all, after_get, after_post, after_put, after_patch, after_delete, after_options + {% for type in ["before", "after"] %} + {% for method in FILTER_METHODS %} + def {{type.id}}_{{method.id}}(path = "*", &block : HTTP::Server::Context -> _) + filter_handler.{{type.id}}({{method}}.upcase, path, &block) + end + {% end %} + {% end %} + + def clear + @router_included = false + @handler_position = 0 + @default_handlers_setup = false + + handlers.clear + custom_handlers.clear + filter_handlers.clear + error_handlers.clear + + route_handler.clear + websocket_handler.clear + end + + def handlers=(handlers : Array(HTTP::Handler)) + clear + @handlers.replace(handlers) + end + + def add_handler(handler : HTTP::Handler) + @custom_handlers << {nil, handler} + end + + def add_handler(handler : HTTP::Handler, position : Int32) + @custom_handlers << {position, handler} + end + + def add_filter_handler(handler : HTTP::Handler) + @filter_handlers << handler + end + + def add_error_handler(status_code, &handler : HTTP::Server::Context, Exception -> _) + @error_handlers[status_code] = ->(context : HTTP::Server::Context, error : Exception) { handler.call(context, error).to_s } + end + + def setup + unless @default_handlers_setup && @router_included + setup_init_handler + setup_log_handler + setup_error_handler + setup_static_file_handler + setup_custom_handlers + setup_filter_handlers + @default_handlers_setup = true + @router_included = true + handlers.insert(handlers.size, websocket_handler) + handlers.insert(handlers.size, route_handler) + end + end + + private def setup_init_handler + @handlers.insert(@handler_position, Kemal::InitHandler.new(self)) + @handler_position += 1 + end + + private def setup_log_handler + @handlers.insert(@handler_position, logger) + @handler_position += 1 + end + + private def setup_error_handler + if @config.always_rescue? + @error_handler ||= Kemal::ExceptionHandler.new + @handlers.insert(@handler_position, @error_handler.not_nil!) + @handler_position += 1 + end + end + + private def setup_static_file_handler + if @config.serve_static.is_a?(Hash) + @handlers.insert(@handler_position, Kemal::StaticFileHandler.new(@config.public_folder)) + @handler_position += 1 + end + end + + private def setup_custom_handlers + @custom_handlers.each do |ch| + position = ch[0] + if !position + @handlers.insert(@handler_position, ch[1]) + @handler_position += 1 + else + @handlers.insert(position, ch[1]) + @handler_position += 1 + end + end + end + + private def setup_filter_handlers + @filter_handlers.each do |h| + @handlers.insert(@handler_position, h) + end + end + + # Overload of self.run with the default startup logging + def run(port = nil) + run port do + log "[#{config.env}] Kemal is ready to lead at #{config.scheme}://#{config.host_binding}:#{config.port}" + end + end + + # Overload of self.run to allow just a block + def run(&block) + run nil, &block + end + + # The command to run a `Kemal` application. + # The port can be given to `#run` but is optional. + # If not given Kemal will use `Kemal::Config#port` + def run(port = nil, &block) + @config.port = port if port + + setup + + @server = server = HTTP::Server.new(@config.host_binding, @config.port, @handlers) + {% if !flag?(:without_openssl) %} + server.tls = config.ssl + {% end %} + + unless error_handlers.has_key?(404) + error 404 do |env| + render_404 + end + end + + # Test environment doesn't need to have signal trap, built-in images, and logging. + unless config.env == "test" + Signal::INT.trap do + log "Kemal is going to take a rest!" if config.shutdown_message? + Kemal.stop if running? + exit + end + + # This route serves the built-in images for not_found and exceptions. + get "/__kemal__/:image" do |env| + image = env.params.url["image"] + file_path = File.expand_path("lib/kemal/images/#{image}", Dir.current) + if File.exists? file_path + send_file env, file_path + else + halt env, 404 + end + end + end + + @running = true + + yield self + + server.listen if @config.env != "test" + end + + def stop + if @running + if server = @server + server.close + @running = false + else + raise "server is not set. Please use run to set the server." + end + else + raise "Kemal is already stopped." + end + end +end diff --git a/src/kemal/base_log_handler.cr b/src/kemal/base_log_handler.cr index 37ee980b..399ef154 100644 --- a/src/kemal/base_log_handler.cr +++ b/src/kemal/base_log_handler.cr @@ -1,3 +1,5 @@ +require "http" + module Kemal # All loggers must inherit from `Kemal::BaseLogHandler`. abstract class BaseLogHandler diff --git a/src/kemal/cli.cr b/src/kemal/cli.cr index 656a4e69..1304ddff 100644 --- a/src/kemal/cli.cr +++ b/src/kemal/cli.cr @@ -3,11 +3,10 @@ require "option_parser" module Kemal # Handles all the initialization from the command line. class CLI - def initialize(args) + def initialize(args, @config : Config = Kemal.config) @ssl_enabled = false @key_file = "" @cert_file = "" - @config = Kemal.config read_env if args parse args diff --git a/src/kemal/config.cr b/src/kemal/config.cr index 04bbdd7e..99b1f9d5 100644 --- a/src/kemal/config.cr +++ b/src/kemal/config.cr @@ -8,161 +8,50 @@ module Kemal # Kemal.config # ``` class Config - INSTANCE = Config.new - HANDLERS = [] of HTTP::Handler - CUSTOM_HANDLERS = [] of Tuple(Nil | Int32, HTTP::Handler) - FILTER_HANDLERS = [] of HTTP::Handler - ERROR_HANDLERS = {} of Int32 => HTTP::Server::Context, Exception -> String - + property host_binding = "0.0.0.0" + property port = 3000 {% if flag?(:without_openssl) %} - @ssl : Bool? + property ssl : Bool? {% else %} - @ssl : OpenSSL::SSL::Context::Server? + property ssl : OpenSSL::SSL::Context::Server? {% end %} - property host_binding, ssl, port, env, public_folder, logging, running - property always_rescue, server : HTTP::Server?, extra_options, shutdown_message property serve_static : (Bool | Hash(String, Bool)) property static_headers : (HTTP::Server::Response, String, File::Info -> Void)? property powered_by_header : Bool = true - - def initialize - @host_binding = "0.0.0.0" - @port = 3000 - @env = "development" - @serve_static = {"dir_listing" => false, "gzip" => true} - @public_folder = "./public" - @logging = true - @logger = nil - @error_handler = nil - @always_rescue = true - @router_included = false - @default_handlers_setup = false - @running = false - @shutdown_message = true - @handler_position = 0 - end - - def logger - @logger.not_nil! - end - - def logger=(logger : Kemal::BaseLogHandler) - @logger = logger + property env = "development" + property serve_static : Hash(String, Bool) | Bool = {"dir_listing" => false, "gzip" => true} + property public_folder = "./public" + property? logging = true + property? always_rescue = true + property? shutdown_message = true + property extra_options : (OptionParser ->)? + + # Creates a config with default values. + def initialize( + @host_binding = "0.0.0.0", + @port = 3000, + @ssl = nil, + @env = "development", + @serve_static = {"dir_listing" => false, "gzip" => true}, + @public_folder = "./public", + @logging = true, + @always_rescue = true, + @shutdown_message = true, + @extra_options = nil, + static_headers = nil) end def scheme ssl ? "https" : "http" end - def clear - @powered_by_header = true - @router_included = false - @handler_position = 0 - @default_handlers_setup = false - HANDLERS.clear - CUSTOM_HANDLERS.clear - FILTER_HANDLERS.clear - ERROR_HANDLERS.clear - end - - def handlers - HANDLERS - end - - def handlers=(handlers : Array(HTTP::Handler)) - clear - HANDLERS.replace(handlers) - end - - def add_handler(handler : HTTP::Handler) - CUSTOM_HANDLERS << {nil, handler} - end - - def add_handler(handler : HTTP::Handler, position : Int32) - CUSTOM_HANDLERS << {position, handler} - end - - def add_filter_handler(handler : HTTP::Handler) - FILTER_HANDLERS << handler - end - - def error_handlers - ERROR_HANDLERS - end - - def add_error_handler(status_code : Int32, &handler : HTTP::Server::Context, Exception -> _) - ERROR_HANDLERS[status_code] = ->(context : HTTP::Server::Context, error : Exception) { handler.call(context, error).to_s } - end - def extra_options(&@extra_options : OptionParser ->) end - def setup - unless @default_handlers_setup && @router_included - setup_init_handler - setup_log_handler - setup_error_handler - setup_static_file_handler - setup_custom_handlers - setup_filter_handlers - @default_handlers_setup = true - @router_included = true - HANDLERS.insert(HANDLERS.size, Kemal::WebSocketHandler::INSTANCE) - HANDLERS.insert(HANDLERS.size, Kemal::RouteHandler::INSTANCE) - end - end - - private def setup_init_handler - HANDLERS.insert(@handler_position, Kemal::InitHandler::INSTANCE) - @handler_position += 1 + def serve_static?(key) + config = @serve_static + config.try(&.[key]?) || config == true end - - private def setup_log_handler - @logger ||= if @logging - Kemal::LogHandler.new - else - Kemal::NullLogHandler.new - end - HANDLERS.insert(@handler_position, @logger.not_nil!) - @handler_position += 1 - end - - private def setup_error_handler - if @always_rescue - @error_handler ||= Kemal::ExceptionHandler.new - HANDLERS.insert(@handler_position, @error_handler.not_nil!) - @handler_position += 1 - end - end - - private def setup_static_file_handler - if @serve_static.is_a?(Hash) - HANDLERS.insert(@handler_position, Kemal::StaticFileHandler.new(@public_folder)) - @handler_position += 1 - end - end - - private def setup_custom_handlers - CUSTOM_HANDLERS.each do |ch0, ch1| - position = ch0 - HANDLERS.insert (position || @handler_position), ch1 - @handler_position += 1 - end - end - - private def setup_filter_handlers - FILTER_HANDLERS.each do |h| - HANDLERS.insert(@handler_position, h) - end - end - end - - def self.config - yield Config::INSTANCE - end - - def self.config - Config::INSTANCE end end diff --git a/src/kemal/dsl.cr b/src/kemal/dsl.cr index 15b37424..ba1c021e 100644 --- a/src/kemal/dsl.cr +++ b/src/kemal/dsl.cr @@ -6,32 +6,28 @@ # - WebSocket(ws) # - before_* # - error -HTTP_METHODS = %w(get post put patch delete options) -FILTER_METHODS = %w(get post put patch delete options all) -{% for method in HTTP_METHODS %} - def {{method.id}}(path : String, &block : HTTP::Server::Context -> _) - raise Kemal::Exceptions::InvalidPathStartException.new({{method}}, path) unless Kemal::Utils.path_starts_with_slash?(path) - Kemal::RouteHandler::INSTANCE.add_route({{method}}.upcase, path, &block) +{% for method in Kemal::Base::HTTP_METHODS %} + def {{method.id}}(path, &block : HTTP::Server::Context -> _) + Kemal.application.{{method.id}}(path, &block) end {% end %} -def ws(path : String, &block : HTTP::WebSocket, HTTP::Server::Context -> Void) - raise Kemal::Exceptions::InvalidPathStartException.new("ws", path) unless Kemal::Utils.path_starts_with_slash?(path) - Kemal::WebSocketHandler::INSTANCE.add_route path, &block +def ws(path, &block : HTTP::WebSocket, HTTP::Server::Context -> Void) + Kemal.application.ws(path, &block) end -def error(status_code : Int32, &block : HTTP::Server::Context, Exception -> _) - Kemal.config.add_error_handler status_code, &block +def error(status_code, &block : HTTP::Server::Context, Exception -> _) + Kemal.application.add_error_handler status_code, &block end # All the helper methods available are: # - before_all, before_get, before_post, before_put, before_patch, before_delete, before_options # - after_all, after_get, after_post, after_put, after_patch, after_delete, after_options {% for type in ["before", "after"] %} - {% for method in FILTER_METHODS %} - def {{type.id}}_{{method.id}}(path : String = "*", &block : HTTP::Server::Context -> _) - Kemal::FilterHandler::INSTANCE.{{type.id}}({{method}}.upcase, path, &block) + {% for method in Kemal::Base::FILTER_METHODS %} + def {{type.id}}_{{method.id}}(path = "*", &block : HTTP::Server::Context -> _) + Kemal.application.{{type.id}}_{{method.id}}(path, &block) end {% end %} {% end %} diff --git a/src/kemal/exception_handler.cr b/src/kemal/exception_handler.cr index a0b9a690..e1d7168d 100644 --- a/src/kemal/exception_handler.cr +++ b/src/kemal/exception_handler.cr @@ -2,25 +2,26 @@ module Kemal # Handles all the exceptions, including 404, custom errors and 500. class ExceptionHandler include HTTP::Handler - INSTANCE = new def call(context : HTTP::Server::Context) - call_next(context) - rescue ex : Kemal::Exceptions::RouteNotFound - call_exception_with_status_code(context, ex, 404) - rescue ex : Kemal::Exceptions::CustomException - call_exception_with_status_code(context, ex, context.response.status_code) - rescue ex : Exception - log("Exception: #{ex.inspect_with_backtrace}") - return call_exception_with_status_code(context, ex, 500) if Kemal.config.error_handlers.has_key?(500) - verbosity = Kemal.config.env == "production" ? false : true - return render_500(context, ex, verbosity) + begin + call_next(context) + rescue ex : Kemal::Exceptions::RouteNotFound + call_exception_with_status_code(context, ex, 404) + rescue ex : Kemal::Exceptions::CustomException + call_exception_with_status_code(context, ex, context.response.status_code) + rescue ex : Exception + log("Exception: #{ex.inspect_with_backtrace}") + return call_exception_with_status_code(context, ex, 500) if context.app.error_handlers.has_key?(500) + verbosity = context.app.config.env == "production" ? false : true + return render_500(context, ex.inspect_with_backtrace, verbosity) + end end - private def call_exception_with_status_code(context : HTTP::Server::Context, exception : Exception, status_code : Int32) - if !Kemal.config.error_handlers.empty? && Kemal.config.error_handlers.has_key?(status_code) + private def call_exception_with_status_code(context, exception, status_code) + if context.app.error_handlers.has_key?(status_code) context.response.content_type = "text/html" unless context.response.headers.has_key?("Content-Type") - context.response.print Kemal.config.error_handlers[status_code].call(context, exception) + context.response.print context.app.error_handlers[status_code].call(context, exception) context.response.status_code = status_code context end diff --git a/src/kemal/ext/context.cr b/src/kemal/ext/context.cr index f9a12caf..d4d02b34 100644 --- a/src/kemal/ext/context.cr +++ b/src/kemal/ext/context.cr @@ -8,6 +8,8 @@ class HTTP::Server # :nodoc: STORE_MAPPINGS = [Nil, String, Int32, Int64, Float64, Bool] + property! app : Kemal::Base + macro finished alias StoreTypes = Union({{ *STORE_MAPPINGS }}) @store = {} of String => StoreTypes @@ -31,7 +33,7 @@ class HTTP::Server end def route_lookup - Kemal::RouteHandler::INSTANCE.lookup_route(@request.method.as(String), @request.path) + app.route_handler.lookup_route(@request.override_method.as(String), @request.path) end def route_found? @@ -39,7 +41,7 @@ class HTTP::Server end def ws_route_lookup - Kemal::WebSocketHandler::INSTANCE.lookup_ws_route(@request.path) + app.websocket_handler.lookup_ws_route(@request.path) end def ws_route_found? diff --git a/src/kemal/filter_handler.cr b/src/kemal/filter_handler.cr index 6d28680a..a1357448 100644 --- a/src/kemal/filter_handler.cr +++ b/src/kemal/filter_handler.cr @@ -2,20 +2,18 @@ module Kemal # :nodoc: class FilterHandler include HTTP::Handler - INSTANCE = new # This middleware is lazily instantiated and added to the handlers as soon as a call to `after_X` or `before_X` is made. def initialize @tree = Radix::Tree(Array(FilterBlock)).new - Kemal.config.add_filter_handler(self) end # The call order of the filters is `before_all -> before_x -> X -> after_x -> after_all`. def call(context : HTTP::Server::Context) return call_next(context) unless context.route_found? call_block_for_path_type("ALL", context.request.path, :before, context) - call_block_for_path_type(context.request.method, context.request.path, :before, context) - if Kemal.config.error_handlers.has_key?(context.response.status_code) + call_block_for_path_type(context.request.override_method, context.request.path, :before, context) + if context.app.error_handlers.has_key?(context.response.status_code) raise Kemal::Exceptions::CustomException.new(context) end call_next(context) diff --git a/src/kemal/helpers/helpers.cr b/src/kemal/helpers/helpers.cr index f487c997..e6d1d13f 100644 --- a/src/kemal/helpers/helpers.cr +++ b/src/kemal/helpers/helpers.cr @@ -7,13 +7,13 @@ # - `Kemal::ExceptionHandler` # - `Kemal::StaticFileHandler` # - Here goes custom handlers -# - `Kemal::RouteHandler` +# - Kemal::RouteHandler def add_handler(handler : HTTP::Handler) - Kemal.config.add_handler handler + Kemal.application.add_handler handler end def add_handler(handler : HTTP::Handler, position : Int32) - Kemal.config.add_handler handler, position + Kemal.application.add_handler handler, position end # Sets public folder from which the static assets will be served. @@ -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.config.logger.write "#{message}\n" + Kemal.application.logger.write "#{message}\n" end # Enables / Disables logging. @@ -64,8 +64,8 @@ end # logger MyCustomLogger.new # ``` def logger(logger : Kemal::BaseLogHandler) - Kemal.config.logger = logger - Kemal.config.add_handler logger + Kemal.application.logger = logger + Kemal.application.add_handler logger end # Enables / Disables static file serving. diff --git a/src/kemal/init_handler.cr b/src/kemal/init_handler.cr index 881325b6..81752236 100644 --- a/src/kemal/init_handler.cr +++ b/src/kemal/init_handler.cr @@ -4,11 +4,15 @@ module Kemal class InitHandler include HTTP::Handler - INSTANCE = new + getter app : Kemal::Base + + def initialize(@app) + end def call(context : HTTP::Server::Context) context.response.headers.add "X-Powered-By", "Kemal" if Kemal.config.powered_by_header context.response.content_type = "text/html" unless context.response.headers.has_key?("Content-Type") + context.app = app call_next context end end diff --git a/src/kemal/route_handler.cr b/src/kemal/route_handler.cr index 528d7736..fd2f672b 100644 --- a/src/kemal/route_handler.cr +++ b/src/kemal/route_handler.cr @@ -47,7 +47,7 @@ module Kemal raise Kemal::Exceptions::RouteNotFound.new(context) unless context.route_found? content = context.route.handler.call(context) - if !Kemal.config.error_handlers.empty? && Kemal.config.error_handlers.has_key?(context.response.status_code) + if context.app.error_handlers.has_key?(context.response.status_code) raise Kemal::Exceptions::CustomException.new(context) end @@ -63,5 +63,9 @@ module Kemal node = radix_path method, path @routes.add node, route end + + def clear + @routes = Radix::Tree(Route).new + end end end diff --git a/src/kemal/websocket_handler.cr b/src/kemal/websocket_handler.cr index addbecfa..da81b2fa 100644 --- a/src/kemal/websocket_handler.cr +++ b/src/kemal/websocket_handler.cr @@ -2,7 +2,6 @@ module Kemal class WebSocketHandler include HTTP::Handler - INSTANCE = new property routes def initialize @@ -39,5 +38,9 @@ module Kemal context.request.headers.includes_word?("Connection", "Upgrade") end + + def clear + @routes = Radix::Tree(WebSocket).new + end end end 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 02/18] 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 | 177 +++++++++--------- src/kemal/helpers/templates.cr | 54 ++++++ 11 files changed, 353 insertions(+), 173 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 1aac161b..54293b45 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 782826c9..11f47e78 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 23a2e45b..cf118ae5 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 99b1f9d5..b129c65d 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 ba1c021e..0d94283e 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 e6d1d13f..457b3038 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 00000000..094e5d37 --- /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 00000000..90c6e9dd --- /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 00000000..c30d0e98 --- /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 4b5e3090..1cfffde2 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 -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 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 + # 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 + # 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) }} + # 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 diff --git a/src/kemal/helpers/templates.cr b/src/kemal/helpers/templates.cr index b343fc8a..924941fa 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 From f5c80c7b67fa990e8b8651649b7c8c25f53b291b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Mon, 17 Jul 2017 18:14:26 +0200 Subject: [PATCH 03/18] Extract default app behavior from Kemal::Base to Kemal::Application --- spec/config_spec.cr | 11 ++++++-- src/kemal/application.cr | 37 ++++++++++++++++++++++++++ src/kemal/base.cr | 56 +++++++++++++++------------------------- src/kemal/config.cr | 15 +++++++++++ src/kemal/dsl.cr | 1 + 5 files changed, 83 insertions(+), 37 deletions(-) create mode 100644 src/kemal/application.cr diff --git a/spec/config_spec.cr b/spec/config_spec.cr index 48fc31d0..9ead52f6 100644 --- a/spec/config_spec.cr +++ b/spec/config_spec.cr @@ -27,11 +27,18 @@ describe "Config" do config.host_binding.should eq "127.0.0.1" end - it "adds a custom handler" do + it "adds a custom handler to Base" do application = Kemal::Base.new application.add_handler CustomTestHandler.new application.setup - application.handlers.size.should eq(8) + application.handlers.size.should eq 6 + end + + it "adds a custom handler to Application" do + application = Kemal::Application.new + application.add_handler CustomTestHandler.new + application.setup + application.handlers.size.should eq 9 end it "toggles the shutdown message" do diff --git a/src/kemal/application.cr b/src/kemal/application.cr new file mode 100644 index 00000000..e7fa19c7 --- /dev/null +++ b/src/kemal/application.cr @@ -0,0 +1,37 @@ +class Kemal::Application < Kemal::Base + def initialize(config = Config.default) + super config + add_filter_handler(filter_handler) + end + + # Overload of self.run with the default startup logging + def run(port = nil) + run port do + log "[#{config.env}] Kemal is ready to lead at #{config.scheme}://#{config.host_binding}:#{port || config.port}" + end + end + + private def prepare_for_server_start + super + + unless error_handlers.has_key?(404) + error 404 do |env| + render_404 + end + end + + # Test environment doesn't need to have signal trap, built-in images, and logging. + unless @config.env == "test" + # This route serves the built-in images for not_found and exceptions. + get "/__kemal__/:image" do |env| + image = env.params.url["image"] + file_path = File.expand_path("lib/kemal/images/#{image}", Dir.current) + if File.exists? file_path + send_file env, file_path + else + halt env, 404 + end + end + end + end +end diff --git a/src/kemal/base.cr b/src/kemal/base.cr index cf118ae5..09af652e 100644 --- a/src/kemal/base.cr +++ b/src/kemal/base.cr @@ -30,7 +30,7 @@ class Kemal::Base property! server : HTTP::Server property? running = false - def initialize(@config = Config.new) + def initialize(@config = Config.base) @logger = if @config.logging? Kemal::LogHandler.new else @@ -161,61 +161,47 @@ class Kemal::Base end # Overload of self.run with the default startup logging - def run(port = nil) + def run(port : Int32? = nil) run port do log "[#{config.env}] Kemal is ready to lead at #{config.scheme}://#{config.host_binding}:#{config.port}" end end - # Overload of self.run to allow just a block - def run(&block) - run nil, &block - end - # The command to run a `Kemal` application. # The port can be given to `#run` but is optional. # If not given Kemal will use `Kemal::Config#port` - def run(port = nil, &block) - @config.port = port if port - + def run(port : Int32? = nil) setup - @server = server = HTTP::Server.new(@config.host_binding, @config.port, @handlers) - {% if !flag?(:without_openssl) %} - server.tls = config.ssl - {% end %} + prepare_for_server_start - unless error_handlers.has_key?(404) - error 404 do |env| - render_404 - end + start_server(port) do + yield self end + end - # Test environment doesn't need to have signal trap, built-in images, and logging. - unless config.env == "test" + private def prepare_for_server_start + unless @config.env == "test" Signal::INT.trap do - log "Kemal is going to take a rest!" if config.shutdown_message? - Kemal.stop if running? + log "Kemal is going to take a rest!" if @config.shutdown_message? + stop if running? exit end - - # This route serves the built-in images for not_found and exceptions. - get "/__kemal__/:image" do |env| - image = env.params.url["image"] - file_path = File.expand_path("lib/kemal/images/#{image}", Dir.current) - if File.exists? file_path - send_file env, file_path - else - halt env, 404 - end - end end + end + + private def start_server(port) + @server = server = HTTP::Server.new(@config.host_binding, port || @config.port, @handlers) + {% if !flag?(:without_openssl) %} + server.tls = config.ssl + {% end %} + server.bind @running = true - yield self + yield - server.listen if @config.env != "test" + server.listen unless @config.env == "test" end def stop diff --git a/src/kemal/config.cr b/src/kemal/config.cr index b129c65d..e4db16af 100644 --- a/src/kemal/config.cr +++ b/src/kemal/config.cr @@ -57,5 +57,20 @@ module Kemal def extra_options(&@extra_options : OptionParser ->) end + + # Create a config with default values + def self.default + new + end + + # Creates a config with basic value (disabled logging, disabled serve_static, disabled shutdown_message) + def self.base + new.tap do |config| + config.logging = false + config.serve_static = false + config.shutdown_message = false + config.always_rescue = false + end + end end end diff --git a/src/kemal/dsl.cr b/src/kemal/dsl.cr index 0d94283e..4c8575a2 100644 --- a/src/kemal/dsl.cr +++ b/src/kemal/dsl.cr @@ -6,6 +6,7 @@ # - WebSocket(ws) # - before_* # - error +require "../kemal" require "./dsl/*" {% for method in Kemal::Base::HTTP_METHODS %} From 29b18c927c654fbdd5accfe96b397f20d4156349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Mon, 17 Jul 2017 22:11:02 +0200 Subject: [PATCH 04/18] Extract Base::Builder and Base::DSL from Kemal::Base --- spec/config_spec.cr | 2 +- spec/helpers_spec.cr | 2 +- src/kemal/application.cr | 3 +- src/kemal/base.cr | 147 ++++---------------------------------- src/kemal/base/builder.cr | 108 ++++++++++++++++++++++++++++ src/kemal/base/dsl.cr | 91 +++++++++++++++++++++++ src/kemal/config.cr | 13 ++-- 7 files changed, 223 insertions(+), 143 deletions(-) create mode 100644 src/kemal/base/builder.cr create mode 100644 src/kemal/base/dsl.cr diff --git a/spec/config_spec.cr b/spec/config_spec.cr index 9ead52f6..f6db81fe 100644 --- a/spec/config_spec.cr +++ b/spec/config_spec.cr @@ -38,7 +38,7 @@ describe "Config" do application = Kemal::Application.new application.add_handler CustomTestHandler.new application.setup - application.handlers.size.should eq 9 + application.handlers.size.should eq 8 end it "toggles the shutdown message" do diff --git a/spec/helpers_spec.cr b/spec/helpers_spec.cr index 21272eda..a7290d61 100644 --- a/spec/helpers_spec.cr +++ b/spec/helpers_spec.cr @@ -12,7 +12,7 @@ describe "Macros" do it "adds a custom handler" do add_handler CustomTestHandler.new Kemal.application.setup - Kemal.application.handlers.size.should eq 7 + Kemal.application.handlers.size.should eq 8 end end diff --git a/src/kemal/application.cr b/src/kemal/application.cr index e7fa19c7..c5d1c4cf 100644 --- a/src/kemal/application.cr +++ b/src/kemal/application.cr @@ -1,7 +1,6 @@ class Kemal::Application < Kemal::Base def initialize(config = Config.default) - super config - add_filter_handler(filter_handler) + super(config) end # Overload of self.run with the default startup logging diff --git a/src/kemal/base.cr b/src/kemal/base.cr index 09af652e..f33e1bc4 100644 --- a/src/kemal/base.cr +++ b/src/kemal/base.cr @@ -1,4 +1,5 @@ require "./helpers/*" +require "./base/*" # Kemal Base # The DSL currently consists of @@ -10,19 +11,18 @@ class Kemal::Base include FileHelpers include Templates include Macros + include Base::DSL + include Base::Builder - HTTP_METHODS = %w(get post put patch delete options) - FILTER_METHODS = %w(get post put patch delete options all) - + # :nodoc: getter route_handler = Kemal::RouteHandler.new + # :nodoc: getter filter_handler = Kemal::FilterHandler.new + # :nodoc: getter websocket_handler = Kemal::WebSocketHandler.new getter handlers = [] of HTTP::Handler - getter custom_handlers = [] of Tuple(Nil | Int32, HTTP::Handler) - getter filter_handlers = [] of HTTP::Handler getter error_handlers = {} of Int32 => HTTP::Server::Context, Exception -> String - @handler_position = 0 getter config : Config @@ -31,133 +31,11 @@ class Kemal::Base property? running = false def initialize(@config = Config.base) - @logger = if @config.logging? - Kemal::LogHandler.new - else - Kemal::NullLogHandler.new - end - add_filter_handler(filter_handler) - end - - {% for method in HTTP_METHODS %} - def {{method.id}}(path, &block : HTTP::Server::Context -> _) - raise Kemal::Exceptions::InvalidPathStartException.new({{method}}, path) unless Kemal::Utils.path_starts_with_slash?(path) - route_handler.add_route({{method}}.upcase, path, &block) - end - {% end %} - - def ws(path, &block : HTTP::WebSocket, HTTP::Server::Context -> Void) - raise Kemal::Exceptions::InvalidPathStartException.new("ws", path) unless Kemal::Utils.path_starts_with_slash?(path) - websocket_handler.add_route path, &block - end - - def error(status_code, &block : HTTP::Server::Context, Exception -> _) - add_error_handler status_code, &block - end - - # All the helper methods available are: - # - before_all, before_get, before_post, before_put, before_patch, before_delete, before_options - # - after_all, after_get, after_post, after_put, after_patch, after_delete, after_options - {% for type in ["before", "after"] %} - {% for method in FILTER_METHODS %} - def {{type.id}}_{{method.id}}(path = "*", &block : HTTP::Server::Context -> _) - filter_handler.{{type.id}}({{method}}.upcase, path, &block) - end - {% end %} - {% end %} - - def clear - @router_included = false - @handler_position = 0 - @default_handlers_setup = false - - handlers.clear - custom_handlers.clear - filter_handlers.clear - error_handlers.clear - - route_handler.clear - websocket_handler.clear - end - - def handlers=(handlers : Array(HTTP::Handler)) - clear - @handlers.replace(handlers) - end - - def add_handler(handler : HTTP::Handler) - @custom_handlers << {nil, handler} - end - - def add_handler(handler : HTTP::Handler, position : Int32) - @custom_handlers << {position, handler} - end - - def add_filter_handler(handler : HTTP::Handler) - @filter_handlers << handler - end + @filter_handler = FilterHandler.new(self) + @route_handler = RouteHandler.new(self) + @websocket_handler = WebSocketHandler.new(self) - def add_error_handler(status_code, &handler : HTTP::Server::Context, Exception -> _) - @error_handlers[status_code] = ->(context : HTTP::Server::Context, error : Exception) { handler.call(context, error).to_s } - end - - def setup - unless @default_handlers_setup && @router_included - setup_init_handler - setup_log_handler - setup_error_handler - setup_static_file_handler - setup_custom_handlers - setup_filter_handlers - @default_handlers_setup = true - @router_included = true - handlers.insert(handlers.size, websocket_handler) - handlers.insert(handlers.size, route_handler) - end - end - - private def setup_init_handler - @handlers.insert(@handler_position, Kemal::InitHandler.new(self)) - @handler_position += 1 - end - - private def setup_log_handler - @handlers.insert(@handler_position, logger) - @handler_position += 1 - end - - private def setup_error_handler - if @config.always_rescue? - @error_handler ||= Kemal::ExceptionHandler.new - @handlers.insert(@handler_position, @error_handler.not_nil!) - @handler_position += 1 - end - end - - private def setup_static_file_handler - if @config.serve_static.is_a?(Hash) - @handlers.insert(@handler_position, Kemal::StaticFileHandler.new(@config.public_folder)) - @handler_position += 1 - end - end - - private def setup_custom_handlers - @custom_handlers.each do |ch| - position = ch[0] - if !position - @handlers.insert(@handler_position, ch[1]) - @handler_position += 1 - else - @handlers.insert(position, ch[1]) - @handler_position += 1 - end - end - end - - private def setup_filter_handlers - @filter_handlers.each do |h| - @handlers.insert(@handler_position, h) - end + initialize_defaults end # Overload of self.run with the default startup logging @@ -180,6 +58,11 @@ class Kemal::Base end end + # DEPRECATED: This method should be replaced with `#running?` + def running + running? + end + private def prepare_for_server_start unless @config.env == "test" Signal::INT.trap do diff --git a/src/kemal/base/builder.cr b/src/kemal/base/builder.cr new file mode 100644 index 00000000..f330234d --- /dev/null +++ b/src/kemal/base/builder.cr @@ -0,0 +1,108 @@ +class Kemal::Base + module Builder + getter custom_handlers = [] of Tuple(Nil | Int32, HTTP::Handler) + getter filter_handlers = [] of HTTP::Handler + @handler_position = 0 + + def clear + @router_included = false + @handler_position = 0 + @default_handlers_setup = false + + handlers.clear + custom_handlers.clear + filter_handlers.clear + error_handlers.clear + + route_handler.clear + websocket_handler.clear + end + + def handlers=(handlers : Array(HTTP::Handler)) + clear + @handlers.replace(handlers) + end + + def add_handler(handler : HTTP::Handler) + @custom_handlers << {nil, handler} + end + + def add_handler(handler : HTTP::Handler, position : Int32) + @custom_handlers << {position, handler} + end + + def add_filter_handler(handler : HTTP::Handler) + @filter_handlers << handler + end + + def add_error_handler(status_code, &handler : HTTP::Server::Context, Exception -> _) + @error_handlers[status_code] = ->(context : HTTP::Server::Context, error : Exception) { handler.call(context, error).to_s } + end + + def setup + @logger = if @config.logging? + LogHandler.new + else + NullLogHandler.new + end + unless @default_handlers_setup && @router_included + setup_init_handler + setup_log_handler + setup_error_handler + setup_static_file_handler + setup_custom_handlers + setup_filter_handlers + @default_handlers_setup = true + @router_included = true + handlers.insert(handlers.size, websocket_handler) + handlers.insert(handlers.size, route_handler) + end + end + + private def setup_init_handler + @handlers.insert(@handler_position, Kemal::InitHandler.new(self)) + @handler_position += 1 + end + + private def setup_log_handler + @handlers.insert(@handler_position, logger) + @handler_position += 1 + end + + private def setup_error_handler + if @config.always_rescue? + error_handler = @error_handler ||= Kemal::ExceptionHandler.new + @handlers.insert(@handler_position, error_handler) + @handler_position += 1 + end + end + + private def setup_static_file_handler + if @config.serve_static.is_a?(Hash) + @handlers.insert(@handler_position, Kemal::StaticFileHandler.new(@config.public_folder)) + @handler_position += 1 + end + end + + # Handle WebSocketHandler + private def setup_custom_handlers + @custom_handlers.each do |ch| + position = ch[0] + if !position + @handlers.insert(@handler_position, ch[1]) + @handler_position += 1 + else + @handlers.insert(position, ch[1]) + @handler_position += 1 + end + end + end + + private def setup_filter_handlers + @handlers.insert(@handler_position, filter_handler) + @filter_handlers.each do |h| + @handlers.insert(@handler_position, h) + end + end + end +end diff --git a/src/kemal/base/dsl.cr b/src/kemal/base/dsl.cr new file mode 100644 index 00000000..a7473acc --- /dev/null +++ b/src/kemal/base/dsl.cr @@ -0,0 +1,91 @@ +class Kemal::Base + module DSL + HTTP_METHODS = %w(get post put patch delete options) + FILTER_METHODS = %w(get post put patch delete options all) + + macro included + # :nodoc: + DEFAULT_HANDLERS = [] of {String, String, (HTTP::Server::Context -> Nil)} + # :nodoc: + WEBSOCKET_HANDLERS = [] of {String, (HTTP::WebSocket, HTTP::Server::Context -> Void)} + # :nodoc: + DEFAULT_ERROR_HANDLERS = [] of {Int32, (HTTP::Server::Context, Exception -> Nil)} + # :nodoc: + DEFAULT_FILTERS = [] of {Symbol, String, String, (HTTP::Server::Context -> Nil)} + end + + {% for method in HTTP_METHODS %} + def {{method.id}}(path, &block : HTTP::Server::Context -> _) + raise Kemal::Exceptions::InvalidPathStartException.new({{method}}, path) unless Kemal::Utils.path_starts_with_slash?(path) + route_handler.add_route({{method}}.upcase, path, &block) + end + {% end %} + + def ws(path, &block : HTTP::WebSocket, HTTP::Server::Context -> Void) + raise Kemal::Exceptions::InvalidPathStartException.new("ws", path) unless Kemal::Utils.path_starts_with_slash?(path) + websocket_handler.add_route path, &block + end + + def error(status_code, &block : HTTP::Server::Context, Exception -> _) + add_error_handler status_code, &block + end + + # All the helper methods available are: + # - before_all, before_get, before_post, before_put, before_patch, before_delete, before_options + # - after_all, after_get, after_post, after_put, after_patch, after_delete, after_options + {% for type in ["before", "after"] %} + {% for method in FILTER_METHODS %} + def {{type.id}}_{{method.id}}(path = "*", &block : HTTP::Server::Context -> _) + filter_handler.{{type.id}}({{method}}.upcase, path, &block) + end + {% end %} + {% end %} + + private def initialize_defaults + DEFAULT_HANDLERS.each do |method, path, block| + route_handler.add_route(method.upcase, path, &block) + end + + WEBSOCKET_HANDLERS.each do |path, block| + ws(path, &block) + end + + DEFAULT_ERROR_HANDLERS.each do |status_code, block| + add_error_handler status_code, &block + end + + DEFAULT_FILTERS.each do |type, method, path, block| + if type == :before + filter_handler.before(method, path, &block) + else + filter_handler.after(method, path, &block) + end + end + end + + {% for method in HTTP_METHODS %} + def self.{{method.id}}(path, &block : HTTP::Server::Context -> _) + DEFAULT_HANDLERS << { {{method}}, path, block } + end + {% end %} + + def self.ws(path, &block : HTTP::WebSocket, HTTP::Server::Context -> Void) + WEBSOCKET_HANDLERS << {path, block} + end + + def self.error(status_code, &block : HTTP::Server::Context, Exception -> _) + DEFAULT_ERROR_HANDLERS << {status_code, block} + end + + # All the helper methods available are: + # - before_all, before_get, before_post, before_put, before_patch, before_delete, before_options + # - after_all, after_get, after_post, after_put, after_patch, after_delete, after_options + {% for type in [:before, :after] %} + {% for method in FILTER_METHODS %} + def self.{{type.id}}_{{method.id}}(path = "*", &block : HTTP::Server::Context -> _) + DEFAULT_FILTERS << { {{type}}, {{method}}, path, block } + end + {% end %} + {% end %} + end +end diff --git a/src/kemal/config.cr b/src/kemal/config.cr index e4db16af..58f4979b 100644 --- a/src/kemal/config.cr +++ b/src/kemal/config.cr @@ -54,7 +54,6 @@ module Kemal (config.is_a?(Hash) && config[key]?) || false end - def extra_options(&@extra_options : OptionParser ->) end @@ -65,12 +64,12 @@ module Kemal # Creates a config with basic value (disabled logging, disabled serve_static, disabled shutdown_message) def self.base - new.tap do |config| - config.logging = false - config.serve_static = false - config.shutdown_message = false - config.always_rescue = false - end + new( + logging: false, + serve_static: false, + shutdown_message: false, + always_rescue: false, + ) end end end From 2e42b3f48c8254fb9d186e2f79162a7cf49defec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Tue, 18 Jul 2017 00:42:05 +0200 Subject: [PATCH 05/18] Decouple specs from global state to isolated tests --- spec/context_spec.cr | 132 ++++++++++++------------------ spec/dsl_helper.cr | 58 +++++++++++++ spec/exception_handler_spec.cr | 91 ++++++++++---------- spec/handler_spec.cr | 57 +++++++------ spec/helpers_spec.cr | 42 ++++++---- spec/param_parser_spec.cr | 2 +- spec/route_handler_spec.cr | 2 +- spec/route_spec.cr | 2 +- spec/run_spec.cr | 2 +- spec/spec_helper.cr | 79 ++---------------- spec/static_file_handler_spec.cr | 47 ++++++----- spec/view_spec.cr | 27 +++--- spec/websocket_handler_spec.cr | 41 +++++++--- src/kemal/base.cr | 4 + src/kemal/base/builder.cr | 4 +- src/kemal/config.cr | 4 + src/kemal/exception_handler.cr | 9 +- src/kemal/helpers/file_helpers.cr | 17 ++-- src/kemal/static_file_handler.cr | 12 ++- 19 files changed, 332 insertions(+), 300 deletions(-) create mode 100644 spec/dsl_helper.cr diff --git a/spec/context_spec.cr b/spec/context_spec.cr index 0148d431..ea1dbb65 100644 --- a/spec/context_spec.cr +++ b/spec/context_spec.cr @@ -1,102 +1,72 @@ require "./spec_helper" describe "Context" do - context "headers" do - it "sets content type" do - get "/" do |env| - env.response.content_type = "application/json" - "Hello" - end - request = HTTP::Request.new("GET", "/") - client_response = call_request_on_app(request) - client_response.headers["Content-Type"].should eq("application/json") + it "sets content type" do + app = Kemal::Base.new + app.get "/" do |env| + env.response.content_type = "application/json" + "Hello" end + request = HTTP::Request.new("GET", "/") + client_response = call_request_on_app(app, request) + client_response.headers["Content-Type"].should eq("application/json") + end - it "parses headers" do - get "/" do |env| - name = env.request.headers["name"] - "Hello #{name}" - end - headers = HTTP::Headers.new - headers["name"] = "kemal" - request = HTTP::Request.new("GET", "/", headers) - client_response = call_request_on_app(request) - client_response.body.should eq "Hello kemal" + it "parses headers" do + app = Kemal::Base.new + app.get "/" do |env| + name = env.request.headers["name"] + "Hello #{name}" end + headers = HTTP::Headers.new + headers["name"] = "kemal" + request = HTTP::Request.new("GET", "/", headers) + client_response = call_request_on_app(app, request) + client_response.body.should eq "Hello kemal" + end - it "sets response headers" do - get "/" do |env| - env.response.headers.add "Accept-Language", "tr" - end - request = HTTP::Request.new("GET", "/") - client_response = call_request_on_app(request) - client_response.headers["Accept-Language"].should eq "tr" + it "sets response headers" do + app = Kemal::Base.new + app.get "/" do |env| + env.response.headers.add "Accept-Language", "tr" end + request = HTTP::Request.new("GET", "/") + client_response = call_request_on_app(app, request) + client_response.headers["Accept-Language"].should eq "tr" end - context "storage" do - it "can store primitive types" do - before_get "/" do |env| - env.set "before_get", "Kemal" - env.set "before_get_int", 123 - env.set "before_get_float", 3.5 - end - - get "/" do |env| - { - before_get: env.get("before_get"), - before_get_int: env.get("before_get_int"), - before_get_float: env.get("before_get_float"), - } - end - - request = HTTP::Request.new("GET", "/") - io = IO::Memory.new - response = HTTP::Server::Response.new(io) - context = HTTP::Server::Context.new(request, response) - Kemal::FilterHandler::INSTANCE.call(context) - Kemal::RouteHandler::INSTANCE.call(context) - - context.get("before_get").should eq "Kemal" - context.get("before_get_int").should eq 123 - context.get("before_get_float").should eq 3.5 + it "can store variables" do + app = Kemal::Base.new + app.before_get "/" do |env| + t = TestContextStorageType.new + t.id = 32 + a = AnotherContextStorageType.new + env.set "key", "value" + env.set "before_get", "Kemal" + env.set "before_get_int", 123 + env.set "before_get_context_test", t + env.set "another_context_test", a + env.set "before_get_float", 3.5 end - it "can store custom types" do - before_get "/" do |env| - t = TestContextStorageType.new - t.id = 32 - a = AnotherContextStorageType.new - - env.set "before_get_context_test", t - env.set "another_context_test", a - end - - get "/" do |env| - { - before_get_context_test: env.get("before_get_context_test"), - another_context_test: env.get("another_context_test"), - } - end - - request = HTTP::Request.new("GET", "/") - io = IO::Memory.new - response = HTTP::Server::Response.new(io) - context = HTTP::Server::Context.new(request, response) - Kemal::FilterHandler::INSTANCE.call(context) - Kemal::RouteHandler::INSTANCE.call(context) - - context.get("before_get_context_test").as(TestContextStorageType).id.should eq 32 - context.get("another_context_test").as(AnotherContextStorageType).name.should eq "kemal-context" + app.get "/" do |env| + env.set "key", "value" + { + key: env.get("key"), + before_get: env.get("before_get"), + before_get_int: env.get("before_get_int"), + before_get_float: env.get("before_get_float"), + before_get_context_test: env.get("before_get_context_test"), + } end request = HTTP::Request.new("GET", "/") io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) - context.app = Kemal.application - Kemal.application.filter_handler.call(context) - Kemal.application.route_handler.call(context) + context.app = app + app.filter_handler.call(context) + app.route_handler.call(context) context.store["key"].should eq "value" context.store["before_get"].should eq "Kemal" context.store["before_get_int"].should eq 123 diff --git a/spec/dsl_helper.cr b/spec/dsl_helper.cr new file mode 100644 index 00000000..5aa05eed --- /dev/null +++ b/spec/dsl_helper.cr @@ -0,0 +1,58 @@ +require "./spec_helper" +require "../src/kemal/dsl" + +include Kemal + +class CustomLogHandler < Kemal::BaseLogHandler + def call(env) + call_next env + end + + def write(message) + end +end + +class TestContextStorageType + property id + @id = 1 + + def to_s + @id + end +end + +class AnotherContextStorageType + property name + @name = "kemal-context" +end + +add_context_storage_type(TestContextStorageType) +add_context_storage_type(AnotherContextStorageType) + +def create_request_and_return_io(handler, request) + io = IO::Memory.new + response = HTTP::Server::Response.new(io) + context = HTTP::Server::Context.new(request, response) + context.app = Kemal.application + handler.call(context) + response.close + io.rewind + io +end + +def call_request_on_app(request) + call_request_on_app(Kemal.application, request) +end + +def build_main_handler + build_main_handler(Kemal.application) +end + +Spec.before_each do + config = Kemal.config + config.env = "development" +end + +Spec.after_each do + Kemal.application.clear +end diff --git a/spec/exception_handler_spec.cr b/spec/exception_handler_spec.cr index bfa875d8..95df2889 100644 --- a/spec/exception_handler_spec.cr +++ b/spec/exception_handler_spec.cr @@ -1,18 +1,13 @@ -require "./spec_helper" - -private INSTANCE = Kemal::ExceptionHandler.new +require "./dsl_helper" describe "Kemal::ExceptionHandler" do it "renders 404 on route not found" do - get "/" do - "Hello" - end - request = HTTP::Request.new("GET", "/asd") io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) - INSTANCE.call(context) + subject = Kemal::ExceptionHandler.new(Kemal::Base.new) + subject.call(context) response.close io.rewind response = HTTP::Client::Response.from_io(io, decompress: false) @@ -20,19 +15,21 @@ describe "Kemal::ExceptionHandler" do end it "renders custom error" do - error 403 do - "403 error" - end - get "/" do |env| - env.response.status_code = 403 - end request = HTTP::Request.new("GET", "/") io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) - context.app = Kemal.application - INSTANCE.next = Kemal::RouteHandler.new - INSTANCE.call(context) + app = Kemal::Base.new + app.error 403 do + "403 error" + end + app.get "/" do |env| + env.response.status_code = 403 + end + context.app = app + subject = Kemal::ExceptionHandler.new(app) + subject.next = Kemal::RouteHandler.new + subject.call(context) response.close io.rewind response = HTTP::Client::Response.from_io(io, decompress: false) @@ -42,19 +39,21 @@ describe "Kemal::ExceptionHandler" do end it "renders custom 500 error" do - error 500 do - "Something happened" - end - get "/" do |env| - env.response.status_code = 500 - end request = HTTP::Request.new("GET", "/") io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) - context.app = Kemal.application - INSTANCE.next = Kemal::RouteHandler.new - INSTANCE.call(context) + app = Kemal::Base.new + app.error 500 do |env| + "Something happened" + end + app.get "/" do |env| + env.response.status_code = 500 + end + context.app = app + subject = Kemal::ExceptionHandler.new(app) + subject.next = Kemal::RouteHandler.new + subject.call(context) response.close io.rewind response = HTTP::Client::Response.from_io(io, decompress: false) @@ -64,20 +63,22 @@ describe "Kemal::ExceptionHandler" do end it "keeps the specified error Content-Type" do - error 500 do + request = HTTP::Request.new("GET", "/") + io = IO::Memory.new + response = HTTP::Server::Response.new(io) + context = HTTP::Server::Context.new(request, response) + app = Kemal::Base.new + app.error 500 do |env| "Something happened" end - get "/" do |env| + app.get "/" do |env| env.response.content_type = "application/json" env.response.status_code = 500 end - request = HTTP::Request.new("GET", "/") - io = IO::Memory.new - response = HTTP::Server::Response.new(io) - context = HTTP::Server::Context.new(request, response) - context.app = Kemal.application - INSTANCE.next = Kemal::RouteHandler.new - INSTANCE.call(context) + context.app = app + subject = Kemal::ExceptionHandler.new(app) + subject.next = Kemal::RouteHandler.new + subject.call(context) response.close io.rewind response = HTTP::Client::Response.from_io(io, decompress: false) @@ -87,20 +88,22 @@ describe "Kemal::ExceptionHandler" do end it "renders custom error with env and error" do - error 500 do |_, err| + request = HTTP::Request.new("GET", "/") + io = IO::Memory.new + response = HTTP::Server::Response.new(io) + context = HTTP::Server::Context.new(request, response) + app = Kemal::Base.new + app.error 500 do |env, err| err.message end - get "/" do |env| + app.get "/" do |env| env.response.content_type = "application/json" env.response.status_code = 500 end - request = HTTP::Request.new("GET", "/") - io = IO::Memory.new - response = HTTP::Server::Response.new(io) - context = HTTP::Server::Context.new(request, response) - context.app = Kemal.application - INSTANCE.next = Kemal::RouteHandler.new - INSTANCE.call(context) + context.app = app + subject = Kemal::ExceptionHandler.new(Kemal::Base.new) + subject.next = Kemal::RouteHandler.new + subject.call(context) response.close io.rewind response = HTTP::Client::Response.from_io(io, decompress: false) diff --git a/spec/handler_spec.cr b/spec/handler_spec.cr index e256fa7e..cd2a450f 100644 --- a/spec/handler_spec.cr +++ b/spec/handler_spec.cr @@ -78,81 +78,88 @@ describe "Handler" do filter_middleware._add_route_filter("GET", "/", :before) do |env| env.response << " so" end - Kemal.application.add_filter_handler filter_middleware + app = Kemal::Base.new + app.add_filter_handler filter_middleware - add_handler CustomTestHandler.new + app.add_handler CustomTestHandler.new - get "/" do + app.get "/" do |env| " Great" end request = HTTP::Request.new("GET", "/") - client_response = call_request_on_app(request) + client_response = call_request_on_app(app, request) client_response.status_code.should eq(200) client_response.body.should eq("Kemal is so Great") end it "runs specified only_routes in middleware" do - get "/only" do + app = Kemal::Base.new + app.get "/only" do |env| "Get" end - add_handler OnlyHandler.new + app.add_handler OnlyHandler.new request = HTTP::Request.new("GET", "/only") - client_response = call_request_on_app(request) + client_response = call_request_on_app(app, request) client_response.body.should eq "OnlyGet" end it "doesn't run specified exclude_routes in middleware" do - get "/" do + app = Kemal::Base.new + app.get "/" do |env| "Get" end - get "/exclude" do + app.get "/exclude" do "Exclude" end - add_handler ExcludeHandler.new + app.add_handler ExcludeHandler.new request = HTTP::Request.new("GET", "/") - client_response = call_request_on_app(request) + client_response = call_request_on_app(app, request) client_response.body.should eq "ExcludeGet" end it "runs specified only_routes with method in middleware" do - post "/only" do + app = Kemal::Base.new + app.post "/only" do "Post" end - get "/only" do + app.get "/only" do "Get" end - add_handler PostOnlyHandler.new + app.add_handler PostOnlyHandler.new request = HTTP::Request.new("POST", "/only") - client_response = call_request_on_app(request) + client_response = call_request_on_app(app, request) client_response.body.should eq "OnlyPost" end it "doesn't run specified exclude_routes with method in middleware" do - post "/exclude" do + app = Kemal::Base.new + app.post "/exclude" do "Post" end - post "/only" do + app.post "/only" do "Post" end - add_handler PostOnlyHandler.new - add_handler PostExcludeHandler.new + app.add_handler PostOnlyHandler.new + app.add_handler PostExcludeHandler.new request = HTTP::Request.new("POST", "/only") - client_response = call_request_on_app(request) + client_response = call_request_on_app(app, request) client_response.body.should eq "OnlyExcludePost" end it "adds a handler at given position" do post_handler = PostOnlyHandler.new - add_handler post_handler, 1 - Kemal.application.setup - Kemal.application.handlers[1].should eq post_handler + app = Kemal::Base.new + app.add_handler post_handler, 1 + app.setup + app.handlers[1].should eq post_handler end it "assigns custom handlers" do post_only_handler = PostOnlyHandler.new post_exclude_handler = PostExcludeHandler.new - Kemal.application.handlers = [post_only_handler, post_exclude_handler] - Kemal.application.handlers.should eq [post_only_handler, post_exclude_handler] + app = Kemal::Base.new + app.handlers = [post_only_handler, post_exclude_handler] + app.handlers.should eq [post_only_handler, post_exclude_handler] end it "is able to use %w in macros" do diff --git a/spec/helpers_spec.cr b/spec/helpers_spec.cr index a7290d61..4efa80c8 100644 --- a/spec/helpers_spec.cr +++ b/spec/helpers_spec.cr @@ -10,9 +10,10 @@ describe "Macros" do describe "#add_handler" do it "adds a custom handler" do - add_handler CustomTestHandler.new - Kemal.application.setup - Kemal.application.handlers.size.should eq 8 + app = Kemal::Application.new + app.add_handler CustomTestHandler.new + app.setup + app.handlers.size.should eq 8 end end @@ -23,7 +24,6 @@ describe "Macros" do end it "sets a custom logger" do - config = Kemal.config logger CustomLogHandler.new Kemal.application.logger.should be_a(CustomLogHandler) end @@ -31,32 +31,34 @@ describe "Macros" do describe "#halt" do it "can break block with halt macro" do - get "/non-breaking" do + app = Kemal::Base.new + app.get "/non-breaking" do |env| "hello" "world" end request = HTTP::Request.new("GET", "/non-breaking") - client_response = call_request_on_app(request) + client_response = call_request_on_app(app, request) client_response.status_code.should eq(200) client_response.body.should eq("world") - get "/breaking" do |env| + app.get "/breaking" do |env| halt env, 404, "hello" "world" end request = HTTP::Request.new("GET", "/breaking") - client_response = call_request_on_app(request) + client_response = call_request_on_app(app, request) client_response.status_code.should eq(404) client_response.body.should eq("hello") end it "can break block with halt macro using default values" do - get "/" do |env| + app = Kemal::Base.new + app.get "/" do |env| halt env "world" end request = HTTP::Request.new("GET", "/") - client_response = call_request_on_app(request) + client_response = call_request_on_app(app, request) client_response.status_code.should eq(200) client_response.body.should eq("") end @@ -64,7 +66,8 @@ describe "Macros" do describe "#headers" do it "can add headers" do - get "/headers" do |env| + app = Kemal::Base.new + app.get "/headers" do |env| env.response.headers.add "Content-Type", "image/png" headers env, { "Access-Control-Allow-Origin" => "*", @@ -72,7 +75,7 @@ describe "Macros" do } end request = HTTP::Request.new("GET", "/headers") - response = call_request_on_app(request) + response = call_request_on_app(app, request) response.headers["Access-Control-Allow-Origin"].should eq("*") response.headers["Content-Type"].should eq("text/plain") end @@ -80,36 +83,39 @@ describe "Macros" do describe "#send_file" do it "sends file with given path and default mime-type" do - get "/" do |env| + app = Kemal::Base.new + app.get "/" do |env| send_file env, "./spec/asset/hello.ecr" end request = HTTP::Request.new("GET", "/") - response = call_request_on_app(request) + response = call_request_on_app(app, request) response.status_code.should eq(200) response.headers["Content-Type"].should eq("application/octet-stream") response.headers["Content-Length"].should eq("18") end it "sends file with given path and given mime-type" do - get "/" do |env| + app = Kemal::Base.new + app.get "/" do |env| send_file env, "./spec/asset/hello.ecr", "image/jpeg" end request = HTTP::Request.new("GET", "/") - response = call_request_on_app(request) + response = call_request_on_app(app, request) response.status_code.should eq(200) response.headers["Content-Type"].should eq("image/jpeg") response.headers["Content-Length"].should eq("18") end it "sends file with binary stream" do - get "/" do |env| + app = Kemal::Base.new + app.get "/" do |env| send_file env, "Serdar".to_slice end request = HTTP::Request.new("GET", "/") - response = call_request_on_app(request) + response = call_request_on_app(app, request) response.status_code.should eq(200) response.headers["Content-Type"].should eq("application/octet-stream") response.headers["Content-Length"].should eq("6") diff --git a/spec/param_parser_spec.cr b/spec/param_parser_spec.cr index f85c94e4..07d76043 100644 --- a/spec/param_parser_spec.cr +++ b/spec/param_parser_spec.cr @@ -1,4 +1,4 @@ -require "./spec_helper" +require "./dsl_helper" describe "ParamParser" do it "parses query params" do diff --git a/spec/route_handler_spec.cr b/spec/route_handler_spec.cr index a6db5e12..5b677b51 100644 --- a/spec/route_handler_spec.cr +++ b/spec/route_handler_spec.cr @@ -1,4 +1,4 @@ -require "./spec_helper" +require "./dsl_helper" describe "Kemal::RouteHandler" do it "routes" do diff --git a/spec/route_spec.cr b/spec/route_spec.cr index 7634d51d..cb6139b0 100644 --- a/spec/route_spec.cr +++ b/spec/route_spec.cr @@ -1,4 +1,4 @@ -require "./spec_helper" +require "./dsl_helper" describe "Route" do describe "match?" do diff --git a/spec/run_spec.cr b/spec/run_spec.cr index 915fab8c..b711b68b 100644 --- a/spec/run_spec.cr +++ b/spec/run_spec.cr @@ -1,4 +1,4 @@ -require "./spec_helper" +require "./dsl_helper" private def run(code) code = <<-CR diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 2e62d70f..bfa50b0f 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -1,89 +1,24 @@ require "spec" -require "../src/**" -require "../src/kemal/base" -require "../src/kemal/dsl" +require "../src/kemal" -include Kemal - -class CustomLogHandler < Kemal::BaseLogHandler - def call(env) - call_next env - end - - def write(message) - end -end - -class TestContextStorageType - property id - @id = 1 - - def to_s - @id - end -end - -class AnotherContextStorageType - property name - @name = "kemal-context" -end - -add_context_storage_type(TestContextStorageType) -add_context_storage_type(AnotherContextStorageType) - -def create_request_and_return_io_and_context(handler, request) +def call_request_on_app(app, request) io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) - context.app = Kemal.application - handler.call(context) - response.close - io.rewind - {io, context} -end - -def create_ws_request_and_return_io_and_context(handler, request) - io = IO::Memory.new - response = HTTP::Server::Response.new(io) - context = HTTP::Server::Context.new(request, response) - context.app = Kemal.application - begin - handler.call context - rescue IO::Error - # Raises because the IO::Memory is empty - end - io.rewind - {io, context} -end - -def call_request_on_app(request) - io = IO::Memory.new - response = HTTP::Server::Response.new(io) - context = HTTP::Server::Context.new(request, response) - main_handler = build_main_handler + main_handler = build_main_handler(app) main_handler.call context response.close io.rewind HTTP::Client::Response.from_io(io, decompress: false) end -def build_main_handler - Kemal.application.setup - main_handler = Kemal.application.handlers.first +def build_main_handler(app) + app.setup + main_handler = app.handlers.first current_handler = main_handler - Kemal.application.handlers.each_with_index do |handler, index| + app.handlers.each_with_index do |handler, index| current_handler.next = handler current_handler = handler end main_handler end - -Spec.before_each do - config = Kemal.config - config.env = "development" - config.logging = false -end - -Spec.after_each do - Kemal.application.clear -end diff --git a/spec/static_file_handler_spec.cr b/spec/static_file_handler_spec.cr index 54293b45..bbb0a244 100644 --- a/spec/static_file_handler_spec.cr +++ b/spec/static_file_handler_spec.cr @@ -1,17 +1,22 @@ require "./spec_helper" -private def handle(request, fallthrough = true) +private def handle(request, config = default_config, 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 = Kemal::StaticFileHandler.new config, fallthrough handler.call context response.close io.rewind HTTP::Client::Response.from_io(io) end +private def default_config + Kemal::Config.new.tap do |config| + config.public_folder = "#{__DIR__}/static" + end +end + describe Kemal::StaticFileHandler do file = File.open "#{__DIR__}/static/dir/test.txt" file_size = file.size @@ -37,38 +42,43 @@ describe Kemal::StaticFileHandler do end it "should not list directory's entries" do - serve_static({"gzip" => true, "dir_listing" => false}) - response = handle HTTP::Request.new("GET", "/dir/") + config = default_config + config.serve_static = {"gzip" => true, "dir_listing" => false} + response = handle HTTP::Request.new("GET", "/dir/"), config response.status_code.should eq(404) end it "should list directory's entries when config is set" do - serve_static({"gzip" => true, "dir_listing" => true}) - response = handle HTTP::Request.new("GET", "/dir/") + config = default_config + config.serve_static = {"gzip" => true, "dir_listing" => true} + response = handle HTTP::Request.new("GET", "/dir/"), config response.status_code.should eq(200) response.body.should match(/test.txt/) end it "should gzip a file if config is true, headers accept gzip and file is > 880 bytes" do - serve_static({"gzip" => true, "dir_listing" => true}) + config = default_config + config.serve_static = {"gzip" => true, "dir_listing" => true} headers = HTTP::Headers{"Accept-Encoding" => "gzip, deflate, sdch, br"} - response = handle HTTP::Request.new("GET", "/dir/bigger.txt", headers) + response = handle HTTP::Request.new("GET", "/dir/bigger.txt", headers), config response.status_code.should eq(200) response.headers["Content-Encoding"].should eq "gzip" end it "should not gzip a file if config is true, headers accept gzip and file is < 880 bytes" do - serve_static({"gzip" => true, "dir_listing" => true}) + config = default_config + config.serve_static = {"gzip" => true, "dir_listing" => true} headers = HTTP::Headers{"Accept-Encoding" => "gzip, deflate, sdch, br"} - response = handle HTTP::Request.new("GET", "/dir/test.txt", headers) + response = handle HTTP::Request.new("GET", "/dir/test.txt", headers), config response.status_code.should eq(200) response.headers["Content-Encoding"]?.should be_nil end it "should not gzip a file if config is false, headers accept gzip and file is > 880 bytes" do - serve_static({"gzip" => false, "dir_listing" => true}) + config = default_config + config.serve_static = {"gzip" => false, "dir_listing" => true} headers = HTTP::Headers{"Accept-Encoding" => "gzip, deflate, sdch, br"} - response = handle HTTP::Request.new("GET", "/dir/bigger.txt", headers) + response = handle HTTP::Request.new("GET", "/dir/bigger.txt", headers), config response.status_code.should eq(200) response.headers["Content-Encoding"]?.should be_nil end @@ -97,7 +107,7 @@ describe Kemal::StaticFileHandler do %w(POST PUT DELETE).each do |method| response = handle HTTP::Request.new(method, "/dir/test.txt") response.status_code.should eq(404) - response = handle HTTP::Request.new(method, "/dir/test.txt"), false + response = handle HTTP::Request.new(method, "/dir/test.txt"), fallthrough: false response.status_code.should eq(405) response.headers["Allow"].should eq("GET, HEAD") end @@ -133,22 +143,21 @@ describe Kemal::StaticFileHandler do end it "should handle setting custom headers" do - headers = Proc(HTTP::Server::Response, String, File::Info, Void).new do |response, path, stat| + config = default_config + config.static_headers = Proc(HTTP::Server::Response, String, File::Info, Void).new do |response, path, stat| if path =~ /\.html$/ response.headers.add("Access-Control-Allow-Origin", "*") end response.headers.add("Content-Size", stat.size.to_s) end - static_headers(&headers) - - response = handle HTTP::Request.new("GET", "/dir/test.txt") + response = handle HTTP::Request.new("GET", "/dir/test.txt"), config response.headers.has_key?("Access-Control-Allow-Origin").should be_false response.headers["Content-Size"].should eq( File.info("#{__DIR__}/static/dir/test.txt").size.to_s ) - response = handle HTTP::Request.new("GET", "/dir/index.html") + response = handle HTTP::Request.new("GET", "/dir/index.html"), config response.headers["Access-Control-Allow-Origin"].should eq("*") end end diff --git a/spec/view_spec.cr b/spec/view_spec.cr index d09f4de4..f6e6cf7a 100644 --- a/spec/view_spec.cr +++ b/spec/view_spec.cr @@ -1,4 +1,4 @@ -require "./spec_helper" +require "./dsl_helper" macro render_with_base_and_layout(filename) render "spec/asset/#{{{filename}}}", "spec/asset/layout.ecr" @@ -6,56 +6,61 @@ end describe "Views" do it "renders file" do - get "/view/:name" do |env| + app = Kemal::Base.new + app.get "/view/:name" do |env| name = env.params.url["name"] render "spec/asset/hello.ecr" end request = HTTP::Request.new("GET", "/view/world") - client_response = call_request_on_app(request) + client_response = call_request_on_app(app, request) client_response.body.should contain("Hello world") end it "renders file with dynamic variables" do - get "/view/:name" do |env| + app = Kemal::Base.new + app.get "/view/:name" do |env| name = env.params.url["name"] render_with_base_and_layout "hello.ecr" end request = HTTP::Request.new("GET", "/view/world") - client_response = call_request_on_app(request) + client_response = call_request_on_app(app, request) client_response.body.should contain("Hello world") end it "renders layout" do - get "/view/:name" do |env| + app = Kemal::Base.new + app.get "/view/:name" do |env| name = env.params.url["name"] render "spec/asset/hello.ecr", "spec/asset/layout.ecr" end request = HTTP::Request.new("GET", "/view/world") - client_response = call_request_on_app(request) + client_response = call_request_on_app(app, request) client_response.body.should contain("Hello world") end it "renders layout with variables" do - get "/view/:name" do |env| + app = Kemal::Base.new + app.get "/view/:name" do |env| name = env.params.url["name"] var1 = "serdar" var2 = "kemal" render "spec/asset/hello_with_content_for.ecr", "spec/asset/layout_with_yield_and_vars.ecr" end request = HTTP::Request.new("GET", "/view/world") - client_response = call_request_on_app(request) + client_response = call_request_on_app(app, request) client_response.body.should contain("Hello world") client_response.body.should contain("serdar") client_response.body.should contain("kemal") end it "renders layout with content_for" do - get "/view/:name" do |env| + app = Kemal::Base.new + app.get "/view/:name" do |env| name = env.params.url["name"] render "spec/asset/hello_with_content_for.ecr", "spec/asset/layout_with_yield.ecr" end request = HTTP::Request.new("GET", "/view/world") - client_response = call_request_on_app(request) + client_response = call_request_on_app(app, request) client_response.body.should contain("Hello world") client_response.body.should contain("

Hello from otherside

") end diff --git a/spec/websocket_handler_spec.cr b/spec/websocket_handler_spec.cr index ab3d58e8..cbcb34e4 100644 --- a/spec/websocket_handler_spec.cr +++ b/spec/websocket_handler_spec.cr @@ -1,10 +1,24 @@ require "./spec_helper" +private def create_ws_request_and_return_io(handler, request, app) + io = IO::Memory.new + response = HTTP::Server::Response.new(io) + context = HTTP::Server::Context.new(request, response) + context.app = app + begin + handler.call context + rescue IO::Error + # Raises because the IO::Memory is empty + end + io +end + describe "Kemal::WebSocketHandler" do it "doesn't match on wrong route" do + app = Kemal::Base.new handler = Kemal::WebSocketHandler.new handler.next = Kemal::RouteHandler.new - ws "/" { } + app.ws "/" { } headers = HTTP::Headers{ "Upgrade" => "websocket", "Connection" => "Upgrade", @@ -14,7 +28,7 @@ describe "Kemal::WebSocketHandler" do io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) - context.app = Kemal.application + context.app = app expect_raises(Kemal::Exceptions::RouteNotFound) do handler.call context @@ -22,9 +36,10 @@ describe "Kemal::WebSocketHandler" do end it "matches on given route" do + app = Kemal::Base.new handler = Kemal::WebSocketHandler.new - ws "/" { |socket, context| socket.send("Match") } - ws "/no_match" { |socket, context| socket.send "No Match" } + app.ws "/" { |socket, context| socket.send("Match") } + app.ws "/no_match" { |socket, context| socket.send "No Match" } headers = HTTP::Headers{ "Upgrade" => "websocket", "Connection" => "Upgrade", @@ -33,13 +48,14 @@ describe "Kemal::WebSocketHandler" do } request = HTTP::Request.new("GET", "/", headers) - io_with_context = create_ws_request_and_return_io_and_context(handler, request)[0] - io_with_context.to_s.should eq("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n\x81\u0005Match") + io_with_context = create_ws_request_and_return_io(handler, request, app) + io_with_context.to_s.should eq("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-Websocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n\x81\u0005Match") end it "fetches named url parameters" do + app = Kemal::Base.new handler = Kemal::WebSocketHandler.new - ws "/:id" { |s, c| c.params.url["id"] } + app.ws "/:id" { |s, c| c.params.url["id"] } headers = HTTP::Headers{ "Upgrade" => "websocket", "Connection" => "Upgrade", @@ -47,20 +63,21 @@ describe "Kemal::WebSocketHandler" do "Sec-WebSocket-Version" => "13", } request = HTTP::Request.new("GET", "/1234", headers) - io_with_context = create_ws_request_and_return_io_and_context(handler, request)[0] - io_with_context.to_s.should eq("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n") + io_with_context = create_ws_request_and_return_io(handler, request, app) + io_with_context.to_s.should eq("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-Websocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n") end it "matches correct verb" do + app = Kemal::Base.new handler = Kemal::WebSocketHandler.new handler.next = Kemal::RouteHandler.new - ws "/" { } - get "/" { "get" } + app.ws "/" { } + app.get "/" { "get" } request = HTTP::Request.new("GET", "/") io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) - context.app = Kemal.application + context.app = app handler.call(context) response.close io.rewind diff --git a/src/kemal/base.cr b/src/kemal/base.cr index f33e1bc4..649d6c1d 100644 --- a/src/kemal/base.cr +++ b/src/kemal/base.cr @@ -99,4 +99,8 @@ class Kemal::Base raise "Kemal is already stopped." end end + + def log(message) + logger.write "#{message}\n" + end end diff --git a/src/kemal/base/builder.cr b/src/kemal/base/builder.cr index f330234d..e4aaad15 100644 --- a/src/kemal/base/builder.cr +++ b/src/kemal/base/builder.cr @@ -71,7 +71,7 @@ class Kemal::Base private def setup_error_handler if @config.always_rescue? - error_handler = @error_handler ||= Kemal::ExceptionHandler.new + error_handler = @error_handler ||= Kemal::ExceptionHandler.new(self) @handlers.insert(@handler_position, error_handler) @handler_position += 1 end @@ -79,7 +79,7 @@ class Kemal::Base private def setup_static_file_handler if @config.serve_static.is_a?(Hash) - @handlers.insert(@handler_position, Kemal::StaticFileHandler.new(@config.public_folder)) + @handlers.insert(@handler_position, Kemal::StaticFileHandler.new(@config)) @handler_position += 1 end end diff --git a/src/kemal/config.cr b/src/kemal/config.cr index 58f4979b..0af21014 100644 --- a/src/kemal/config.cr +++ b/src/kemal/config.cr @@ -57,6 +57,10 @@ module Kemal def extra_options(&@extra_options : OptionParser ->) end + def serve_static?(key) + (h = @serve_static).is_a?(Hash) && h[key]? == true + end + # Create a config with default values def self.default new diff --git a/src/kemal/exception_handler.cr b/src/kemal/exception_handler.cr index e1d7168d..32da24b6 100644 --- a/src/kemal/exception_handler.cr +++ b/src/kemal/exception_handler.cr @@ -3,6 +3,13 @@ module Kemal class ExceptionHandler include HTTP::Handler + getter app : Kemal::Base + + def initialize(@app) + end + + delegate log, to: app + def call(context : HTTP::Server::Context) begin call_next(context) @@ -14,7 +21,7 @@ module Kemal log("Exception: #{ex.inspect_with_backtrace}") return call_exception_with_status_code(context, ex, 500) if context.app.error_handlers.has_key?(500) verbosity = context.app.config.env == "production" ? false : true - return render_500(context, ex.inspect_with_backtrace, verbosity) + return app.render_500(context, ex.inspect_with_backtrace, verbosity) end end diff --git a/src/kemal/helpers/file_helpers.cr b/src/kemal/helpers/file_helpers.cr index c30d0e98..375439cc 100644 --- a/src/kemal/helpers/file_helpers.cr +++ b/src/kemal/helpers/file_helpers.cr @@ -1,8 +1,5 @@ module Kemal::FileHelpers - def log(message) - logger.write "#{message}\n" - end - + extend self # Send a file with given path and base the mime-type on the file extension # or default `application/octet-stream` mime_type. # @@ -15,8 +12,7 @@ module Kemal::FileHelpers # ``` # 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 + def send_file(env : HTTP::Server::Context, path : String, config : Kemal::Config, mime_type : String? = nil) file_path = File.expand_path(path, Dir.current) mime_type ||= Kemal::Utils.mime_type(file_path) env.response.content_type = mime_type @@ -28,17 +24,18 @@ module Kemal::FileHelpers filestat = File.stat(file_path) config.static_headers.try(&.call(env.response, file_path, filestat)) + gzip = config.serve_static?("gzip") 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) + if request_headers.includes_word?("Accept-Encoding", "gzip") && 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) + elsif request_headers.includes_word?("Accept-Encoding", "deflate") && 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) @@ -51,6 +48,10 @@ module Kemal::FileHelpers return end + def send_file(env, path : String, mime_type : String? = nil) + send_file(env, path, env.app.config, mime_type) + end + private def multipart(file, env : HTTP::Server::Context) # See http://httpwg.org/specs/rfc7233.html fileb = file.size diff --git a/src/kemal/static_file_handler.cr b/src/kemal/static_file_handler.cr index 50d1574a..fc6446c4 100644 --- a/src/kemal/static_file_handler.cr +++ b/src/kemal/static_file_handler.cr @@ -4,6 +4,12 @@ module Kemal class StaticFileHandler < HTTP::StaticFileHandler + getter config : Kemal::Config + + def initialize(@config, fallthrough = true) + super(@config.public_folder, fallthrough) + end + def call(context : HTTP::Server::Context) return call_next(context) if context.request.path.not_nil! == "/" @@ -19,7 +25,6 @@ module Kemal return end - config = Kemal.config.serve_static original_path = context.request.path.not_nil! request_path = URI.unescape(original_path) @@ -48,7 +53,7 @@ module Kemal end if Dir.exists?(file_path) - if config.is_a?(Hash) && config["dir_listing"] == true + if @config.serve_static?("dir_listing") context.response.content_type = "text/html" directory_listing(context.response, request_path, file_path) else @@ -62,7 +67,8 @@ module Kemal context.response.status_code = 304 return end - send_file(context, file_path) + + FileHelpers.send_file(context, file_path, config) else call_next(context) end From 53fa65f96443ae2a72622389e72407921c9283a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Sun, 15 Oct 2017 22:32:11 +0200 Subject: [PATCH 06/18] Add class method API to Kemal::Base --- samples/app.cr | 9 +++++++++ spec/application_mode_spec.cr | 27 +++++++++++++++++++++++++++ src/kemal/base.cr | 15 +++++++++++++++ src/kemal/base/dsl.cr | 20 +++++++++++--------- 4 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 samples/app.cr create mode 100644 spec/application_mode_spec.cr diff --git a/samples/app.cr b/samples/app.cr new file mode 100644 index 00000000..2ce65ab5 --- /dev/null +++ b/samples/app.cr @@ -0,0 +1,9 @@ +require "kemal/base" + +class MyApp < Kemal::Application + get "/" do + "Hello Kemal!" + end +end + +MyApp.run diff --git a/spec/application_mode_spec.cr b/spec/application_mode_spec.cr new file mode 100644 index 00000000..49e1f683 --- /dev/null +++ b/spec/application_mode_spec.cr @@ -0,0 +1,27 @@ +require "./spec_helper" + +private class MyApp < Kemal::Application + get "/route1" do |env| + "Route 1" + end + + get "/route2" do |env| + "Route 2" + end +end + +describe MyApp do + it "matches the correct route" do + request = HTTP::Request.new("GET", "/route2") + client_response = call_request_on_app(MyApp.new, request) + client_response.body.should eq("Route 2") + end + + it "doesn't allow a route declaration start without /" do + expect_raises Kemal::Exceptions::InvalidPathStartException, "Route declaration get \"route\" needs to start with '/', should be get \"/route\"" do + MyApp.new.get "route" do |env| + "Route 1" + end + end + end +end diff --git a/src/kemal/base.cr b/src/kemal/base.cr index 649d6c1d..b6eadd2f 100644 --- a/src/kemal/base.cr +++ b/src/kemal/base.cr @@ -13,6 +13,7 @@ class Kemal::Base include Macros include Base::DSL include Base::Builder + extend Base::ClassDSL # :nodoc: getter route_handler = Kemal::RouteHandler.new @@ -58,6 +59,20 @@ class Kemal::Base end end + def self.run(port : Int32? = nil) + new.tap do |app| + Kemal::CLI.new(app.config) + + app.run(port) do + yield app + end + end + end + + def self.run(port : Int32? = nil) + run(port) { } + end + # DEPRECATED: This method should be replaced with `#running?` def running running? diff --git a/src/kemal/base/dsl.cr b/src/kemal/base/dsl.cr index a7473acc..fe2cc914 100644 --- a/src/kemal/base/dsl.cr +++ b/src/kemal/base/dsl.cr @@ -5,13 +5,13 @@ class Kemal::Base macro included # :nodoc: - DEFAULT_HANDLERS = [] of {String, String, (HTTP::Server::Context -> Nil)} + DEFAULT_HANDLERS = [] of {String, String, (HTTP::Server::Context -> String)} # :nodoc: WEBSOCKET_HANDLERS = [] of {String, (HTTP::WebSocket, HTTP::Server::Context -> Void)} # :nodoc: - DEFAULT_ERROR_HANDLERS = [] of {Int32, (HTTP::Server::Context, Exception -> Nil)} + DEFAULT_ERROR_HANDLERS = [] of {Int32, (HTTP::Server::Context, Exception -> String)} # :nodoc: - DEFAULT_FILTERS = [] of {Symbol, String, String, (HTTP::Server::Context -> Nil)} + DEFAULT_FILTERS = [] of {Symbol, String, String, (HTTP::Server::Context -> String)} end {% for method in HTTP_METHODS %} @@ -62,18 +62,20 @@ class Kemal::Base end end end + end - {% for method in HTTP_METHODS %} - def self.{{method.id}}(path, &block : HTTP::Server::Context -> _) + module ClassDSL + {% for method in DSL::HTTP_METHODS %} + def {{method.id}}(path, &block : HTTP::Server::Context -> _) DEFAULT_HANDLERS << { {{method}}, path, block } end {% end %} - def self.ws(path, &block : HTTP::WebSocket, HTTP::Server::Context -> Void) + def ws(path, &block : HTTP::WebSocket, HTTP::Server::Context -> Void) WEBSOCKET_HANDLERS << {path, block} end - def self.error(status_code, &block : HTTP::Server::Context, Exception -> _) + def error(status_code, &block : HTTP::Server::Context, Exception -> _) DEFAULT_ERROR_HANDLERS << {status_code, block} end @@ -81,8 +83,8 @@ class Kemal::Base # - before_all, before_get, before_post, before_put, before_patch, before_delete, before_options # - after_all, after_get, after_post, after_put, after_patch, after_delete, after_options {% for type in [:before, :after] %} - {% for method in FILTER_METHODS %} - def self.{{type.id}}_{{method.id}}(path = "*", &block : HTTP::Server::Context -> _) + {% for method in DSL::FILTER_METHODS %} + def {{type.id}}_{{method.id}}(path = "*", &block : HTTP::Server::Context -> _) DEFAULT_FILTERS << { {{type}}, {{method}}, path, block } end {% end %} From ad91a22789ccf953294a4a43aad07b8277d676a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Thu, 20 Jul 2017 10:48:31 +0200 Subject: [PATCH 07/18] Refactor class level DSL with macros to convert blocks to instance-scoped methods --- samples/app_squared.cr | 17 ++++ spec/application_mode_spec.cr | 23 +++++ spec/helpers_spec.cr | 12 +-- src/kemal/application.cr | 4 +- src/kemal/base.cr | 5 +- src/kemal/base/dsl.cr | 169 ++++++++++++++++++++++++++-------- 6 files changed, 179 insertions(+), 51 deletions(-) create mode 100644 samples/app_squared.cr diff --git a/samples/app_squared.cr b/samples/app_squared.cr new file mode 100644 index 00000000..60c75094 --- /dev/null +++ b/samples/app_squared.cr @@ -0,0 +1,17 @@ +require "../src/kemal/base" + +class MyApp < Kemal::Application + get "/" do |env| + "Hello Kemal!" + end +end + +class OtherApp < Kemal::Application + get "/" do |env| + "Hello World!" + end +end + +spawn { MyApp.run(3002) } + +OtherApp.run(3001) diff --git a/spec/application_mode_spec.cr b/spec/application_mode_spec.cr index 49e1f683..a8448108 100644 --- a/spec/application_mode_spec.cr +++ b/spec/application_mode_spec.cr @@ -8,6 +8,10 @@ private class MyApp < Kemal::Application get "/route2" do |env| "Route 2" end + + get "/file" do |env| + send_file env, "Serdar".to_slice + end end describe MyApp do @@ -24,4 +28,23 @@ describe MyApp do end end end + + it "sends file with binary stream" do + request = HTTP::Request.new("GET", "/file") + response = call_request_on_app(MyApp.new, request) + response.status_code.should eq(200) + response.headers["Content-Type"].should eq("application/octet-stream") + response.headers["Content-Length"].should eq("6") + end + + it "responds to delayed route" do + app = MyApp.new + app.setup + app.get "/delayed" do |env| + "Happy addition!" + end + request = HTTP::Request.new("GET", "/delayed") + client_response = call_request_on_app(app, request) + client_response.body.should eq("Happy addition!") + end end diff --git a/spec/helpers_spec.cr b/spec/helpers_spec.cr index 4efa80c8..4e70bfa3 100644 --- a/spec/helpers_spec.cr +++ b/spec/helpers_spec.cr @@ -42,7 +42,7 @@ describe "Macros" do client_response.body.should eq("world") app.get "/breaking" do |env| - halt env, 404, "hello" + Kemal::Macros.halt env, 404, "hello" "world" end request = HTTP::Request.new("GET", "/breaking") @@ -54,7 +54,7 @@ describe "Macros" do it "can break block with halt macro using default values" do app = Kemal::Base.new app.get "/" do |env| - halt env + Kemal::Macros.halt env "world" end request = HTTP::Request.new("GET", "/") @@ -69,7 +69,7 @@ describe "Macros" do app = Kemal::Base.new app.get "/headers" do |env| env.response.headers.add "Content-Type", "image/png" - headers env, { + app.headers env, { "Access-Control-Allow-Origin" => "*", "Content-Type" => "text/plain", } @@ -85,7 +85,7 @@ describe "Macros" do it "sends file with given path and default mime-type" do app = Kemal::Base.new app.get "/" do |env| - send_file env, "./spec/asset/hello.ecr" + app.send_file env, "./spec/asset/hello.ecr" end request = HTTP::Request.new("GET", "/") @@ -98,7 +98,7 @@ describe "Macros" do it "sends file with given path and given mime-type" do app = Kemal::Base.new app.get "/" do |env| - send_file env, "./spec/asset/hello.ecr", "image/jpeg" + app.send_file env, "./spec/asset/hello.ecr", "image/jpeg" end request = HTTP::Request.new("GET", "/") @@ -111,7 +111,7 @@ describe "Macros" do it "sends file with binary stream" do app = Kemal::Base.new app.get "/" do |env| - send_file env, "Serdar".to_slice + app.send_file env, "Serdar".to_slice end request = HTTP::Request.new("GET", "/") diff --git a/src/kemal/application.cr b/src/kemal/application.cr index c5d1c4cf..5aca0037 100644 --- a/src/kemal/application.cr +++ b/src/kemal/application.cr @@ -14,7 +14,7 @@ class Kemal::Application < Kemal::Base super unless error_handlers.has_key?(404) - error 404 do |env| + self.error 404 do |env| render_404 end end @@ -22,7 +22,7 @@ class Kemal::Application < Kemal::Base # Test environment doesn't need to have signal trap, built-in images, and logging. unless @config.env == "test" # This route serves the built-in images for not_found and exceptions. - get "/__kemal__/:image" do |env| + self.get "/__kemal__/:image" do |env| image = env.params.url["image"] file_path = File.expand_path("lib/kemal/images/#{image}", Dir.current) if File.exists? file_path diff --git a/src/kemal/base.cr b/src/kemal/base.cr index b6eadd2f..94ac3fcd 100644 --- a/src/kemal/base.cr +++ b/src/kemal/base.cr @@ -13,7 +13,6 @@ class Kemal::Base include Macros include Base::DSL include Base::Builder - extend Base::ClassDSL # :nodoc: getter route_handler = Kemal::RouteHandler.new @@ -41,9 +40,7 @@ class Kemal::Base # Overload of self.run with the default startup logging def run(port : Int32? = nil) - run port do - log "[#{config.env}] Kemal is ready to lead at #{config.scheme}://#{config.host_binding}:#{config.port}" - end + run(port) { } end # The command to run a `Kemal` application. diff --git a/src/kemal/base/dsl.cr b/src/kemal/base/dsl.cr index fe2cc914..8b74b393 100644 --- a/src/kemal/base/dsl.cr +++ b/src/kemal/base/dsl.cr @@ -1,31 +1,42 @@ class Kemal::Base + private CUSTOM_METHODS_REGISTRY = {} of _ => _ + + macro inherited + {% CUSTOM_METHODS_REGISTRY[@type] = { + handlers: [] of _, + ws: [] of _, + error: [] of _, + filters: [] of _, + } %} + + include MacroDSL + end + module DSL HTTP_METHODS = %w(get post put patch delete options) FILTER_METHODS = %w(get post put patch delete options all) - macro included - # :nodoc: - DEFAULT_HANDLERS = [] of {String, String, (HTTP::Server::Context -> String)} - # :nodoc: - WEBSOCKET_HANDLERS = [] of {String, (HTTP::WebSocket, HTTP::Server::Context -> Void)} - # :nodoc: - DEFAULT_ERROR_HANDLERS = [] of {Int32, (HTTP::Server::Context, Exception -> String)} - # :nodoc: - DEFAULT_FILTERS = [] of {Symbol, String, String, (HTTP::Server::Context -> String)} - end - {% for method in HTTP_METHODS %} + # Add a `{{method.id.upcase}}` handler. + # + # The block receives an `HTTP::Server::Context` as argument. def {{method.id}}(path, &block : HTTP::Server::Context -> _) - raise Kemal::Exceptions::InvalidPathStartException.new({{method}}, path) unless Kemal::Utils.path_starts_with_slash?(path) + raise Kemal::Exceptions::InvalidPathStartException.new({{method}}, path) unless path.starts_with?("/") route_handler.add_route({{method}}.upcase, path, &block) end {% end %} + # Add a webservice handler. + # + # The block receives `HTTP::WebSocket` and `HTTP::Server::Context` as arguments. def ws(path, &block : HTTP::WebSocket, HTTP::Server::Context -> Void) - raise Kemal::Exceptions::InvalidPathStartException.new("ws", path) unless Kemal::Utils.path_starts_with_slash?(path) + raise Kemal::Exceptions::InvalidPathStartException.new("ws", path) unless path.starts_with?("/") websocket_handler.add_route path, &block end + # Add an error handler for *status_code*. + # + # The block receives `HTTP::Server::Context` and `Exception` as arguments. def error(status_code, &block : HTTP::Server::Context, Exception -> _) add_error_handler status_code, &block end @@ -35,57 +46,137 @@ class Kemal::Base # - after_all, after_get, after_post, after_put, after_patch, after_delete, after_options {% for type in ["before", "after"] %} {% for method in FILTER_METHODS %} + # Add a filter for this class that runs {{type.id}} each `{{method.id.upcase}}` request (optionally limited to a specific *path*). + # + # The block receives an `HTTP::Server::Context` as argument. def {{type.id}}_{{method.id}}(path = "*", &block : HTTP::Server::Context -> _) filter_handler.{{type.id}}({{method}}.upcase, path, &block) end {% end %} {% end %} - private def initialize_defaults - DEFAULT_HANDLERS.each do |method, path, block| - route_handler.add_route(method.upcase, path, &block) + private macro initialize_defaults + {% if CUSTOM_METHODS_REGISTRY[@type] %} + {% for handler in CUSTOM_METHODS_REGISTRY[@type][:handlers] %} + self.{{handler[0].id}}({{handler[1]}}) do |context| + {{handler[2].id}}(context) end + {% end %} - WEBSOCKET_HANDLERS.each do |path, block| - ws(path, &block) + {% for ws in CUSTOM_METHODS_REGISTRY[@type][:ws] %} + self.ws({{handler[0]}}) do |websocket, context| + {{handler[1].id}}(websocket, context) end + {% end %} - DEFAULT_ERROR_HANDLERS.each do |status_code, block| - add_error_handler status_code, &block + {% for ws in CUSTOM_METHODS_REGISTRY[@type][:error] %} + self.add_error_handler({{handler[0]}}) do |context| + {{handler[1].id}}(context) end + {% end %} - DEFAULT_FILTERS.each do |type, method, path, block| - if type == :before - filter_handler.before(method, path, &block) - else - filter_handler.after(method, path, &block) + {% for filter in CUSTOM_METHODS_REGISTRY[@type][:filters] %} + filter_handler.{{filter[0]}}({{filter[1]}}, {{filter[2]}}) do |context| + {{filter[3]}}(context) end - end + {% end %} + {% end %} end end - module ClassDSL + module MacroDSL {% for method in DSL::HTTP_METHODS %} - def {{method.id}}(path, &block : HTTP::Server::Context -> _) - DEFAULT_HANDLERS << { {{method}}, path, block } + # Define a `{{method.id.upcase}}` handler for this class. + # + # It will be initialized in every instance. + # The block receives an `HTTP::Server::Context` as argument and is scoped to the instance. + # + # Example: + # ``` + # class MyClass < Kemal::Base + # {{method.id}}("/route") do |context| + # # ... + # end + # end + # ``` + # NOTE: This macro *must* be called from class scope as it expands to a custom method definition. + macro {{method.id}}(path, &block) + \{% raise "invalid path start for {{method.id}}: path must start with \"/\"" unless path.starts_with?("/") %} + \{% method_name = "__{{method.id}}_#{path.id.gsub(/[^a-zA-Z0-9]/,"_").gsub(/__+/, "_").gsub(/\A_|_\z/, "")}_#{CUSTOM_METHODS_REGISTRY[@type][:handlers].size}" %} + def \{{method_name.id}}(\{{block.args[0].id}}) + \{{block.body}} + end + \{% CUSTOM_METHODS_REGISTRY[@type][:handlers] << { {{method}}, path, method_name } %} end {% end %} - def ws(path, &block : HTTP::WebSocket, HTTP::Server::Context -> Void) - WEBSOCKET_HANDLERS << {path, block} + # Define a webservice handler for this class. + # + # It will be initialized in every instance. + # The block receives `HTTP::WebSocket` and `HTTP::Server::Context` as arguments and is scoped to the instance. + # + # Example: + # ``` + # class MyClass < Kemal::Base + # ws("/wsroute") do |context| + # # ... + # end + # end + # ``` + # NOTE: This macro *must* be called from class scope as it expands to a custom method definition. + macro ws(path, &block) + \{% raise "invalid path start for webservice: path must start with \"/\"" unless path.starts_with?("/") %} + \{% method_name = "__ws_#{path.id.gsub(/[^a-zA-Z0-9]/,"_").gsub(/__+/, "_").gsub(/\A_|_\z/, "")}_#{CUSTOM_METHODS_REGISTRY[@type][:ws].size}" %} + def \{{method_name.id}}(\{{block.args[0].id}}, \{{block.args[1].id}}) + \{{block.body}} + end + \{% CUSTOM_METHODS_REGISTRY[@type][:ws] << { path, method_name } %} end - def error(status_code, &block : HTTP::Server::Context, Exception -> _) - DEFAULT_ERROR_HANDLERS << {status_code, block} + # Define an error handler for this class. + # + # It will be initialized in every instance. + # The block receives `HTTP::Server::Context` and `Exception` as arguments and is scoped to the instance. + # + # Example: + # ``` + # class MyClass < Kemal::Base + # error(403) do |context| + # # ... + # end + # end + # ``` + # NOTE: This macro *must* be called from class scope as it expands to a custom method definition. + macro error(status_code) + \{% method_name = "__error_#{status_code}_#{CUSTOM_METHODS_REGISTRY[@type][:error].size}" %} + def \{{method_name.id}}(\{{block.args[0].id}}) + \{{block.body}} + end + \{% CUSTOM_METHODS_REGISTRY[@type][:error] << { status_code, method_name } %} end - # All the helper methods available are: - # - before_all, before_get, before_post, before_put, before_patch, before_delete, before_options - # - after_all, after_get, after_post, after_put, after_patch, after_delete, after_options - {% for type in [:before, :after] %} + {% for type in ["before", "after"] %} {% for method in DSL::FILTER_METHODS %} - def {{type.id}}_{{method.id}}(path = "*", &block : HTTP::Server::Context -> _) - DEFAULT_FILTERS << { {{type}}, {{method}}, path, block } + # Define a filter for this class that runs {{type.id}} each `{{method.id.upcase}}` request (optionally limited to a specific *path*). + # + # The filter will be initialized in every instance of this class. + # The block receives an `HTTP::Context` as argument and is scoped to the instance. + # + # Example: + # ``` + # class MyClass < Kemal::Base + # {{type.id}}_{{method.id}}("/route") do |context| + # # ... + # end + # end + # ``` + # NOTE: This macro *must* be called from class scope as it expands to a custom method definition. + macro {{type.id}}_{{method.id}}(path = "*", &block) + \{% method_name = "__{{type.id}}_{{method.id}}_#{path.id.gsub(/[^a-zA-Z0-9]/,"_").gsub(/__+/, "_").gsub(/\A_|_\z/, "")}_#{CUSTOM_METHODS_REGISTRY[@type][:handlers].size}" %} + def \{{method_name.id}}(\{{block.args[0].id}}) + \{{block.body}} + end + \{% CUSTOM_METHODS_REGISTRY[@type][:fitlers] << { {{type}}, {{method}}, path, method_name } %} end {% end %} {% end %} From 5d65bcb3b97a8299ca69e5469c77a3626f990efa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Thu, 20 Jul 2017 15:23:32 +0200 Subject: [PATCH 08/18] Refactor spec helper classes locations and visibility --- spec/config_spec.cr | 7 +++++++ spec/dsl_helper.cr | 17 ----------------- spec/handler_spec.cr | 10 +++++----- spec/helpers_spec.cr | 12 ++++++------ spec/spec_helper.cr | 17 +++++++++++++++++ 5 files changed, 35 insertions(+), 28 deletions(-) diff --git a/spec/config_spec.cr b/spec/config_spec.cr index f6db81fe..b87c93f5 100644 --- a/spec/config_spec.cr +++ b/spec/config_spec.cr @@ -1,5 +1,12 @@ require "./spec_helper" +private class CustomTestHandler < Kemal::Handler + def call(env) + env.response << "Kemal" + call_next env + end +end + describe "Config" do it "sets default port to 3000" do config = Kemal::Config.new diff --git a/spec/dsl_helper.cr b/spec/dsl_helper.cr index 5aa05eed..39a4e914 100644 --- a/spec/dsl_helper.cr +++ b/spec/dsl_helper.cr @@ -12,23 +12,6 @@ class CustomLogHandler < Kemal::BaseLogHandler end end -class TestContextStorageType - property id - @id = 1 - - def to_s - @id - end -end - -class AnotherContextStorageType - property name - @name = "kemal-context" -end - -add_context_storage_type(TestContextStorageType) -add_context_storage_type(AnotherContextStorageType) - def create_request_and_return_io(handler, request) io = IO::Memory.new response = HTTP::Server::Response.new(io) diff --git a/spec/handler_spec.cr b/spec/handler_spec.cr index cd2a450f..1b48cccc 100644 --- a/spec/handler_spec.cr +++ b/spec/handler_spec.cr @@ -1,13 +1,13 @@ require "./spec_helper" -class CustomTestHandler < Kemal::Handler +private class CustomTestHandler < Kemal::Handler def call(env) env.response << "Kemal" call_next env end end -class OnlyHandler < Kemal::Handler +private class OnlyHandler < Kemal::Handler only ["/only"] def call(env) @@ -17,7 +17,7 @@ class OnlyHandler < Kemal::Handler end end -class ExcludeHandler < Kemal::Handler +private class ExcludeHandler < Kemal::Handler exclude ["/exclude"] def call(env) @@ -27,7 +27,7 @@ class ExcludeHandler < Kemal::Handler end end -class PostOnlyHandler < Kemal::Handler +private class PostOnlyHandler < Kemal::Handler only ["/only", "/route1", "/route2"], "POST" def call(env) @@ -37,7 +37,7 @@ class PostOnlyHandler < Kemal::Handler end end -class PostExcludeHandler < Kemal::Handler +private class PostExcludeHandler < Kemal::Handler exclude ["/exclude"], "POST" def call(env) diff --git a/spec/helpers_spec.cr b/spec/helpers_spec.cr index 4e70bfa3..b03cbc90 100644 --- a/spec/helpers_spec.cr +++ b/spec/helpers_spec.cr @@ -1,13 +1,13 @@ require "./spec_helper" -describe "Macros" do - describe "#public_folder" do - it "sets public folder" do - public_folder "/some/path/to/folder" - Kemal.config.public_folder.should eq("/some/path/to/folder") - end +private class CustomTestHandler < Kemal::Handler + def call(env) + env.response << "Kemal" + call_next env end +end +describe "Macros" do describe "#add_handler" do it "adds a custom handler" do app = Kemal::Application.new diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index bfa50b0f..f840b611 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -1,6 +1,23 @@ require "spec" require "../src/kemal" +class TestContextStorageType + property id + @id = 1 + + def to_s + @id + end +end + +class AnotherContextStorageType + property name + @name = "kemal-context" +end + +Kemal::Macros.add_context_storage_type(TestContextStorageType) +Kemal::Macros.add_context_storage_type(AnotherContextStorageType) + def call_request_on_app(app, request) io = IO::Memory.new response = HTTP::Server::Response.new(io) From 34022273b2f1308f930582a86e454eb98f642575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Thu, 20 Jul 2017 15:24:09 +0200 Subject: [PATCH 09/18] Refactor dsl_helper_spec --- spec/dsl_helpers_spec.cr | 47 ++++++++++++++++++++++++++++++++++++++++ spec/helpers_spec.cr | 12 ---------- 2 files changed, 47 insertions(+), 12 deletions(-) create mode 100644 spec/dsl_helpers_spec.cr diff --git a/spec/dsl_helpers_spec.cr b/spec/dsl_helpers_spec.cr new file mode 100644 index 00000000..338e154b --- /dev/null +++ b/spec/dsl_helpers_spec.cr @@ -0,0 +1,47 @@ +require "./dsl_helper" + +describe "Macros" do + describe "#public_folder" do + it "sets public folder" do + public_folder "/some/path/to/folder" + Kemal.config.public_folder.should eq("/some/path/to/folder") + end + end + + describe "#logging" do + it "sets logging status" do + logging false + Kemal.config.logging?.should be_false + end + + it "sets a custom logger" do + logger CustomLogHandler.new + Kemal.application.logger.should be_a(CustomLogHandler) + end + end + + describe "#gzip" do + it "adds HTTP::CompressHandler to handlers" do + gzip true + Kemal.application.setup + Kemal.application.handlers[4].should be_a(HTTP::CompressHandler) + end + end + + describe "#serve_static" do + it "should disable static file hosting" do + serve_static false + Kemal.config.serve_static.should be_false + end + + it "should disble enable gzip and dir_listing" do + serve_static({"gzip" => true, "dir_listing" => true}) + conf = Kemal.config.serve_static + conf.is_a?(Hash).should be_true + if conf.is_a?(Hash) + conf["gzip"].should be_true + conf["dir_listing"].should be_true + end + end + end +end diff --git a/spec/helpers_spec.cr b/spec/helpers_spec.cr index b03cbc90..d5139cdb 100644 --- a/spec/helpers_spec.cr +++ b/spec/helpers_spec.cr @@ -17,18 +17,6 @@ describe "Macros" do end end - describe "#logging" do - it "sets logging status" do - logging false - Kemal.config.logging?.should be_false - end - - it "sets a custom logger" do - logger CustomLogHandler.new - Kemal.application.logger.should be_a(CustomLogHandler) - end - end - describe "#halt" do it "can break block with halt macro" do app = Kemal::Base.new From dc93bb2235488dc86737d06116833901c3ca832d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Mon, 16 Oct 2017 00:14:08 +0200 Subject: [PATCH 10/18] Refactor Kemal main includes: * `require "kemal"` loads `kemal/base` and `kemal/dsl` * `require "kemal/base` loads only `kemal/base` --- src/kemal/base.cr | 2 ++ src/kemal/main.cr | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 src/kemal/main.cr diff --git a/src/kemal/base.cr b/src/kemal/base.cr index 94ac3fcd..db3b0787 100644 --- a/src/kemal/base.cr +++ b/src/kemal/base.cr @@ -116,3 +116,5 @@ class Kemal::Base logger.write "#{message}\n" end end + +require "./main" diff --git a/src/kemal/main.cr b/src/kemal/main.cr new file mode 100644 index 00000000..c0b3bcac --- /dev/null +++ b/src/kemal/main.cr @@ -0,0 +1,58 @@ +require "http" +require "json" +require "uri" +require "tempfile" +require "./application" +require "./base_log_handler" +require "./cli" +require "./exception_handler" +require "./log_handler" +require "./config" +require "./exceptions" +require "./file_upload" +require "./filter_handler" +require "./handler" +require "./init_handler" +require "./null_log_handler" +require "./param_parser" +require "./response" +require "./route" +require "./route_handler" +require "./ssl" +require "./static_file_handler" +require "./websocket" +require "./websocket_handler" +require "./ext/*" +require "./helpers/*" + +module Kemal + def self.application + @@application ||= Kemal::Application.new + end + + def self.config + application.config + end + + # Overload of `self.run` with the default startup logging. + def self.run(port : Int32? = nil) + CLI.new(config) + + application.run(port) + end + + # The command to run a `Kemal` application. + # The port can be given to `#run` but is optional. + # If not given Kemal will use `Kemal::Config#port` + def self.run(port : Int32? = nil) + CLI.new(config) + + application.run(port) do |application| + yield application + end + end + + def self.stop + application.stop + end +end From 72bcac6dd56adad9a008cc7b366515578ca34b96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Wed, 18 Oct 2017 19:48:01 +0200 Subject: [PATCH 11/18] Add app reference to the handlers which are bound to the app --- spec/context_spec.cr | 1 - spec/dsl_helper.cr | 4 ++-- spec/exception_handler_spec.cr | 14 +++++--------- spec/handler_spec.cr | 5 ++--- spec/middleware/filters_spec.cr | 14 +++++++------- spec/websocket_handler_spec.cr | 16 +++++++--------- src/kemal/base.cr | 9 +++++---- src/kemal/exception_handler.cr | 8 ++++---- src/kemal/filter_handler.cr | 10 ++++++---- src/kemal/helpers/file_helpers.cr | 2 +- src/kemal/route_handler.cr | 14 ++++++++++++-- src/kemal/websocket_handler.cr | 10 +++++++--- 12 files changed, 58 insertions(+), 49 deletions(-) diff --git a/spec/context_spec.cr b/spec/context_spec.cr index ea1dbb65..1bfb9f7c 100644 --- a/spec/context_spec.cr +++ b/spec/context_spec.cr @@ -64,7 +64,6 @@ describe "Context" do io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) - context.app = app app.filter_handler.call(context) app.route_handler.call(context) context.store["key"].should eq "value" diff --git a/spec/dsl_helper.cr b/spec/dsl_helper.cr index 39a4e914..a636c4ca 100644 --- a/spec/dsl_helper.cr +++ b/spec/dsl_helper.cr @@ -12,11 +12,11 @@ class CustomLogHandler < Kemal::BaseLogHandler end end -def create_request_and_return_io(handler, request) +def create_request_and_return_io(handler, request, app = Kemal.application) io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) - context.app = Kemal.application + context.app = app handler.call(context) response.close io.rewind diff --git a/spec/exception_handler_spec.cr b/spec/exception_handler_spec.cr index 95df2889..dc72b943 100644 --- a/spec/exception_handler_spec.cr +++ b/spec/exception_handler_spec.cr @@ -26,9 +26,8 @@ describe "Kemal::ExceptionHandler" do app.get "/" do |env| env.response.status_code = 403 end - context.app = app subject = Kemal::ExceptionHandler.new(app) - subject.next = Kemal::RouteHandler.new + subject.next = app.route_handler subject.call(context) response.close io.rewind @@ -50,9 +49,8 @@ describe "Kemal::ExceptionHandler" do app.get "/" do |env| env.response.status_code = 500 end - context.app = app subject = Kemal::ExceptionHandler.new(app) - subject.next = Kemal::RouteHandler.new + subject.next = app.route_handler subject.call(context) response.close io.rewind @@ -75,9 +73,8 @@ describe "Kemal::ExceptionHandler" do env.response.content_type = "application/json" env.response.status_code = 500 end - context.app = app subject = Kemal::ExceptionHandler.new(app) - subject.next = Kemal::RouteHandler.new + subject.next = app.route_handler subject.call(context) response.close io.rewind @@ -100,9 +97,8 @@ describe "Kemal::ExceptionHandler" do env.response.content_type = "application/json" env.response.status_code = 500 end - context.app = app - subject = Kemal::ExceptionHandler.new(Kemal::Base.new) - subject.next = Kemal::RouteHandler.new + subject = Kemal::ExceptionHandler.new(app) + subject.next = app.route_handler subject.call(context) response.close io.rewind diff --git a/spec/handler_spec.cr b/spec/handler_spec.cr index 1b48cccc..85e20017 100644 --- a/spec/handler_spec.cr +++ b/spec/handler_spec.cr @@ -69,8 +69,8 @@ end describe "Handler" do it "adds custom handler before before_*" do - filter_middleware = Kemal::FilterHandler.new - Kemal.application.add_filter_handler filter_middleware + app = Kemal::Base.new + filter_middleware = Kemal::FilterHandler.new(app) filter_middleware._add_route_filter("GET", "/", :before) do |env| env.response << " is" end @@ -78,7 +78,6 @@ describe "Handler" do filter_middleware._add_route_filter("GET", "/", :before) do |env| env.response << " so" end - app = Kemal::Base.new app.add_filter_handler filter_middleware app.add_handler CustomTestHandler.new diff --git a/spec/middleware/filters_spec.cr b/spec/middleware/filters_spec.cr index 65f1d48e..6a5596ac 100644 --- a/spec/middleware/filters_spec.cr +++ b/spec/middleware/filters_spec.cr @@ -5,7 +5,7 @@ describe "Kemal::FilterHandler" do test_filter = FilterTest.new test_filter.modified = "false" - filter_middleware = Kemal::FilterHandler.new + filter_middleware = Kemal::FilterHandler.new(Kemal.application) filter_middleware._add_route_filter("GET", "/greetings", :before) { test_filter.modified = "true" } kemal = Kemal.application.route_handler @@ -23,7 +23,7 @@ describe "Kemal::FilterHandler" do test_filter = FilterTest.new test_filter.modified = "false" - filter_middleware = Kemal::FilterHandler.new + filter_middleware = Kemal::FilterHandler.new(Kemal.application) filter_middleware._add_route_filter("GET", "/greetings", :before) { test_filter.modified = test_filter.modified == "true" ? "false" : "true" } kemal = Kemal.application.route_handler @@ -49,7 +49,7 @@ describe "Kemal::FilterHandler" do test_filter = FilterTest.new test_filter.modified = "false" - filter_middleware = Kemal::FilterHandler.new + filter_middleware = Kemal::FilterHandler.new(Kemal.application) filter_middleware._add_route_filter("ALL", "/greetings", :before) { test_filter.modified = test_filter.modified == "true" ? "false" : "true" } filter_middleware._add_route_filter("GET", "/greetings", :before) { test_filter.modified = test_filter.modified == "true" ? "false" : "true" } filter_middleware._add_route_filter("POST", "/greetings", :before) { test_filter.modified = test_filter.modified == "true" ? "false" : "true" } @@ -77,7 +77,7 @@ describe "Kemal::FilterHandler" do test_filter = FilterTest.new test_filter.modified = "false" - filter_middleware = Kemal::FilterHandler.new + filter_middleware = Kemal::FilterHandler.new(Kemal.application) filter_middleware._add_route_filter("GET", "/greetings", :after) { test_filter.modified = "true" } kemal = Kemal.application.route_handler @@ -95,7 +95,7 @@ describe "Kemal::FilterHandler" do test_filter = FilterTest.new test_filter.modified = "false" - filter_middleware = Kemal::FilterHandler.new + filter_middleware = Kemal::FilterHandler.new(Kemal.application) filter_middleware._add_route_filter("GET", "/greetings", :after) { test_filter.modified = test_filter.modified == "true" ? "false" : "true" } kemal = Kemal.application.route_handler @@ -121,7 +121,7 @@ describe "Kemal::FilterHandler" do test_filter = FilterTest.new test_filter.modified = "false" - filter_middleware = Kemal::FilterHandler.new + filter_middleware = Kemal::FilterHandler.new(Kemal.application) filter_middleware._add_route_filter("ALL", "/greetings", :after) { test_filter.modified = test_filter.modified == "true" ? "false" : "true" } filter_middleware._add_route_filter("GET", "/greetings", :after) { test_filter.modified = test_filter.modified == "true" ? "false" : "true" } filter_middleware._add_route_filter("POST", "/greetings", :after) { test_filter.modified = test_filter.modified == "true" ? "false" : "true" } @@ -152,7 +152,7 @@ describe "Kemal::FilterHandler" do test_filter_third = FilterTest.new test_filter_third.modified = "false" - filter_middleware = Kemal::FilterHandler.new + filter_middleware = Kemal::FilterHandler.new(Kemal.application) filter_middleware._add_route_filter("ALL", "/greetings", :before) { test_filter.modified = test_filter.modified == "true" ? "false" : "true" } filter_middleware._add_route_filter("ALL", "/greetings", :before) { test_filter_second.modified = test_filter_second.modified == "true" ? "false" : "true" } filter_middleware._add_route_filter("ALL", "/greetings", :before) { test_filter_third.modified = test_filter_third.modified == "true" ? "false" : "true" } diff --git a/spec/websocket_handler_spec.cr b/spec/websocket_handler_spec.cr index cbcb34e4..e89dcc52 100644 --- a/spec/websocket_handler_spec.cr +++ b/spec/websocket_handler_spec.cr @@ -1,6 +1,6 @@ require "./spec_helper" -private def create_ws_request_and_return_io(handler, request, app) +private def create_ws_request_and_return_io(handler, request, app = Kemal.application) io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) @@ -16,8 +16,8 @@ end describe "Kemal::WebSocketHandler" do it "doesn't match on wrong route" do app = Kemal::Base.new - handler = Kemal::WebSocketHandler.new - handler.next = Kemal::RouteHandler.new + handler = app.websocket_handler + handler.next = app.route_handler app.ws "/" { } headers = HTTP::Headers{ "Upgrade" => "websocket", @@ -28,7 +28,6 @@ describe "Kemal::WebSocketHandler" do io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) - context.app = app expect_raises(Kemal::Exceptions::RouteNotFound) do handler.call context @@ -37,7 +36,7 @@ describe "Kemal::WebSocketHandler" do it "matches on given route" do app = Kemal::Base.new - handler = Kemal::WebSocketHandler.new + handler = app.websocket_handler app.ws "/" { |socket, context| socket.send("Match") } app.ws "/no_match" { |socket, context| socket.send "No Match" } headers = HTTP::Headers{ @@ -54,7 +53,7 @@ describe "Kemal::WebSocketHandler" do it "fetches named url parameters" do app = Kemal::Base.new - handler = Kemal::WebSocketHandler.new + handler = app.websocket_handler app.ws "/:id" { |s, c| c.params.url["id"] } headers = HTTP::Headers{ "Upgrade" => "websocket", @@ -69,15 +68,14 @@ describe "Kemal::WebSocketHandler" do it "matches correct verb" do app = Kemal::Base.new - handler = Kemal::WebSocketHandler.new - handler.next = Kemal::RouteHandler.new + handler = app.websocket_handler + handler.next = app.route_handler app.ws "/" { } app.get "/" { "get" } request = HTTP::Request.new("GET", "/") io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) - context.app = app handler.call(context) response.close io.rewind diff --git a/src/kemal/base.cr b/src/kemal/base.cr index db3b0787..a4b205d4 100644 --- a/src/kemal/base.cr +++ b/src/kemal/base.cr @@ -15,18 +15,19 @@ class Kemal::Base include Base::Builder # :nodoc: - getter route_handler = Kemal::RouteHandler.new + # TODO: These ivars are initialized in the constructor, but their values depend on `self`. + getter! route_handler : RouteHandler? # :nodoc: - getter filter_handler = Kemal::FilterHandler.new + getter! filter_handler : FilterHandler? # :nodoc: - getter websocket_handler = Kemal::WebSocketHandler.new + getter! websocket_handler : WebSocketHandler? getter handlers = [] of HTTP::Handler getter error_handlers = {} of Int32 => HTTP::Server::Context, Exception -> String getter config : Config - property! logger : Kemal::BaseLogHandler + property! logger : BaseLogHandler property! server : HTTP::Server property? running = false diff --git a/src/kemal/exception_handler.cr b/src/kemal/exception_handler.cr index 32da24b6..98a8643a 100644 --- a/src/kemal/exception_handler.cr +++ b/src/kemal/exception_handler.cr @@ -19,16 +19,16 @@ module Kemal call_exception_with_status_code(context, ex, context.response.status_code) rescue ex : Exception log("Exception: #{ex.inspect_with_backtrace}") - return call_exception_with_status_code(context, ex, 500) if context.app.error_handlers.has_key?(500) - verbosity = context.app.config.env == "production" ? false : true + return call_exception_with_status_code(context, ex, 500) if app.error_handlers.has_key?(500) + verbosity = app.config.env == "production" ? false : true return app.render_500(context, ex.inspect_with_backtrace, verbosity) end end private def call_exception_with_status_code(context, exception, status_code) - if context.app.error_handlers.has_key?(status_code) + if !app.error_handlers.empty? && app.error_handlers.has_key?(status_code) context.response.content_type = "text/html" unless context.response.headers.has_key?("Content-Type") - context.response.print context.app.error_handlers[status_code].call(context, exception) + context.response.print app.error_handlers[status_code].call(context, exception) context.response.status_code = status_code context end diff --git a/src/kemal/filter_handler.cr b/src/kemal/filter_handler.cr index a1357448..f6c58941 100644 --- a/src/kemal/filter_handler.cr +++ b/src/kemal/filter_handler.cr @@ -3,17 +3,19 @@ module Kemal class FilterHandler include HTTP::Handler + getter app : Kemal::Base + # This middleware is lazily instantiated and added to the handlers as soon as a call to `after_X` or `before_X` is made. - def initialize + def initialize(@app) @tree = Radix::Tree(Array(FilterBlock)).new end - # The call order of the filters is `before_all -> before_x -> X -> after_x -> after_all`. + # The call order of the filters is before_all -> before_x -> X -> after_x -> after_all def call(context : HTTP::Server::Context) - return call_next(context) unless context.route_found? + return call_next(context) unless app.route_handler.route_defined?(context.request) call_block_for_path_type("ALL", context.request.path, :before, context) call_block_for_path_type(context.request.override_method, context.request.path, :before, context) - if context.app.error_handlers.has_key?(context.response.status_code) + if !app.error_handlers.empty? && app.error_handlers.has_key?(context.response.status_code) raise Kemal::Exceptions::CustomException.new(context) end call_next(context) diff --git a/src/kemal/helpers/file_helpers.cr b/src/kemal/helpers/file_helpers.cr index 375439cc..b0a5636c 100644 --- a/src/kemal/helpers/file_helpers.cr +++ b/src/kemal/helpers/file_helpers.cr @@ -49,7 +49,7 @@ module Kemal::FileHelpers end def send_file(env, path : String, mime_type : String? = nil) - send_file(env, path, env.app.config, mime_type) + send_file(env, path, config, mime_type) end private def multipart(file, env : HTTP::Server::Context) diff --git a/src/kemal/route_handler.cr b/src/kemal/route_handler.cr index fd2f672b..ca7f3c8a 100644 --- a/src/kemal/route_handler.cr +++ b/src/kemal/route_handler.cr @@ -8,7 +8,9 @@ module Kemal CACHED_ROUTES_LIMIT = 1024 property routes, cached_routes - def initialize + getter app : Kemal::Base + + def initialize(@app) @routes = Radix::Tree(Route).new @cached_routes = Hash(String, Radix::Result(Route)).new end @@ -42,12 +44,20 @@ module Kemal route end + def lookup_route(request) + lookup_route request.override_method.as(String), request.path + end + + def route_defined?(request) + lookup_route(request).found? + end + # Processes the route if it's a match. Otherwise renders 404. private def process_request(context) raise Kemal::Exceptions::RouteNotFound.new(context) unless context.route_found? content = context.route.handler.call(context) - if context.app.error_handlers.has_key?(context.response.status_code) + if !app.error_handlers.empty? && app.error_handlers.has_key?(context.response.status_code) raise Kemal::Exceptions::CustomException.new(context) end diff --git a/src/kemal/websocket_handler.cr b/src/kemal/websocket_handler.cr index da81b2fa..a2515d35 100644 --- a/src/kemal/websocket_handler.cr +++ b/src/kemal/websocket_handler.cr @@ -4,13 +4,17 @@ module Kemal property routes - def initialize + getter app : Kemal::Base + + def initialize(@app) @routes = Radix::Tree(WebSocket).new end def call(context : HTTP::Server::Context) - return call_next(context) unless context.ws_route_found? && websocket_upgrade_request?(context) - content = context.websocket.call(context) + route = lookup_ws_route(context.request.path) + return call_next(context) unless route.found? && websocket_upgrade_request?(context) + context.request.url_params ||= route.params + content = route.payload.call(context) context.response.print(content) context end From 5917af3f14cd1c24a0f491a2a55b37974dfeba49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Wed, 18 Oct 2017 17:53:44 +0200 Subject: [PATCH 12/18] Remove Context#app to reduce GC load (experimental) --- spec/dsl_helper.cr | 3 +-- spec/param_parser_spec.cr | 8 ++++---- spec/websocket_handler_spec.cr | 9 +++------ src/kemal/ext/context.cr | 2 -- src/kemal/init_handler.cr | 1 - 5 files changed, 8 insertions(+), 15 deletions(-) diff --git a/spec/dsl_helper.cr b/spec/dsl_helper.cr index a636c4ca..fdd6169c 100644 --- a/spec/dsl_helper.cr +++ b/spec/dsl_helper.cr @@ -12,11 +12,10 @@ class CustomLogHandler < Kemal::BaseLogHandler end end -def create_request_and_return_io(handler, request, app = Kemal.application) +def create_request_and_return_io(handler, request) io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) - context.app = app handler.call(context) response.close io.rewind diff --git a/spec/param_parser_spec.cr b/spec/param_parser_spec.cr index 07d76043..2eeb5525 100644 --- a/spec/param_parser_spec.cr +++ b/spec/param_parser_spec.cr @@ -22,14 +22,14 @@ describe "ParamParser" do end it "parses url params" do - kemal = Kemal.application.route_handler - kemal.add_route "POST", "/hello/:hasan" do |env| + route_handler = Kemal.application.route_handler + route_handler.add_route "POST", "/hello/:hasan" do |env| "hello #{env.params.url["hasan"]}" end request = HTTP::Request.new("POST", "/hello/cemal") # Radix tree MUST be run to parse url params. - context = create_request_and_return_io_and_context(kemal, request)[1] - url_params = Kemal::ParamParser.new(request, context.route_lookup.params).url + io_with_context = create_request_and_return_io(route_handler, request) + url_params = Kemal::ParamParser.new(request).url url_params["hasan"].should eq "cemal" end diff --git a/spec/websocket_handler_spec.cr b/spec/websocket_handler_spec.cr index e89dcc52..736d477d 100644 --- a/spec/websocket_handler_spec.cr +++ b/spec/websocket_handler_spec.cr @@ -1,10 +1,9 @@ require "./spec_helper" -private def create_ws_request_and_return_io(handler, request, app = Kemal.application) +private def create_ws_request_and_return_io(handler, request) io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) - context.app = app begin handler.call context rescue IO::Error @@ -36,7 +35,6 @@ describe "Kemal::WebSocketHandler" do it "matches on given route" do app = Kemal::Base.new - handler = app.websocket_handler app.ws "/" { |socket, context| socket.send("Match") } app.ws "/no_match" { |socket, context| socket.send "No Match" } headers = HTTP::Headers{ @@ -47,13 +45,12 @@ describe "Kemal::WebSocketHandler" do } request = HTTP::Request.new("GET", "/", headers) - io_with_context = create_ws_request_and_return_io(handler, request, app) + io_with_context = create_ws_request_and_return_io(app.websocket_handler, request) io_with_context.to_s.should eq("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-Websocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n\x81\u0005Match") end it "fetches named url parameters" do app = Kemal::Base.new - handler = app.websocket_handler app.ws "/:id" { |s, c| c.params.url["id"] } headers = HTTP::Headers{ "Upgrade" => "websocket", @@ -62,7 +59,7 @@ describe "Kemal::WebSocketHandler" do "Sec-WebSocket-Version" => "13", } request = HTTP::Request.new("GET", "/1234", headers) - io_with_context = create_ws_request_and_return_io(handler, request, app) + io_with_context = create_ws_request_and_return_io(app.websocket_handler, request) io_with_context.to_s.should eq("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-Websocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n") end diff --git a/src/kemal/ext/context.cr b/src/kemal/ext/context.cr index d4d02b34..ac680310 100644 --- a/src/kemal/ext/context.cr +++ b/src/kemal/ext/context.cr @@ -8,8 +8,6 @@ class HTTP::Server # :nodoc: STORE_MAPPINGS = [Nil, String, Int32, Int64, Float64, Bool] - property! app : Kemal::Base - macro finished alias StoreTypes = Union({{ *STORE_MAPPINGS }}) @store = {} of String => StoreTypes diff --git a/src/kemal/init_handler.cr b/src/kemal/init_handler.cr index 81752236..7674b948 100644 --- a/src/kemal/init_handler.cr +++ b/src/kemal/init_handler.cr @@ -12,7 +12,6 @@ module Kemal def call(context : HTTP::Server::Context) context.response.headers.add "X-Powered-By", "Kemal" if Kemal.config.powered_by_header context.response.content_type = "text/html" unless context.response.headers.has_key?("Content-Type") - context.app = app call_next context end end From 6d12d111f47d00a44d2527ccb4dc7bd71d8b4ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Wed, 18 Oct 2017 17:57:23 +0200 Subject: [PATCH 13/18] refactor Request#param_parser --- src/kemal/ext/request.cr | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/kemal/ext/request.cr diff --git a/src/kemal/ext/request.cr b/src/kemal/ext/request.cr new file mode 100644 index 00000000..035e7361 --- /dev/null +++ b/src/kemal/ext/request.cr @@ -0,0 +1,36 @@ +class HTTP::Request + property override_method + property url_params : Hash(String, String)? + @param_parser : Kemal::ParamParser? + + def override_method + @override_method ||= check_for_method_override! + end + + def content_type + @headers["Content-Type"]? + end + + def param_parser + @param_parser ||= Kemal::ParamParser.new(self) + end + + # Checks if method contained in _method param is valid one + def self.override_method_valid?(override_method : String) + return false unless override_method.is_a?(String) + override_method = override_method.upcase + override_method == "PUT" || override_method == "PATCH" || override_method == "DELETE" + end + + # Checks if request params contain _method param to override request incoming method + private def check_for_method_override! + @override_method = @method + if @method == "POST" + params = param_parser.body + if params.has_key?("_method") && HTTP::Request.override_method_valid?(params["_method"]) + @override_method = params["_method"] + end + end + @override_method + end +end From e6dc494f7eaff20885777d020110cefec9b801bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Wed, 18 Oct 2017 17:57:49 +0200 Subject: [PATCH 14/18] crystal tool format --- spec/websocket_handler_spec.cr | 4 ++-- src/kemal/base.cr | 2 +- src/kemal/helpers/file_helpers.cr | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/spec/websocket_handler_spec.cr b/spec/websocket_handler_spec.cr index 736d477d..285ad85b 100644 --- a/spec/websocket_handler_spec.cr +++ b/spec/websocket_handler_spec.cr @@ -65,8 +65,8 @@ describe "Kemal::WebSocketHandler" do it "matches correct verb" do app = Kemal::Base.new - handler = app.websocket_handler - handler.next = app.route_handler + handler = app.websocket_handler + handler.next = app.route_handler app.ws "/" { } app.get "/" { "get" } request = HTTP::Request.new("GET", "/") diff --git a/src/kemal/base.cr b/src/kemal/base.cr index a4b205d4..1b9872b1 100644 --- a/src/kemal/base.cr +++ b/src/kemal/base.cr @@ -68,7 +68,7 @@ class Kemal::Base end def self.run(port : Int32? = nil) - run(port) { } + run(port) { } end # DEPRECATED: This method should be replaced with `#running?` diff --git a/src/kemal/helpers/file_helpers.cr b/src/kemal/helpers/file_helpers.cr index b0a5636c..cdc2b65c 100644 --- a/src/kemal/helpers/file_helpers.cr +++ b/src/kemal/helpers/file_helpers.cr @@ -1,5 +1,6 @@ module Kemal::FileHelpers extend self + # Send a file with given path and base the mime-type on the file extension # or default `application/octet-stream` mime_type. # From 5f1ce1e0fc514d9195862c72cdfb9fc1e1cd0817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Fri, 27 Oct 2017 18:15:29 +0200 Subject: [PATCH 15/18] Move signal trapping to Kemal::Application --- src/kemal/application.cr | 8 +++++++- src/kemal/base.cr | 7 ------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/kemal/application.cr b/src/kemal/application.cr index 5aca0037..0222d5b5 100644 --- a/src/kemal/application.cr +++ b/src/kemal/application.cr @@ -19,8 +19,14 @@ class Kemal::Application < Kemal::Base end end - # Test environment doesn't need to have signal trap, built-in images, and logging. + # Test environment doesn't need to have signal trap and built-in images. unless @config.env == "test" + Signal::INT.trap do + log "Kemal is going to take a rest!" if @config.shutdown_message? + stop if running? + exit + end + # This route serves the built-in images for not_found and exceptions. self.get "/__kemal__/:image" do |env| image = env.params.url["image"] diff --git a/src/kemal/base.cr b/src/kemal/base.cr index 1b9872b1..e1451012 100644 --- a/src/kemal/base.cr +++ b/src/kemal/base.cr @@ -77,13 +77,6 @@ class Kemal::Base end private def prepare_for_server_start - unless @config.env == "test" - Signal::INT.trap do - log "Kemal is going to take a rest!" if @config.shutdown_message? - stop if running? - exit - end - end end private def start_server(port) From 4fefd9cb5cbc2fa9058f4d338ebd501a72e70879 Mon Sep 17 00:00:00 2001 From: sdogruyol Date: Mon, 17 Dec 2018 18:58:02 +0300 Subject: [PATCH 16/18] WIP --- src/kemal/base.cr | 14 ++++++++++---- src/kemal/helpers/file_helpers.cr | 2 +- src/kemal/main.cr | 1 - 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/kemal/base.cr b/src/kemal/base.cr index e1451012..bbb073a5 100644 --- a/src/kemal/base.cr +++ b/src/kemal/base.cr @@ -80,12 +80,18 @@ class Kemal::Base end private def start_server(port) - @server = server = HTTP::Server.new(@config.host_binding, port || @config.port, @handlers) - {% if !flag?(:without_openssl) %} - server.tls = config.ssl + @server = server = HTTP::Server.new(@handlers) + + {% if flag?(:without_openssl) %} + server.bind_tcp(@config.host_binding, port || @config.port) + {% else %} + if ssl = config.ssl + server.bind_tls(@config.host_binding, port || @config.port, ssl) + else + server.bind_tcp(@config.host_binding, port || @config.port) + end {% end %} - server.bind @running = true yield diff --git a/src/kemal/helpers/file_helpers.cr b/src/kemal/helpers/file_helpers.cr index cdc2b65c..ade82880 100644 --- a/src/kemal/helpers/file_helpers.cr +++ b/src/kemal/helpers/file_helpers.cr @@ -22,7 +22,7 @@ module Kemal::FileHelpers 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) + filestat = File.info(file_path) config.static_headers.try(&.call(env.response, file_path, filestat)) gzip = config.serve_static?("gzip") diff --git a/src/kemal/main.cr b/src/kemal/main.cr index c0b3bcac..4a66d1a5 100644 --- a/src/kemal/main.cr +++ b/src/kemal/main.cr @@ -1,7 +1,6 @@ require "http" require "json" require "uri" -require "tempfile" require "./application" require "./base_log_handler" require "./cli" From 678adaa375b3d9c813e77d6ea4c89c8a6a14a5a7 Mon Sep 17 00:00:00 2001 From: sdogruyol Date: Mon, 17 Dec 2018 22:20:21 +0300 Subject: [PATCH 17/18] Rebased to master and update to Crystal 0.27.0 --- spec/all_spec.cr | 2 +- spec/init_handler_spec.cr | 3 +- spec/middleware/filters_spec.cr | 58 +++++++++++++------------- spec/param_parser_spec.cr | 4 +- spec/websocket_handler_spec.cr | 4 +- src/kemal/dsl/templates.cr | 4 +- src/kemal/exception_handler.cr | 2 +- src/kemal/ext/context.cr | 28 +------------ src/kemal/helpers/templates.cr | 72 +++++---------------------------- src/kemal/main.cr | 11 +++-- src/kemal/param_parser.cr | 11 ++++- src/kemal/route_handler.cr | 8 +++- 12 files changed, 71 insertions(+), 136 deletions(-) diff --git a/spec/all_spec.cr b/spec/all_spec.cr index 938dadf6..1cc44147 100644 --- a/spec/all_spec.cr +++ b/spec/all_spec.cr @@ -1 +1 @@ -require "./*" +# require "./*" diff --git a/spec/init_handler_spec.cr b/spec/init_handler_spec.cr index 43fac041..09e874cd 100644 --- a/spec/init_handler_spec.cr +++ b/spec/init_handler_spec.cr @@ -28,7 +28,8 @@ describe "Kemal::InitHandler" do io = IO::Memory.new response = HTTP::Server::Response.new(io) context = HTTP::Server::Context.new(request, response) - Kemal::InitHandler::INSTANCE.call(context) + init_handler = Kemal::InitHandler.new(Kemal::Base.new) + init_handler.call(context) context.response.headers["X-Powered-By"]?.should be_nil end end diff --git a/spec/middleware/filters_spec.cr b/spec/middleware/filters_spec.cr index 6a5596ac..e6421559 100644 --- a/spec/middleware/filters_spec.cr +++ b/spec/middleware/filters_spec.cr @@ -1,4 +1,4 @@ -require "../spec_helper" +require "../dsl_helper" describe "Kemal::FilterHandler" do it "executes code before home request" do @@ -13,8 +13,8 @@ describe "Kemal::FilterHandler" do test_filter.modified.should eq("false") request = HTTP::Request.new("GET", "/greetings") - create_request_and_return_io_and_context(filter_middleware, request) - io_with_context = create_request_and_return_io_and_context(kemal, request)[0] + create_request_and_return_io(filter_middleware, request) + io_with_context = create_request_and_return_io(kemal, request) client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("true") end @@ -33,14 +33,14 @@ describe "Kemal::FilterHandler" do test_filter.modified.should eq("false") request = HTTP::Request.new("GET", "/greetings") - create_request_and_return_io_and_context(filter_middleware, request) - io_with_context = create_request_and_return_io_and_context(kemal, request)[0] + create_request_and_return_io(filter_middleware, request) + io_with_context = create_request_and_return_io(kemal, request) client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("true") request = HTTP::Request.new("POST", "/greetings") - create_request_and_return_io_and_context(filter_middleware, request) - io_with_context = create_request_and_return_io_and_context(kemal, request)[0] + create_request_and_return_io(filter_middleware, request) + io_with_context = create_request_and_return_io(kemal, request) client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("true") end @@ -61,14 +61,14 @@ describe "Kemal::FilterHandler" do test_filter.modified.should eq("false") request = HTTP::Request.new("GET", "/greetings") - create_request_and_return_io_and_context(filter_middleware, request) - io_with_context = create_request_and_return_io_and_context(kemal, request)[0] + create_request_and_return_io(filter_middleware, request) + io_with_context = create_request_and_return_io(kemal, request) client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("false") request = HTTP::Request.new("POST", "/greetings") - create_request_and_return_io_and_context(filter_middleware, request) - io_with_context = create_request_and_return_io_and_context(kemal, request)[0] + create_request_and_return_io(filter_middleware, request) + io_with_context = create_request_and_return_io(kemal, request) client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("false") end @@ -85,14 +85,14 @@ describe "Kemal::FilterHandler" do test_filter.modified.should eq("false") request = HTTP::Request.new("GET", "/greetings") - create_request_and_return_io_and_context(filter_middleware, request) - io_with_context = create_request_and_return_io_and_context(kemal, request)[0] + create_request_and_return_io(filter_middleware, request) + io_with_context = create_request_and_return_io(kemal, request) client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("true") end it "executes code after GET home request but not POST home request" do - test_filter = FilterTest.new + test_filter = FilterTest.new test_filter.modified = "false" filter_middleware = Kemal::FilterHandler.new(Kemal.application) @@ -105,20 +105,20 @@ describe "Kemal::FilterHandler" do test_filter.modified.should eq("false") request = HTTP::Request.new("GET", "/greetings") - create_request_and_return_io_and_context(filter_middleware, request) - io_with_context = create_request_and_return_io_and_context(kemal, request)[0] + create_request_and_return_io(filter_middleware, request) + io_with_context = create_request_and_return_io(kemal, request) client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("true") request = HTTP::Request.new("POST", "/greetings") - create_request_and_return_io_and_context(filter_middleware, request) - io_with_context = create_request_and_return_io_and_context(kemal, request)[0] + create_request_and_return_io(filter_middleware, request) + io_with_context = create_request_and_return_io(kemal, request) client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("true") end it "executes code after all GET/POST home request" do - test_filter = FilterTest.new + test_filter = FilterTest.new test_filter.modified = "false" filter_middleware = Kemal::FilterHandler.new(Kemal.application) @@ -132,14 +132,14 @@ describe "Kemal::FilterHandler" do test_filter.modified.should eq("false") request = HTTP::Request.new("GET", "/greetings") - create_request_and_return_io_and_context(filter_middleware, request) - io_with_context = create_request_and_return_io_and_context(kemal, request)[0] + create_request_and_return_io(filter_middleware, request) + io_with_context = create_request_and_return_io(kemal, request) client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("false") request = HTTP::Request.new("POST", "/greetings") - create_request_and_return_io_and_context(filter_middleware, request) - io_with_context = create_request_and_return_io_and_context(kemal, request)[0] + create_request_and_return_io(filter_middleware, request) + io_with_context = create_request_and_return_io(kemal, request) client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("false") end @@ -166,20 +166,20 @@ describe "Kemal::FilterHandler" do test_filter_second.modified.should eq("false") test_filter_third.modified.should eq("false") request = HTTP::Request.new("GET", "/greetings") - create_request_and_return_io_and_context(filter_middleware, request) - io_with_context = create_request_and_return_io_and_context(kemal, request)[0] + create_request_and_return_io(filter_middleware, request) + io_with_context = create_request_and_return_io(kemal, request) client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("true") request = HTTP::Request.new("POST", "/greetings") - create_request_and_return_io_and_context(filter_middleware, request) - io_with_context = create_request_and_return_io_and_context(kemal, request)[0] + create_request_and_return_io(filter_middleware, request) + io_with_context = create_request_and_return_io(kemal, request) client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("false") request = HTTP::Request.new("PUT", "/greetings") - create_request_and_return_io_and_context(filter_middleware, request) - io_with_context = create_request_and_return_io_and_context(kemal, request)[0] + create_request_and_return_io(filter_middleware, request) + io_with_context = create_request_and_return_io(kemal, request) client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false) client_response.body.should eq("true") end diff --git a/spec/param_parser_spec.cr b/spec/param_parser_spec.cr index 2eeb5525..0cba4ccd 100644 --- a/spec/param_parser_spec.cr +++ b/spec/param_parser_spec.cr @@ -43,8 +43,8 @@ describe "ParamParser" do end request = HTTP::Request.new("POST", "/hello/sam%2Bspec%40gmail.com/%2419.99/a%C3%B1o") # Radix tree MUST be run to parse url params. - context = create_request_and_return_io_and_context(kemal, request)[1] - url_params = Kemal::ParamParser.new(request, context.route_lookup.params).url + io_with_context = create_request_and_return_io(kemal, request) + url_params = Kemal::ParamParser.new(request).url url_params["email"].should eq "sam+spec@gmail.com" url_params["money"].should eq "$19.99" url_params["spanish"].should eq "año" diff --git a/spec/websocket_handler_spec.cr b/spec/websocket_handler_spec.cr index 285ad85b..976fbc7e 100644 --- a/spec/websocket_handler_spec.cr +++ b/spec/websocket_handler_spec.cr @@ -46,7 +46,7 @@ describe "Kemal::WebSocketHandler" do request = HTTP::Request.new("GET", "/", headers) io_with_context = create_ws_request_and_return_io(app.websocket_handler, request) - io_with_context.to_s.should eq("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-Websocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n\x81\u0005Match") + io_with_context.to_s.should eq("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n\x81\u0005Match") end it "fetches named url parameters" do @@ -60,7 +60,7 @@ describe "Kemal::WebSocketHandler" do } request = HTTP::Request.new("GET", "/1234", headers) io_with_context = create_ws_request_and_return_io(app.websocket_handler, request) - io_with_context.to_s.should eq("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-Websocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n") + io_with_context.to_s.should eq("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n") end it "matches correct verb" do diff --git a/src/kemal/dsl/templates.cr b/src/kemal/dsl/templates.cr index 90c6e9dd..ba865a16 100644 --- a/src/kemal/dsl/templates.cr +++ b/src/kemal/dsl/templates.cr @@ -2,6 +2,6 @@ def render_404 Kemal.application.render_404 end -def render_500(context, backtrace, verbosity) - Kemal.application.render_500(context, backtrace, verbosity) +def render_500(context, exception, verbosity) + Kemal.application.render_500(context, exception, verbosity) end diff --git a/src/kemal/exception_handler.cr b/src/kemal/exception_handler.cr index 98a8643a..24a38493 100644 --- a/src/kemal/exception_handler.cr +++ b/src/kemal/exception_handler.cr @@ -21,7 +21,7 @@ module Kemal log("Exception: #{ex.inspect_with_backtrace}") return call_exception_with_status_code(context, ex, 500) if app.error_handlers.has_key?(500) verbosity = app.config.env == "production" ? false : true - return app.render_500(context, ex.inspect_with_backtrace, verbosity) + return app.render_500(context, ex, verbosity) end end diff --git a/src/kemal/ext/context.cr b/src/kemal/ext/context.cr index ac680310..b8f2d97c 100644 --- a/src/kemal/ext/context.cr +++ b/src/kemal/ext/context.cr @@ -10,11 +10,11 @@ class HTTP::Server macro finished alias StoreTypes = Union({{ *STORE_MAPPINGS }}) - @store = {} of String => StoreTypes + getter store = {} of String => StoreTypes end def params - @params ||= Kemal::ParamParser.new(@request, route_lookup.params) + @params ||= Kemal::ParamParser.new(@request) end def redirect(url : String, status_code : Int32 = 302) @@ -22,30 +22,6 @@ class HTTP::Server @response.status_code = status_code end - def route - route_lookup.payload - end - - def websocket - ws_route_lookup.payload - end - - def route_lookup - app.route_handler.lookup_route(@request.override_method.as(String), @request.path) - end - - def route_found? - route_lookup.found? - end - - def ws_route_lookup - app.websocket_handler.lookup_ws_route(@request.path) - end - - def ws_route_found? - ws_route_lookup.found? - end - def get(name : String) @store[name] end diff --git a/src/kemal/helpers/templates.cr b/src/kemal/helpers/templates.cr index 924941fa..f0bf32ff 100644 --- a/src/kemal/helpers/templates.cr +++ b/src/kemal/helpers/templates.cr @@ -1,9 +1,9 @@ # 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 +module Kemal::Templates + def render_404 + <<-HTML @@ -17,73 +17,21 @@ def render_404

Kemal doesn't know this way.

- - - HTML -end + + + HTML + end -def render_500(context, exception, verbosity) - context.response.status_code = 500 + def render_500(context, exception, verbosity) + context.response.status_code = 500 - template = if verbosity + template = if verbosity Kemal::ExceptionPage.for_runtime_exception(context, exception).to_s else Kemal::ExceptionPage.for_production_exception end - 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 diff --git a/src/kemal/main.cr b/src/kemal/main.cr index 4a66d1a5..a1515dd6 100644 --- a/src/kemal/main.cr +++ b/src/kemal/main.cr @@ -1,28 +1,27 @@ require "http" require "json" require "uri" +require "./ext/*" +require "./helpers/*" require "./application" require "./base_log_handler" require "./cli" require "./exception_handler" require "./log_handler" require "./config" -require "./exceptions" require "./file_upload" require "./filter_handler" require "./handler" require "./init_handler" require "./null_log_handler" require "./param_parser" -require "./response" require "./route" require "./route_handler" require "./ssl" require "./static_file_handler" require "./websocket" require "./websocket_handler" -require "./ext/*" -require "./helpers/*" + module Kemal def self.application @@ -35,7 +34,7 @@ module Kemal # Overload of `self.run` with the default startup logging. def self.run(port : Int32? = nil) - CLI.new(config) + CLI.new(ARGV, config) application.run(port) end @@ -44,7 +43,7 @@ module Kemal # The port can be given to `#run` but is optional. # If not given Kemal will use `Kemal::Config#port` def self.run(port : Int32? = nil) - CLI.new(config) + CLI.new(ARGV, config) application.run(port) do |application| yield application diff --git a/src/kemal/param_parser.cr b/src/kemal/param_parser.cr index 7dfb52bf..152f2312 100644 --- a/src/kemal/param_parser.cr +++ b/src/kemal/param_parser.cr @@ -10,8 +10,9 @@ module Kemal # :nodoc: alias AllParamTypes = Nil | String | Int64 | Float64 | Bool | Hash(String, JSON::Any) | Array(JSON::Any) getter files + getter url : Hash(String, String) - def initialize(@request : HTTP::Request, @url : Hash(String, String) = {} of String => String) + def initialize(@request : HTTP::Request) @query = HTTP::Params.new({} of String => Array(String)) @body = HTTP::Params.new({} of String => Array(String)) @json = {} of String => AllParamTypes @@ -21,6 +22,7 @@ module Kemal @body_parsed = false @json_parsed = false @files_parsed = false + @url = {} of String => String end private def unescape_url_param(value : String) @@ -61,7 +63,12 @@ module Kemal end private def parse_url - @url.each { |key, value| @url[key] = unescape_url_param(value) } + unless @request.url_params.nil? + @request.url_params.not_nil!.each { |key, value| @url[key] = unescape_url_param(value) } + else + @url + end + end private def parse_files diff --git a/src/kemal/route_handler.cr b/src/kemal/route_handler.cr index ca7f3c8a..795a9f16 100644 --- a/src/kemal/route_handler.cr +++ b/src/kemal/route_handler.cr @@ -54,8 +54,11 @@ module Kemal # Processes the route if it's a match. Otherwise renders 404. private def process_request(context) - raise Kemal::Exceptions::RouteNotFound.new(context) unless context.route_found? - content = context.route.handler.call(context) + raise Kemal::Exceptions::RouteNotFound.new(context) unless route_defined?(context.request) + + tree_result = lookup_route(context.request) + context.request.url_params = tree_result.params + content = tree_result.payload.handler.call(context) if !app.error_handlers.empty? && app.error_handlers.has_key?(context.response.status_code) raise Kemal::Exceptions::CustomException.new(context) @@ -76,6 +79,7 @@ module Kemal def clear @routes = Radix::Tree(Route).new + @cached_routes = Hash(String, Radix::Result(Route)).new end end end From 2ee9433bd97c7d750e1c120a162dc0521cbc56c4 Mon Sep 17 00:00:00 2001 From: sdogruyol Date: Wed, 19 Dec 2018 22:54:07 +0300 Subject: [PATCH 18/18] Fix cli --- src/kemal/cli.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kemal/cli.cr b/src/kemal/cli.cr index 1304ddff..898cff6a 100644 --- a/src/kemal/cli.cr +++ b/src/kemal/cli.cr @@ -3,7 +3,7 @@ require "option_parser" module Kemal # Handles all the initialization from the command line. class CLI - def initialize(args, @config : Config = Kemal.config) + def initialize(args = ARGV, @config : Config = Kemal.config) @ssl_enabled = false @key_file = "" @cert_file = ""