Refactor class level DSL with macros to convert blocks to instance-scoped methods

This commit is contained in:
Johannes Müller 2017-07-20 10:48:31 +02:00 committed by sdogruyol
parent 53fa65f964
commit ad91a22789
6 changed files with 179 additions and 51 deletions

17
samples/app_squared.cr Normal file
View file

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

View file

@ -8,6 +8,10 @@ private class MyApp < Kemal::Application
get "/route2" do |env|
"Route 2"
end
get "/file" do |env|
send_file env, "Serdar".to_slice
end
end
describe MyApp do
@ -24,4 +28,23 @@ describe MyApp do
end
end
end
it "sends file with binary stream" do
request = HTTP::Request.new("GET", "/file")
response = call_request_on_app(MyApp.new, request)
response.status_code.should eq(200)
response.headers["Content-Type"].should eq("application/octet-stream")
response.headers["Content-Length"].should eq("6")
end
it "responds to delayed route" do
app = MyApp.new
app.setup
app.get "/delayed" do |env|
"Happy addition!"
end
request = HTTP::Request.new("GET", "/delayed")
client_response = call_request_on_app(app, request)
client_response.body.should eq("Happy addition!")
end
end

View file

@ -42,7 +42,7 @@ describe "Macros" do
client_response.body.should eq("world")
app.get "/breaking" do |env|
halt env, 404, "hello"
Kemal::Macros.halt env, 404, "hello"
"world"
end
request = HTTP::Request.new("GET", "/breaking")
@ -54,7 +54,7 @@ describe "Macros" do
it "can break block with halt macro using default values" do
app = Kemal::Base.new
app.get "/" do |env|
halt env
Kemal::Macros.halt env
"world"
end
request = HTTP::Request.new("GET", "/")
@ -69,7 +69,7 @@ describe "Macros" do
app = Kemal::Base.new
app.get "/headers" do |env|
env.response.headers.add "Content-Type", "image/png"
headers env, {
app.headers env, {
"Access-Control-Allow-Origin" => "*",
"Content-Type" => "text/plain",
}
@ -85,7 +85,7 @@ describe "Macros" do
it "sends file with given path and default mime-type" do
app = Kemal::Base.new
app.get "/" do |env|
send_file env, "./spec/asset/hello.ecr"
app.send_file env, "./spec/asset/hello.ecr"
end
request = HTTP::Request.new("GET", "/")
@ -98,7 +98,7 @@ describe "Macros" do
it "sends file with given path and given mime-type" do
app = Kemal::Base.new
app.get "/" do |env|
send_file env, "./spec/asset/hello.ecr", "image/jpeg"
app.send_file env, "./spec/asset/hello.ecr", "image/jpeg"
end
request = HTTP::Request.new("GET", "/")
@ -111,7 +111,7 @@ describe "Macros" do
it "sends file with binary stream" do
app = Kemal::Base.new
app.get "/" do |env|
send_file env, "Serdar".to_slice
app.send_file env, "Serdar".to_slice
end
request = HTTP::Request.new("GET", "/")

View file

@ -14,7 +14,7 @@ class Kemal::Application < Kemal::Base
super
unless error_handlers.has_key?(404)
error 404 do |env|
self.error 404 do |env|
render_404
end
end
@ -22,7 +22,7 @@ class Kemal::Application < Kemal::Base
# Test environment doesn't need to have signal trap, built-in images, and logging.
unless @config.env == "test"
# This route serves the built-in images for not_found and exceptions.
get "/__kemal__/:image" do |env|
self.get "/__kemal__/:image" do |env|
image = env.params.url["image"]
file_path = File.expand_path("lib/kemal/images/#{image}", Dir.current)
if File.exists? file_path

View file

@ -13,7 +13,6 @@ class Kemal::Base
include Macros
include Base::DSL
include Base::Builder
extend Base::ClassDSL
# :nodoc:
getter route_handler = Kemal::RouteHandler.new
@ -41,9 +40,7 @@ class Kemal::Base
# Overload of self.run with the default startup logging
def run(port : Int32? = nil)
run port do
log "[#{config.env}] Kemal is ready to lead at #{config.scheme}://#{config.host_binding}:#{config.port}"
end
run(port) { }
end
# The command to run a `Kemal` application.

View file

@ -1,31 +1,42 @@
class Kemal::Base
private CUSTOM_METHODS_REGISTRY = {} of _ => _
macro inherited
{% CUSTOM_METHODS_REGISTRY[@type] = {
handlers: [] of _,
ws: [] of _,
error: [] of _,
filters: [] of _,
} %}
include MacroDSL
end
module DSL
HTTP_METHODS = %w(get post put patch delete options)
FILTER_METHODS = %w(get post put patch delete options all)
macro included
# :nodoc:
DEFAULT_HANDLERS = [] of {String, String, (HTTP::Server::Context -> String)}
# :nodoc:
WEBSOCKET_HANDLERS = [] of {String, (HTTP::WebSocket, HTTP::Server::Context -> Void)}
# :nodoc:
DEFAULT_ERROR_HANDLERS = [] of {Int32, (HTTP::Server::Context, Exception -> String)}
# :nodoc:
DEFAULT_FILTERS = [] of {Symbol, String, String, (HTTP::Server::Context -> String)}
end
{% for method in HTTP_METHODS %}
# Add a `{{method.id.upcase}}` handler.
#
# The block receives an `HTTP::Server::Context` as argument.
def {{method.id}}(path, &block : HTTP::Server::Context -> _)
raise Kemal::Exceptions::InvalidPathStartException.new({{method}}, path) unless Kemal::Utils.path_starts_with_slash?(path)
raise Kemal::Exceptions::InvalidPathStartException.new({{method}}, path) unless path.starts_with?("/")
route_handler.add_route({{method}}.upcase, path, &block)
end
{% end %}
# Add a webservice handler.
#
# The block receives `HTTP::WebSocket` and `HTTP::Server::Context` as arguments.
def ws(path, &block : HTTP::WebSocket, HTTP::Server::Context -> Void)
raise Kemal::Exceptions::InvalidPathStartException.new("ws", path) unless Kemal::Utils.path_starts_with_slash?(path)
raise Kemal::Exceptions::InvalidPathStartException.new("ws", path) unless path.starts_with?("/")
websocket_handler.add_route path, &block
end
# Add an error handler for *status_code*.
#
# The block receives `HTTP::Server::Context` and `Exception` as arguments.
def error(status_code, &block : HTTP::Server::Context, Exception -> _)
add_error_handler status_code, &block
end
@ -35,57 +46,137 @@ class Kemal::Base
# - after_all, after_get, after_post, after_put, after_patch, after_delete, after_options
{% for type in ["before", "after"] %}
{% for method in FILTER_METHODS %}
# Add a filter for this class that runs {{type.id}} each `{{method.id.upcase}}` request (optionally limited to a specific *path*).
#
# The block receives an `HTTP::Server::Context` as argument.
def {{type.id}}_{{method.id}}(path = "*", &block : HTTP::Server::Context -> _)
filter_handler.{{type.id}}({{method}}.upcase, path, &block)
end
{% end %}
{% end %}
private def initialize_defaults
DEFAULT_HANDLERS.each do |method, path, block|
route_handler.add_route(method.upcase, path, &block)
private macro initialize_defaults
{% if CUSTOM_METHODS_REGISTRY[@type] %}
{% for handler in CUSTOM_METHODS_REGISTRY[@type][:handlers] %}
self.{{handler[0].id}}({{handler[1]}}) do |context|
{{handler[2].id}}(context)
end
{% end %}
WEBSOCKET_HANDLERS.each do |path, block|
ws(path, &block)
{% for ws in CUSTOM_METHODS_REGISTRY[@type][:ws] %}
self.ws({{handler[0]}}) do |websocket, context|
{{handler[1].id}}(websocket, context)
end
{% end %}
DEFAULT_ERROR_HANDLERS.each do |status_code, block|
add_error_handler status_code, &block
{% for ws in CUSTOM_METHODS_REGISTRY[@type][:error] %}
self.add_error_handler({{handler[0]}}) do |context|
{{handler[1].id}}(context)
end
{% end %}
DEFAULT_FILTERS.each do |type, method, path, block|
if type == :before
filter_handler.before(method, path, &block)
else
filter_handler.after(method, path, &block)
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 ClassDSL
module MacroDSL
{% for method in DSL::HTTP_METHODS %}
def {{method.id}}(path, &block : HTTP::Server::Context -> _)
DEFAULT_HANDLERS << { {{method}}, path, block }
# Define a `{{method.id.upcase}}` handler for this class.
#
# It will be initialized in every instance.
# The block receives an `HTTP::Server::Context` as argument and is scoped to the instance.
#
# Example:
# ```
# class MyClass < Kemal::Base
# {{method.id}}("/route") do |context|
# # ...
# end
# end
# ```
# NOTE: This macro *must* be called from class scope as it expands to a custom method definition.
macro {{method.id}}(path, &block)
\{% raise "invalid path start for {{method.id}}: path must start with \"/\"" unless path.starts_with?("/") %}
\{% method_name = "__{{method.id}}_#{path.id.gsub(/[^a-zA-Z0-9]/,"_").gsub(/__+/, "_").gsub(/\A_|_\z/, "")}_#{CUSTOM_METHODS_REGISTRY[@type][:handlers].size}" %}
def \{{method_name.id}}(\{{block.args[0].id}})
\{{block.body}}
end
\{% CUSTOM_METHODS_REGISTRY[@type][:handlers] << { {{method}}, path, method_name } %}
end
{% end %}
def ws(path, &block : HTTP::WebSocket, HTTP::Server::Context -> Void)
WEBSOCKET_HANDLERS << {path, block}
# Define a webservice handler for this class.
#
# It will be initialized in every instance.
# The block receives `HTTP::WebSocket` and `HTTP::Server::Context` as arguments and is scoped to the instance.
#
# Example:
# ```
# class MyClass < Kemal::Base
# ws("/wsroute") do |context|
# # ...
# end
# end
# ```
# NOTE: This macro *must* be called from class scope as it expands to a custom method definition.
macro ws(path, &block)
\{% raise "invalid path start for webservice: path must start with \"/\"" unless path.starts_with?("/") %}
\{% method_name = "__ws_#{path.id.gsub(/[^a-zA-Z0-9]/,"_").gsub(/__+/, "_").gsub(/\A_|_\z/, "")}_#{CUSTOM_METHODS_REGISTRY[@type][:ws].size}" %}
def \{{method_name.id}}(\{{block.args[0].id}}, \{{block.args[1].id}})
\{{block.body}}
end
\{% CUSTOM_METHODS_REGISTRY[@type][:ws] << { path, method_name } %}
end
def error(status_code, &block : HTTP::Server::Context, Exception -> _)
DEFAULT_ERROR_HANDLERS << {status_code, block}
# Define an error handler for this class.
#
# It will be initialized in every instance.
# The block receives `HTTP::Server::Context` and `Exception` as arguments and is scoped to the instance.
#
# Example:
# ```
# class MyClass < Kemal::Base
# error(403) do |context|
# # ...
# end
# end
# ```
# NOTE: This macro *must* be called from class scope as it expands to a custom method definition.
macro error(status_code)
\{% method_name = "__error_#{status_code}_#{CUSTOM_METHODS_REGISTRY[@type][:error].size}" %}
def \{{method_name.id}}(\{{block.args[0].id}})
\{{block.body}}
end
\{% CUSTOM_METHODS_REGISTRY[@type][:error] << { status_code, method_name } %}
end
# All the helper methods available are:
# - before_all, before_get, before_post, before_put, before_patch, before_delete, before_options
# - after_all, after_get, after_post, after_put, after_patch, after_delete, after_options
{% for type in [:before, :after] %}
{% for type in ["before", "after"] %}
{% for method in DSL::FILTER_METHODS %}
def {{type.id}}_{{method.id}}(path = "*", &block : HTTP::Server::Context -> _)
DEFAULT_FILTERS << { {{type}}, {{method}}, path, block }
# Define a filter for this class that runs {{type.id}} each `{{method.id.upcase}}` request (optionally limited to a specific *path*).
#
# The filter will be initialized in every instance of this class.
# The block receives an `HTTP::Context` as argument and is scoped to the instance.
#
# Example:
# ```
# class MyClass < Kemal::Base
# {{type.id}}_{{method.id}}("/route") do |context|
# # ...
# end
# end
# ```
# NOTE: This macro *must* be called from class scope as it expands to a custom method definition.
macro {{type.id}}_{{method.id}}(path = "*", &block)
\{% method_name = "__{{type.id}}_{{method.id}}_#{path.id.gsub(/[^a-zA-Z0-9]/,"_").gsub(/__+/, "_").gsub(/\A_|_\z/, "")}_#{CUSTOM_METHODS_REGISTRY[@type][:handlers].size}" %}
def \{{method_name.id}}(\{{block.args[0].id}})
\{{block.body}}
end
\{% CUSTOM_METHODS_REGISTRY[@type][:fitlers] << { {{type}}, {{method}}, path, method_name } %}
end
{% end %}
{% end %}