Refactor global namespace DSL into OOP Kemal::Base

This commit is contained in:
Johannes Müller 2017-07-16 19:16:12 +02:00 committed by sdogruyol
parent a5d8df7382
commit aaa2109837
25 changed files with 420 additions and 387 deletions

3
.gitignore vendored
View file

@ -1,5 +1,6 @@
/doc/
/bin/
/lib/
/.crystal/
/.shards/
*.log

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 }

View file

@ -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"]

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

227
src/kemal/base.cr Normal file
View file

@ -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

View file

@ -1,3 +1,5 @@
require "http"
module Kemal
# All loggers must inherit from `Kemal::BaseLogHandler`.
abstract class BaseLogHandler

View file

@ -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

View file

@ -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

View file

@ -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 %}

View file

@ -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

View file

@ -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?

View file

@ -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)

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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