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] 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 | 165 ++++------------------- 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, 420 insertions(+), 387 deletions(-) create mode 100644 src/kemal/base.cr diff --git a/.gitignore b/.gitignore index 5a0d7aa..5411668 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 31a8438..48fc31d 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 c972926..0148d43 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 78da426..bfa875d 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 9b1019f..e256fa7 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 21c0cdf..21272ed 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 601bbc1..43fac04 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 9bc2564..65f1d48 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 d63a229..f85c94e 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 2553455..915fab8 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 0bc127a..2e62d70 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 bc02d3c..ab3d58e 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 a57330e..782826c 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 0000000..23a2e45 --- /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 37ee980..399ef15 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 656a4e6..1304ddf 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 04bbdd7..99b1f9d 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 + 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 ->)? - 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 + # 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 + def serve_static?(key) + config = @serve_static + config.try(&.[key]?) || config == true end - - private def setup_init_handler - HANDLERS.insert(@handler_position, Kemal::InitHandler::INSTANCE) - @handler_position += 1 - 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 15b3742..ba1c021 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 a0b9a69..e1d7168 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 f9a12ca..d4d02b3 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 6d28680..a135744 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 f487c99..e6d1d13 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 881325b..8175223 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 528d773..fd2f672 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 addbecf..da81b2f 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