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/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/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/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/application_mode_spec.cr b/spec/application_mode_spec.cr
new file mode 100644
index 00000000..a8448108
--- /dev/null
+++ b/spec/application_mode_spec.cr
@@ -0,0 +1,50 @@
+require "./spec_helper"
+
+private class MyApp < Kemal::Application
+ get "/route1" do |env|
+ "Route 1"
+ end
+
+ get "/route2" do |env|
+ "Route 2"
+ end
+
+ get "/file" do |env|
+ send_file env, "Serdar".to_slice
+ 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
+
+ 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/config_spec.cr b/spec/config_spec.cr
index 31a84380..b87c93f5 100644
--- a/spec/config_spec.cr
+++ b/spec/config_spec.cr
@@ -1,16 +1,25 @@
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
- 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 +29,35 @@ 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)
+ it "adds a custom handler to Base" do
+ application = Kemal::Base.new
+ application.add_handler CustomTestHandler.new
+ application.setup
+ 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 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 +67,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..1bfb9f7c 100644
--- a/spec/context_spec.cr
+++ b/spec/context_spec.cr
@@ -1,107 +1,75 @@
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
- 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)
+ 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
+ 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/dsl_helper.cr b/spec/dsl_helper.cr
new file mode 100644
index 00000000..fdd6169c
--- /dev/null
+++ b/spec/dsl_helper.cr
@@ -0,0 +1,40 @@
+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
+
+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)
+ 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/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/exception_handler_spec.cr b/spec/exception_handler_spec.cr
index 78da4263..dc72b943 100644
--- a/spec/exception_handler_spec.cr
+++ b/spec/exception_handler_spec.cr
@@ -1,16 +1,13 @@
-require "./spec_helper"
+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)
- Kemal::ExceptionHandler::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)
@@ -18,18 +15,20 @@ 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)
- Kemal::ExceptionHandler::INSTANCE.next = Kemal::RouteHandler::INSTANCE
- Kemal::ExceptionHandler::INSTANCE.call(context)
+ app = Kemal::Base.new
+ app.error 403 do
+ "403 error"
+ end
+ app.get "/" do |env|
+ env.response.status_code = 403
+ end
+ subject = Kemal::ExceptionHandler.new(app)
+ subject.next = app.route_handler
+ subject.call(context)
response.close
io.rewind
response = HTTP::Client::Response.from_io(io, decompress: false)
@@ -39,18 +38,20 @@ 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)
- Kemal::ExceptionHandler::INSTANCE.next = Kemal::RouteHandler::INSTANCE
- Kemal::ExceptionHandler::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
+ subject = Kemal::ExceptionHandler.new(app)
+ subject.next = app.route_handler
+ subject.call(context)
response.close
io.rewind
response = HTTP::Client::Response.from_io(io, decompress: false)
@@ -60,19 +61,21 @@ 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)
- Kemal::ExceptionHandler::INSTANCE.next = Kemal::RouteHandler::INSTANCE
- Kemal::ExceptionHandler::INSTANCE.call(context)
+ subject = Kemal::ExceptionHandler.new(app)
+ subject.next = app.route_handler
+ subject.call(context)
response.close
io.rewind
response = HTTP::Client::Response.from_io(io, decompress: false)
@@ -82,19 +85,21 @@ 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)
- Kemal::ExceptionHandler::INSTANCE.next = Kemal::RouteHandler::INSTANCE
- Kemal::ExceptionHandler::INSTANCE.call(context)
+ subject = Kemal::ExceptionHandler.new(app)
+ subject.next = app.route_handler
+ 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 9b1019fc..85e20017 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)
@@ -69,7 +69,8 @@ end
describe "Handler" do
it "adds custom handler before before_*" do
- filter_middleware = Kemal::FilterHandler.new
+ app = Kemal::Base.new
+ filter_middleware = Kemal::FilterHandler.new(app)
filter_middleware._add_route_filter("GET", "/", :before) do |env|
env.response << " is"
end
@@ -77,85 +78,93 @@ describe "Handler" do
filter_middleware._add_route_filter("GET", "/", :before) do |env|
env.response << " so"
end
- add_handler CustomTestHandler.new
+ app.add_filter_handler filter_middleware
- get "/" do
+ app.add_handler CustomTestHandler.new
+
+ 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.config.setup
- Kemal.config.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.config.handlers = [post_only_handler, post_exclude_handler]
- Kemal.config.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
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..d5139cdb 100644
--- a/spec/helpers_spec.cr
+++ b/spec/helpers_spec.cr
@@ -1,62 +1,52 @@
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
- add_handler CustomTestHandler.new
- Kemal.config.setup
- Kemal.config.handlers.size.should eq 7
- end
- end
-
- describe "#logging" do
- it "sets logging status" do
- logging false
- Kemal.config.logging.should eq false
- end
-
- it "sets a custom logger" do
- config = Kemal::Config::INSTANCE
- logger CustomLogHandler.new
- config.logger.should be_a(CustomLogHandler)
+ app = Kemal::Application.new
+ app.add_handler CustomTestHandler.new
+ app.setup
+ app.handlers.size.should eq 8
end
end
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|
- halt env, 404, "hello"
+ app.get "/breaking" do |env|
+ Kemal::Macros.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|
- halt env
+ app = Kemal::Base.new
+ app.get "/" do |env|
+ Kemal::Macros.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,15 +54,16 @@ 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, {
+ app.headers env, {
"Access-Control-Allow-Origin" => "*",
"Content-Type" => "text/plain",
}
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 +71,39 @@ describe "Macros" do
describe "#send_file" do
it "sends file with given path and default mime-type" do
- get "/" do |env|
- send_file env, "./spec/asset/hello.ecr"
+ app = Kemal::Base.new
+ app.get "/" do |env|
+ app.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|
- send_file env, "./spec/asset/hello.ecr", "image/jpeg"
+ app = Kemal::Base.new
+ app.get "/" do |env|
+ app.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|
- send_file env, "Serdar".to_slice
+ app = Kemal::Base.new
+ app.get "/" do |env|
+ app.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")
@@ -119,24 +113,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..09e874cd 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
@@ -26,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 9bc2564c..e6421559 100644
--- a/spec/middleware/filters_spec.cr
+++ b/spec/middleware/filters_spec.cr
@@ -1,20 +1,20 @@
-require "../spec_helper"
+require "../dsl_helper"
describe "Kemal::FilterHandler" do
it "executes code before home request" 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::RouteHandler::INSTANCE
+ kemal = Kemal.application.route_handler
kemal.add_route "GET", "/greetings" { test_filter.modified }
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
@@ -23,24 +23,24 @@ 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::RouteHandler::INSTANCE
+ kemal = Kemal.application.route_handler
kemal.add_route "GET", "/greetings" { test_filter.modified }
kemal.add_route "POST", "/greetings" { test_filter.modified }
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
@@ -49,26 +49,26 @@ 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" }
- 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 }
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
@@ -77,69 +77,69 @@ 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::RouteHandler::INSTANCE
+ kemal = Kemal.application.route_handler
kemal.add_route "GET", "/greetings" { test_filter.modified }
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
+ 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::RouteHandler::INSTANCE
+ kemal = Kemal.application.route_handler
kemal.add_route "GET", "/greetings" { test_filter.modified }
kemal.add_route "POST", "/greetings" { test_filter.modified }
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
+ 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" }
- 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 }
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
@@ -152,12 +152,12 @@ 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" }
- 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 }
@@ -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 d63a2298..0cba4ccd 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
@@ -22,19 +22,19 @@ describe "ParamParser" do
end
it "parses url params" do
- kemal = Kemal::RouteHandler::INSTANCE
- 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
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"]
@@ -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/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 25534552..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
@@ -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..f840b611 100644
--- a/spec/spec_helper.cr
+++ b/spec/spec_helper.cr
@@ -1,16 +1,5 @@
require "spec"
-require "../src/*"
-
-include Kemal
-
-class CustomLogHandler < Kemal::BaseLogHandler
- def call(env)
- call_next env
- end
-
- def write(message)
- end
-end
+require "../src/kemal"
class TestContextStorageType
property id
@@ -26,63 +15,27 @@ class AnotherContextStorageType
@name = "kemal-context"
end
-add_context_storage_type(TestContextStorageType)
-add_context_storage_type(AnotherContextStorageType)
-
-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)
- 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)
- begin
- handler.call context
- rescue IO::Error
- # Raises because the IO::Memory is empty
- end
- io.rewind
- {io, context}
-end
+Kemal::Macros.add_context_storage_type(TestContextStorageType)
+Kemal::Macros.add_context_storage_type(AnotherContextStorageType)
-def call_request_on_app(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)
- 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.config.setup
- main_handler = Kemal.config.handlers.first
+def build_main_handler(app)
+ app.setup
+ main_handler = app.handlers.first
current_handler = main_handler
- Kemal.config.handlers.each do |handler|
+ 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.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
-end
diff --git a/spec/static_file_handler_spec.cr b/spec/static_file_handler_spec.cr
index 1aac161b..bbb0a244 100644
--- a/spec/static_file_handler_spec.cr
+++ b/spec/static_file_handler_spec.cr
@@ -1,16 +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)
- 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
@@ -36,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
@@ -96,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
@@ -132,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 bc02d3c2..976fbc7e 100644
--- a/spec/websocket_handler_spec.cr
+++ b/spec/websocket_handler_spec.cr
@@ -1,10 +1,23 @@
require "./spec_helper"
+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)
+ 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
- handler = Kemal::WebSocketHandler::INSTANCE
- handler.next = Kemal::RouteHandler::INSTANCE
- ws "/" { }
+ app = Kemal::Base.new
+ handler = app.websocket_handler
+ handler.next = app.route_handler
+ app.ws "/" { }
headers = HTTP::Headers{
"Upgrade" => "websocket",
"Connection" => "Upgrade",
@@ -21,9 +34,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" }
+ app = Kemal::Base.new
+ app.ws "/" { |socket, context| socket.send("Match") }
+ app.ws "/no_match" { |socket, context| socket.send "No Match" }
headers = HTTP::Headers{
"Upgrade" => "websocket",
"Connection" => "Upgrade",
@@ -32,13 +45,13 @@ 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 = 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
- handler = Kemal::WebSocketHandler::INSTANCE
- ws "/:id" { |_, c| c.ws_route_lookup.params["id"] }
+ app = Kemal::Base.new
+ app.ws "/:id" { |s, c| c.params.url["id"] }
headers = HTTP::Headers{
"Upgrade" => "websocket",
"Connection" => "Upgrade",
@@ -46,15 +59,16 @@ 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 = 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
it "matches correct verb" do
- handler = Kemal::WebSocketHandler::INSTANCE
- handler.next = Kemal::RouteHandler::INSTANCE
- ws "/" { }
- get "/" { "get" }
+ app = Kemal::Base.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)
diff --git a/src/kemal.cr b/src/kemal.cr
index a57330e8..11f47e78 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"
diff --git a/src/kemal/application.cr b/src/kemal/application.cr
new file mode 100644
index 00000000..0222d5b5
--- /dev/null
+++ b/src/kemal/application.cr
@@ -0,0 +1,42 @@
+class Kemal::Application < Kemal::Base
+ def initialize(config = Config.default)
+ super(config)
+ 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)
+ self.error 404 do |env|
+ render_404
+ end
+ end
+
+ # 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"]
+ 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
new file mode 100644
index 00000000..bbb073a5
--- /dev/null
+++ b/src/kemal/base.cr
@@ -0,0 +1,120 @@
+require "./helpers/*"
+require "./base/*"
+
+# Kemal Base
+# The DSL currently consists of
+# - get post put patch delete options
+# - WebSocket(ws)
+# - before_*
+# - error
+class Kemal::Base
+ include FileHelpers
+ include Templates
+ include Macros
+ include Base::DSL
+ include Base::Builder
+
+ # :nodoc:
+ # TODO: These ivars are initialized in the constructor, but their values depend on `self`.
+ getter! route_handler : RouteHandler?
+ # :nodoc:
+ getter! filter_handler : FilterHandler?
+ # :nodoc:
+ getter! websocket_handler : WebSocketHandler?
+
+ getter handlers = [] of HTTP::Handler
+ getter error_handlers = {} of Int32 => HTTP::Server::Context, Exception -> String
+
+ getter config : Config
+
+ property! logger : BaseLogHandler
+ property! server : HTTP::Server
+ property? running = false
+
+ def initialize(@config = Config.base)
+ @filter_handler = FilterHandler.new(self)
+ @route_handler = RouteHandler.new(self)
+ @websocket_handler = WebSocketHandler.new(self)
+
+ initialize_defaults
+ end
+
+ # Overload of self.run with the default startup logging
+ def run(port : Int32? = nil)
+ 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 run(port : Int32? = nil)
+ setup
+
+ prepare_for_server_start
+
+ start_server(port) do
+ yield self
+ 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?
+ end
+
+ private def prepare_for_server_start
+ end
+
+ private def start_server(port)
+ @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 %}
+
+ @running = true
+
+ yield
+
+ server.listen unless @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
+
+ def log(message)
+ logger.write "#{message}\n"
+ end
+end
+
+require "./main"
diff --git a/src/kemal/base/builder.cr b/src/kemal/base/builder.cr
new file mode 100644
index 00000000..e4aaad15
--- /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(self)
+ @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))
+ @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..8b74b393
--- /dev/null
+++ b/src/kemal/base/dsl.cr
@@ -0,0 +1,184 @@
+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)
+
+ {% 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 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 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
+
+ # 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 %}
+ # 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 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 %}
+
+ {% for ws in CUSTOM_METHODS_REGISTRY[@type][:ws] %}
+ self.ws({{handler[0]}}) do |websocket, context|
+ {{handler[1].id}}(websocket, context)
+ end
+ {% end %}
+
+ {% for ws in CUSTOM_METHODS_REGISTRY[@type][:error] %}
+ self.add_error_handler({{handler[0]}}) do |context|
+ {{handler[1].id}}(context)
+ end
+ {% end %}
+
+ {% 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
+
+ module MacroDSL
+ {% for method in DSL::HTTP_METHODS %}
+ # 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 %}
+
+ # 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
+
+ # 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
+
+ {% for type in ["before", "after"] %}
+ {% for method in DSL::FILTER_METHODS %}
+ # 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 %}
+ 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..898cff6a 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 = ARGV, @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..0af21014 100644
--- a/src/kemal/config.cr
+++ b/src/kemal/config.cr
@@ -8,161 +8,72 @@ 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.is_a?(Hash) && config[key]?) || false
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
+ def extra_options(&@extra_options : OptionParser ->)
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
+ def serve_static?(key)
+ (h = @serve_static).is_a?(Hash) && h[key]? == true
end
- private def setup_custom_handlers
- CUSTOM_HANDLERS.each do |ch0, ch1|
- position = ch0
- HANDLERS.insert (position || @handler_position), ch1
- @handler_position += 1
- end
+ # Create a config with default values
+ def self.default
+ new
end
- private def setup_filter_handlers
- FILTER_HANDLERS.each do |h|
- HANDLERS.insert(@handler_position, h)
- end
+ # Creates a config with basic value (disabled logging, disabled serve_static, disabled shutdown_message)
+ def self.base
+ new(
+ logging: false,
+ serve_static: false,
+ shutdown_message: false,
+ always_rescue: false,
+ )
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..4c8575a2 100644
--- a/src/kemal/dsl.cr
+++ b/src/kemal/dsl.cr
@@ -6,32 +6,30 @@
# - WebSocket(ws)
# - before_*
# - error
-HTTP_METHODS = %w(get post put patch delete options)
-FILTER_METHODS = %w(get post put patch delete options all)
+require "../kemal"
+require "./dsl/*"
-{% 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/helpers/helpers.cr b/src/kemal/dsl/file_helpers.cr
similarity index 53%
rename from src/kemal/helpers/helpers.cr
rename to src/kemal/dsl/file_helpers.cr
index f487c997..457b3038 100644
--- a/src/kemal/helpers/helpers.cr
+++ b/src/kemal/dsl/file_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.log(message)
end
# Enables / Disables logging.
@@ -64,8 +64,7 @@ end
# logger MyCustomLogger.new
# ```
def logger(logger : Kemal::BaseLogHandler)
- Kemal.config.logger = logger
- Kemal.config.add_handler logger
+ Kemal.application.logger = 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..ba865a16
--- /dev/null
+++ b/src/kemal/dsl/templates.cr
@@ -0,0 +1,7 @@
+def render_404
+ Kemal.application.render_404
+end
+
+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 a0b9a690..24a38493 100644
--- a/src/kemal/exception_handler.cr
+++ b/src/kemal/exception_handler.cr
@@ -2,25 +2,33 @@ module Kemal
# Handles all the exceptions, including 404, custom errors and 500.
class ExceptionHandler
include HTTP::Handler
- INSTANCE = new
+
+ getter app : Kemal::Base
+
+ def initialize(@app)
+ end
+
+ delegate log, to: app
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 app.error_handlers.has_key?(500)
+ verbosity = app.config.env == "production" ? false : true
+ return app.render_500(context, ex, 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 !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 Kemal.config.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/ext/context.cr b/src/kemal/ext/context.cr
index f9a12caf..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
- Kemal::RouteHandler::INSTANCE.lookup_route(@request.method.as(String), @request.path)
- end
-
- def route_found?
- route_lookup.found?
- end
-
- def ws_route_lookup
- Kemal::WebSocketHandler::INSTANCE.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/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
diff --git a/src/kemal/filter_handler.cr b/src/kemal/filter_handler.cr
index 6d28680a..f6c58941 100644
--- a/src/kemal/filter_handler.cr
+++ b/src/kemal/filter_handler.cr
@@ -2,20 +2,20 @@ module Kemal
# :nodoc:
class FilterHandler
include HTTP::Handler
- INSTANCE = new
+
+ 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
- Kemal.config.add_filter_handler(self)
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.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 !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
new file mode 100644
index 00000000..ade82880
--- /dev/null
+++ b/src/kemal/helpers/file_helpers.cr
@@ -0,0 +1,138 @@
+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.
+ #
+ # ```
+ # 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, 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
+ 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)
+
+ 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") && 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") && 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
+
+ def send_file(env, path : String, mime_type : String? = nil)
+ send_file(env, path, config, mime_type)
+ 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..f0bf32ff 100644
--- a/src/kemal/helpers/templates.cr
+++ b/src/kemal/helpers/templates.cr
@@ -1,8 +1,9 @@
# This file contains the built-in view templates that Kemal uses.
# Currently it contains templates for 404 and 500 error codes.
-def render_404
- <<-HTML
+module Kemal::Templates
+ def render_404
+ <<-HTML
@@ -16,20 +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
+ context.response.print template
+ context
+ end
end
diff --git a/src/kemal/init_handler.cr b/src/kemal/init_handler.cr
index 881325b6..7674b948 100644
--- a/src/kemal/init_handler.cr
+++ b/src/kemal/init_handler.cr
@@ -4,7 +4,10 @@ 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
diff --git a/src/kemal/main.cr b/src/kemal/main.cr
new file mode 100644
index 00000000..a1515dd6
--- /dev/null
+++ b/src/kemal/main.cr
@@ -0,0 +1,56 @@
+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 "./file_upload"
+require "./filter_handler"
+require "./handler"
+require "./init_handler"
+require "./null_log_handler"
+require "./param_parser"
+require "./route"
+require "./route_handler"
+require "./ssl"
+require "./static_file_handler"
+require "./websocket"
+require "./websocket_handler"
+
+
+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(ARGV, 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(ARGV, config)
+
+ application.run(port) do |application|
+ yield application
+ end
+ end
+
+ def self.stop
+ application.stop
+ end
+end
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 528d7736..795a9f16 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,23 @@ 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)
+ raise Kemal::Exceptions::RouteNotFound.new(context) unless route_defined?(context.request)
- if !Kemal.config.error_handlers.empty? && Kemal.config.error_handlers.has_key?(context.response.status_code)
+ 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)
end
@@ -63,5 +76,10 @@ module Kemal
node = radix_path method, path
@routes.add node, route
end
+
+ def clear
+ @routes = Radix::Tree(Route).new
+ @cached_routes = Hash(String, Radix::Result(Route)).new
+ end
end
end
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
diff --git a/src/kemal/websocket_handler.cr b/src/kemal/websocket_handler.cr
index addbecfa..a2515d35 100644
--- a/src/kemal/websocket_handler.cr
+++ b/src/kemal/websocket_handler.cr
@@ -2,16 +2,19 @@ module Kemal
class WebSocketHandler
include HTTP::Handler
- INSTANCE = new
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
@@ -39,5 +42,9 @@ module Kemal
context.request.headers.includes_word?("Connection", "Upgrade")
end
+
+ def clear
+ @routes = Radix::Tree(WebSocket).new
+ end
end
end