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