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|
|
get "/route2" do |env|
|
||||||
"Route 2"
|
"Route 2"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
get "/file" do |env|
|
||||||
|
send_file env, "Serdar".to_slice
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe MyApp do
|
describe MyApp do
|
||||||
|
@ -24,4 +28,23 @@ describe MyApp do
|
||||||
end
|
end
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -42,7 +42,7 @@ describe "Macros" do
|
||||||
client_response.body.should eq("world")
|
client_response.body.should eq("world")
|
||||||
|
|
||||||
app.get "/breaking" do |env|
|
app.get "/breaking" do |env|
|
||||||
halt env, 404, "hello"
|
Kemal::Macros.halt env, 404, "hello"
|
||||||
"world"
|
"world"
|
||||||
end
|
end
|
||||||
request = HTTP::Request.new("GET", "/breaking")
|
request = HTTP::Request.new("GET", "/breaking")
|
||||||
|
@ -54,7 +54,7 @@ describe "Macros" do
|
||||||
it "can break block with halt macro using default values" do
|
it "can break block with halt macro using default values" do
|
||||||
app = Kemal::Base.new
|
app = Kemal::Base.new
|
||||||
app.get "/" do |env|
|
app.get "/" do |env|
|
||||||
halt env
|
Kemal::Macros.halt env
|
||||||
"world"
|
"world"
|
||||||
end
|
end
|
||||||
request = HTTP::Request.new("GET", "/")
|
request = HTTP::Request.new("GET", "/")
|
||||||
|
@ -69,7 +69,7 @@ describe "Macros" do
|
||||||
app = Kemal::Base.new
|
app = Kemal::Base.new
|
||||||
app.get "/headers" do |env|
|
app.get "/headers" do |env|
|
||||||
env.response.headers.add "Content-Type", "image/png"
|
env.response.headers.add "Content-Type", "image/png"
|
||||||
headers env, {
|
app.headers env, {
|
||||||
"Access-Control-Allow-Origin" => "*",
|
"Access-Control-Allow-Origin" => "*",
|
||||||
"Content-Type" => "text/plain",
|
"Content-Type" => "text/plain",
|
||||||
}
|
}
|
||||||
|
@ -85,7 +85,7 @@ describe "Macros" do
|
||||||
it "sends file with given path and default mime-type" do
|
it "sends file with given path and default mime-type" do
|
||||||
app = Kemal::Base.new
|
app = Kemal::Base.new
|
||||||
app.get "/" do |env|
|
app.get "/" do |env|
|
||||||
send_file env, "./spec/asset/hello.ecr"
|
app.send_file env, "./spec/asset/hello.ecr"
|
||||||
end
|
end
|
||||||
|
|
||||||
request = HTTP::Request.new("GET", "/")
|
request = HTTP::Request.new("GET", "/")
|
||||||
|
@ -98,7 +98,7 @@ describe "Macros" do
|
||||||
it "sends file with given path and given mime-type" do
|
it "sends file with given path and given mime-type" do
|
||||||
app = Kemal::Base.new
|
app = Kemal::Base.new
|
||||||
app.get "/" do |env|
|
app.get "/" do |env|
|
||||||
send_file env, "./spec/asset/hello.ecr", "image/jpeg"
|
app.send_file env, "./spec/asset/hello.ecr", "image/jpeg"
|
||||||
end
|
end
|
||||||
|
|
||||||
request = HTTP::Request.new("GET", "/")
|
request = HTTP::Request.new("GET", "/")
|
||||||
|
@ -111,7 +111,7 @@ describe "Macros" do
|
||||||
it "sends file with binary stream" do
|
it "sends file with binary stream" do
|
||||||
app = Kemal::Base.new
|
app = Kemal::Base.new
|
||||||
app.get "/" do |env|
|
app.get "/" do |env|
|
||||||
send_file env, "Serdar".to_slice
|
app.send_file env, "Serdar".to_slice
|
||||||
end
|
end
|
||||||
|
|
||||||
request = HTTP::Request.new("GET", "/")
|
request = HTTP::Request.new("GET", "/")
|
||||||
|
|
|
@ -14,7 +14,7 @@ class Kemal::Application < Kemal::Base
|
||||||
super
|
super
|
||||||
|
|
||||||
unless error_handlers.has_key?(404)
|
unless error_handlers.has_key?(404)
|
||||||
error 404 do |env|
|
self.error 404 do |env|
|
||||||
render_404
|
render_404
|
||||||
end
|
end
|
||||||
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.
|
# Test environment doesn't need to have signal trap, built-in images, and logging.
|
||||||
unless @config.env == "test"
|
unless @config.env == "test"
|
||||||
# This route serves the built-in images for not_found and exceptions.
|
# 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"]
|
image = env.params.url["image"]
|
||||||
file_path = File.expand_path("lib/kemal/images/#{image}", Dir.current)
|
file_path = File.expand_path("lib/kemal/images/#{image}", Dir.current)
|
||||||
if File.exists? file_path
|
if File.exists? file_path
|
||||||
|
|
|
@ -13,7 +13,6 @@ class Kemal::Base
|
||||||
include Macros
|
include Macros
|
||||||
include Base::DSL
|
include Base::DSL
|
||||||
include Base::Builder
|
include Base::Builder
|
||||||
extend Base::ClassDSL
|
|
||||||
|
|
||||||
# :nodoc:
|
# :nodoc:
|
||||||
getter route_handler = Kemal::RouteHandler.new
|
getter route_handler = Kemal::RouteHandler.new
|
||||||
|
@ -41,9 +40,7 @@ class Kemal::Base
|
||||||
|
|
||||||
# Overload of self.run with the default startup logging
|
# Overload of self.run with the default startup logging
|
||||||
def run(port : Int32? = nil)
|
def run(port : Int32? = nil)
|
||||||
run port do
|
run(port) { }
|
||||||
log "[#{config.env}] Kemal is ready to lead at #{config.scheme}://#{config.host_binding}:#{config.port}"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# The command to run a `Kemal` application.
|
# The command to run a `Kemal` application.
|
||||||
|
|
|
@ -1,31 +1,42 @@
|
||||||
class Kemal::Base
|
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
|
module DSL
|
||||||
HTTP_METHODS = %w(get post put patch delete options)
|
HTTP_METHODS = %w(get post put patch delete options)
|
||||||
FILTER_METHODS = %w(get post put patch delete options all)
|
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 %}
|
{% 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 -> _)
|
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)
|
route_handler.add_route({{method}}.upcase, path, &block)
|
||||||
end
|
end
|
||||||
{% 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)
|
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
|
websocket_handler.add_route path, &block
|
||||||
end
|
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 -> _)
|
def error(status_code, &block : HTTP::Server::Context, Exception -> _)
|
||||||
add_error_handler status_code, &block
|
add_error_handler status_code, &block
|
||||||
end
|
end
|
||||||
|
@ -35,57 +46,137 @@ class Kemal::Base
|
||||||
# - after_all, after_get, after_post, after_put, after_patch, after_delete, after_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 FILTER_METHODS %}
|
{% 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 -> _)
|
def {{type.id}}_{{method.id}}(path = "*", &block : HTTP::Server::Context -> _)
|
||||||
filter_handler.{{type.id}}({{method}}.upcase, path, &block)
|
filter_handler.{{type.id}}({{method}}.upcase, path, &block)
|
||||||
end
|
end
|
||||||
{% end %}
|
{% end %}
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
||||||
private def initialize_defaults
|
private macro initialize_defaults
|
||||||
DEFAULT_HANDLERS.each do |method, path, block|
|
{% if CUSTOM_METHODS_REGISTRY[@type] %}
|
||||||
route_handler.add_route(method.upcase, path, &block)
|
{% for handler in CUSTOM_METHODS_REGISTRY[@type][:handlers] %}
|
||||||
|
self.{{handler[0].id}}({{handler[1]}}) do |context|
|
||||||
|
{{handler[2].id}}(context)
|
||||||
end
|
end
|
||||||
|
{% end %}
|
||||||
|
|
||||||
WEBSOCKET_HANDLERS.each do |path, block|
|
{% for ws in CUSTOM_METHODS_REGISTRY[@type][:ws] %}
|
||||||
ws(path, &block)
|
self.ws({{handler[0]}}) do |websocket, context|
|
||||||
|
{{handler[1].id}}(websocket, context)
|
||||||
end
|
end
|
||||||
|
{% end %}
|
||||||
|
|
||||||
DEFAULT_ERROR_HANDLERS.each do |status_code, block|
|
{% for ws in CUSTOM_METHODS_REGISTRY[@type][:error] %}
|
||||||
add_error_handler status_code, &block
|
self.add_error_handler({{handler[0]}}) do |context|
|
||||||
|
{{handler[1].id}}(context)
|
||||||
end
|
end
|
||||||
|
{% end %}
|
||||||
|
|
||||||
DEFAULT_FILTERS.each do |type, method, path, block|
|
{% for filter in CUSTOM_METHODS_REGISTRY[@type][:filters] %}
|
||||||
if type == :before
|
filter_handler.{{filter[0]}}({{filter[1]}}, {{filter[2]}}) do |context|
|
||||||
filter_handler.before(method, path, &block)
|
{{filter[3]}}(context)
|
||||||
else
|
|
||||||
filter_handler.after(method, path, &block)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
{% end %}
|
||||||
|
{% end %}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
module ClassDSL
|
module MacroDSL
|
||||||
{% for method in DSL::HTTP_METHODS %}
|
{% for method in DSL::HTTP_METHODS %}
|
||||||
def {{method.id}}(path, &block : HTTP::Server::Context -> _)
|
# Define a `{{method.id.upcase}}` handler for this class.
|
||||||
DEFAULT_HANDLERS << { {{method}}, path, block }
|
#
|
||||||
|
# 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
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
||||||
def ws(path, &block : HTTP::WebSocket, HTTP::Server::Context -> Void)
|
# Define a webservice handler for this class.
|
||||||
WEBSOCKET_HANDLERS << {path, block}
|
#
|
||||||
|
# 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
|
end
|
||||||
|
|
||||||
def error(status_code, &block : HTTP::Server::Context, Exception -> _)
|
# Define an error handler for this class.
|
||||||
DEFAULT_ERROR_HANDLERS << {status_code, block}
|
#
|
||||||
|
# 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
|
end
|
||||||
|
|
||||||
# All the helper methods available are:
|
{% for type in ["before", "after"] %}
|
||||||
# - 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 DSL::FILTER_METHODS %}
|
{% for method in DSL::FILTER_METHODS %}
|
||||||
def {{type.id}}_{{method.id}}(path = "*", &block : HTTP::Server::Context -> _)
|
# Define a filter for this class that runs {{type.id}} each `{{method.id.upcase}}` request (optionally limited to a specific *path*).
|
||||||
DEFAULT_FILTERS << { {{type}}, {{method}}, path, block }
|
#
|
||||||
|
# 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 %}
|
{% end %}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue