From 072dcfe9134b187d9c25a7413d89b6ffd3490de6 Mon Sep 17 00:00:00 2001 From: tbrand Date: Fri, 3 Nov 2017 21:38:01 +0900 Subject: [PATCH 1/3] api to action --- README.md | 94 +++++++++++++++++------------------ sample/sample.cr | 78 +++++++++++++---------------- sample/tips.cr | 35 +++++++------ src/router.cr | 17 ++++++- src/router/handler.cr | 3 ++ src/router/handler/handler.cr | 37 ++++++++++++++ src/router/handlers.cr | 2 - src/router/handlers/router.cr | 64 ------------------------ 8 files changed, 155 insertions(+), 175 deletions(-) create mode 100644 src/router/handler.cr create mode 100644 src/router/handler/handler.cr delete mode 100644 src/router/handlers.cr delete mode 100644 src/router/handlers/router.cr diff --git a/README.md b/README.md index cbddf82..b5b17d2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ --- The default web server of the Crystal is quite good :smile: but it weak at routing :cry:. -Kemal is an awesome defacto standard web framework for Crystal :smile:, but it's too fat for some purpose :cry:. +Kemal or other web frameworks written in Crystal are awesome :smile:, but it's too fat for some purpose :cry:. **router.cr** is a **minimum** but **High Performance** middleware for Crystal web server. See the amazing performance of **router.cr** [here](https://github.com/tbrand/which_is_the_fastest).:rocket: @@ -36,38 +36,58 @@ class WebServer end ``` -In the following sample codes, `class WebServer ... end` will be omitted. -To initialize RouteHandler +Define a method to draw all routes for your web server. ```crystal -@route_handler = RouteHandler.new +class WebServer + include Router + + def draw_routes + # Drawing routes HERE! + end +end ``` -To define API, call API.new with `context` and `params`(optional) where context is HTTP::Server::Context and params is Hash(String, String). All APIs have to return the context at end of the method. In this example, params is omitted. (The usage of params is later) -```crystal -@index = API.new do |context| - context.response.print "Hello router.cr" - context # returning context -end +In that method, call HTTP method name (downcase) like `get` or `post` with PATH and BLOCK where + - PATH : String + - BLOCK : block of HTTP::Server::Context, Hash(String, String) -> HTTP::Server::Context ``` +class WebServer + include Router -Define your routes in a `draw` block. -```crystal -draw(@route_handler) do # Draw routes - get "/", @index + def draw_routes + get "/" do |context, params| + context.response.print "Hello router.cr!" + context + end + end end ``` -To activate the routes -```crystal -def run - server = HTTP::Server.new(3000, @route_handler) # Set RouteHandler to your server - server.listen +Here we've defined a GET route at root path (/) that just print out "Hello router.cr" when we get access. +To activate (run) the route, just define run methods for your server with route_handler +``` +class WebServer + include Router + + def draw_routes + get "/" do |context, params| + context.response.print "Hello router.cr!" + context + end + end + + def run + server = HTTP::Server.new(3000, route_handler) + server.listen + end end ``` +Here route_handler is getter defined in Router. So you can call `route_handler` at anywhere in WebServer instance. Finally, run your server. ```crystal web_server = WebServer.new +web_server.draw_routes web_server.run ``` @@ -78,41 +98,17 @@ See [sample](https://github.com/tbrand/router.cr/blob/master/sample/sample.cr) a `params` is a Hash(String, String) that is used when you define a path parameters such as `/user/:id` (`:id` is a parameters). Here is an example. ```crystal class WebServer - @route_handler = RouteHandler.new - - @user = API.new do |context, params| - context.response.print params["id"] # get :id in url from params - context - end - - def initialize - draw(@route_handler) do - get "/user/:id", @user - end - end -end -``` - -`params` also includes query params such as `/user?id=3`. Here is an example. -```crystal -class WebServer - @route_handler = RouteHandler.new - - @user = API.new do |context, params| - response_body = "user: " - # Get a query param like /user?id=3 - response_body += params["id"] if params.has_key?("id") - context.response.print response_body - context - end + include Router - def initialize - draw(@route_handler) do - get "/user", @user + def draw_routes + get "/user/:id" do |context, params| + context.response.print params["id"] # get :id in url from params + context end end end ``` +Note that `params` also includes query params such as `/user?id=3`. See [sample](https://github.com/tbrand/router.cr/blob/master/sample/sample.cr) and [tips]([sample](https://github.com/tbrand/router.cr/blob/master/sample/tips.cr)) for details. diff --git a/sample/sample.cr b/sample/sample.cr index 36e97bf..d89e321 100644 --- a/sample/sample.cr +++ b/sample/sample.cr @@ -1,64 +1,54 @@ require "../src/router" class WebServer - # Add Router functions to WebServer.class + # Add Router functions to WebServer include Router - # Initialize RouteHandler - @route_handler = RouteHandler.new - - # To define API, call API.new with context and params(optional) where - # context : HTTP::Server::Context - # params : Hash(String, String) - # - # HTTP::Server::Context is a default context of the http request/response - # This includes body, header, response and so on - # See https://crystal-lang.org/api/HTTP/Server/Context.html - # All API have to return HTTP::Server::Context - # For this, Basically, you just put the context on the last line of each API - - # GET "/" - @index = API.new do |context| - context.response.print "Hello router.cr" - context - end - - # params is used when you define parameters in your url such as '/user/:id' (path parameters) - # In this case, you can get the 'id' by params["id"] - # GET "/user/:id" - @user = API.new do |context, params| - context.response.print params["id"] # get :id in url from params - context + def initialize end + # Define a method to draw routes of your server + # Here we define + # GET "/" + # GET "/user/:id" # POST "/user" - @register_user = API.new do |context| - context - end + def draw_routes + # Define index access for this server + # We just print a result "Hello router.cr!" here + get "/" do |context, params| + context.response.print "Hello router.cr!" + context + end - def initialize - # Define routes in `draw` block - draw(@route_handler) do - get "/", @index - get "/user/:id", @user - post "/user", @register_user + # You can get path parameter from `params` param + # It's a Hash of String => String + get "/user/:id" do |context, params| + context.response.print params["id"] + context end - # Try - # curl localhost:3000 - # curl localhost:3000/user/3 - # curl localhost:3000/user -X POST + # Currently you can define a methods in following list + # get -> GET + # post -> POST + # put -> PUT + # patch -> PATCH + # delete -> DELETE + # options -> OPTIONS + # Here we define POST route + post "/user" do |context, params| + context + end end + # Running this server on port 3000 + # router_handler getter of RouteHandler + # that's defined in Router module def run - # set RouteHandler to your server - server = HTTP::Server.new(3000, @route_handler) + server = HTTP::Server.new(3000, route_handler) server.listen end end -# Initialize WebServer.class web_server = WebServer.new - -# Start running +web_server.draw_routes web_server.run diff --git a/sample/tips.cr b/sample/tips.cr index a708a2b..bed1131 100644 --- a/sample/tips.cr +++ b/sample/tips.cr @@ -19,33 +19,40 @@ class WebServer @log_handler = HTTP::LogHandler.new(STDOUT) @error_handler = HTTP::ErrorHandler.new @static_file_handler = HTTP::StaticFileHandler.new(File.expand_path("../public", __FILE__)) - @route_handler = RouteHandler.new - @index = API.new do |context| - context.response.print "OK" - context + def initialize end - def initialize - draw(@route_handler) do - get "/ok", @index + def draw_routes + get "/" do |context, params| + context.response.print "OK" + context end end def run + # Drawing routes for this server + draw_routes + # Please note about the order of the handlers. - # LogHandler should be the first of the array since it should get all accesses. - # ErrorHandler should be the next of the LogHandler to handle all errors. - # StatifFileHandler should be the next of the ErrorHandler since it should serve static files before accesses coming to RouteHandler. - # StaticFileHandler will pass the access to the RouteHandler if the file or directory does not exist. - # So RouteHandler should be last. + # 1. LogHandler should be the first of the array + # since it should get all accesses. + # 2. ErrorHandler should be the next of the LogHandler to handle all errors. + # 3. StatifFileHandler should be the next of the ErrorHandler + # since it should serve static files before accesses coming to RouteHandler. + # 4. StaticFileHandler will pass the access to the RouteHandler + # if the file or directory does not exist. + # 5. So RouteHandler should be last. + # # The array of the handlers should be like this. - # Note: if a route can't be handled by RouteHandler (a.k.a. route not found) and this handler is the last, a 404 error response will be returned; otherwise the execution will continue with the next handler in a row + # Note: if a route can't be handled by RouteHandler (a.k.a. route not found) + # and this handler is the last, a 404 error response will be returned; + # otherwise the execution will continue with the next handler in a row handlers = [ @log_handler, @error_handler, @static_file_handler, - @route_handler, + route_handler, ] server = HTTP::Server.new(3000, handlers) diff --git a/src/router.cr b/src/router.cr index 77c75db..efbcfa7 100644 --- a/src/router.cr +++ b/src/router.cr @@ -1,5 +1,18 @@ -require "./router/*" +require "./router/handler" +require "./router/version" module Router - # Main module + alias Action = HTTP::Server::Context, Hash(String, String) -> HTTP::Server::Context + record RouteContext, action : Action, params : Hash(String, String) + getter route_handler : RouteHandler = RouteHandler.new + + HTTP_METHODS = %w(get post put patch delete options) + + # Define each method for supported http methods + {% for http_method in HTTP_METHODS %} + def {{http_method.id}}(path : String, + &block : Action) + @route_handler.add_route("{{http_method.id.upcase}}" + path, block) + end + {% end %} end diff --git a/src/router/handler.cr b/src/router/handler.cr new file mode 100644 index 0000000..b1b79c5 --- /dev/null +++ b/src/router/handler.cr @@ -0,0 +1,3 @@ +require "radix" +require "http/server" +require "./handler/handler" diff --git a/src/router/handler/handler.cr b/src/router/handler/handler.cr new file mode 100644 index 0000000..fb306df --- /dev/null +++ b/src/router/handler/handler.cr @@ -0,0 +1,37 @@ +module Router + class RouteHandler + include HTTP::Handler + + def initialize + @tree = Radix::Tree(Action).new + end + + def search_route(context : HTTP::Server::Context) : RouteContext? + method = context.request.method + route = @tree.find(method.upcase + context.request.path) + + # Merge query params into path params + context.request.query_params.each do |k, v| + route.params[k] = v unless route.params.has_key?(k) + end + + if route.found? + return RouteContext.new(route.payload, route.params) + end + + nil + end + + def call(context : HTTP::Server::Context) + if route_context = search_route(context) + route_context.action.call(context, route_context.params) + else + call_next(context) + end + end + + def add_route(key : String, action : Action) + @tree.add(key, action) + end + end +end diff --git a/src/router/handlers.cr b/src/router/handlers.cr deleted file mode 100644 index 9aa18ff..0000000 --- a/src/router/handlers.cr +++ /dev/null @@ -1,2 +0,0 @@ -require "http/server" -require "./handlers/*" diff --git a/src/router/handlers/router.cr b/src/router/handlers/router.cr deleted file mode 100644 index 16d95ef..0000000 --- a/src/router/handlers/router.cr +++ /dev/null @@ -1,64 +0,0 @@ -require "radix" - -module Router - # Alias of API - alias API = Proc(HTTP::Server::Context, Hash(String, String), HTTP::Server::Context) - - # This includes API and url parametes - record RouteContext, api : API, params : Hash(String, String) - - class RouteHandler - include HTTP::Handler - - def initialize - @tree = Radix::Tree(API).new - end - - def search_route(context : HTTP::Server::Context) : RouteContext? - method = context.request.method - route = @tree.find(method.upcase + context.request.path) - - # Merge query params into path params - context.request.query_params.each do |k, v| - route.params[k] = v unless route.params.has_key?(k) - end - - if route.found? - return RouteContext.new(route.payload, route.params) - end - - nil - end - - def call(context : HTTP::Server::Context) - if route_context = search_route(context) - route_context.api.call(context, route_context.params) - else - call_next(context) - end - end - - def add_route(key : String, api : API) - @tree.add(key, api) - end - end - - # RouteHandler to be drawn - @tmp_route_handler : RouteHandler? - - def draw(route_handler : RouteHandler?, &block) - @tmp_route_handler = route_handler - yield - @tmp_route_handler = nil - end - - # Supported http methods - HTTP_METHODS = %w(get post put patch delete options) - - {% for http_method in HTTP_METHODS %} - def {{http_method.id}}(path : String, api : API) - abort "Please call `{{http_method.id}}` in `draw`" if @tmp_route_handler.nil? - @tmp_route_handler.not_nil!.add_route("{{http_method.id.upcase}}" + path, api) - end - {% end %} -end From 63a8be5699210954d74460b5269d7c62fb49e833 Mon Sep 17 00:00:00 2001 From: tbrand Date: Fri, 3 Nov 2017 21:51:06 +0900 Subject: [PATCH 2/3] fix spec --- spec/mock_server.cr | 55 +++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/spec/mock_server.cr b/spec/mock_server.cr index 71b4a3c..b6bcf04 100644 --- a/spec/mock_server.cr +++ b/spec/mock_server.cr @@ -6,45 +6,42 @@ class MockServer @server : HTTP::Server? @route_handler = RouteHandler.new - @index = API.new do |context| - context.response.print "index" - context - end + def draw_routes + get "/" do |context, params| + context.response.print "index" + context + end - @param = API.new do |context, params| - result_body = "params:#{params["id"]}" - result_body += ", query_params:#{params["q"]}" if params.has_key?("q") - context.response.print result_body - context - end + get "/params/:id" do |context, params| + result_body = "params:#{params["id"]}" + result_body += ", query_params:#{params["q"]}" if params.has_key?("q") + context.response.print result_body + context + end - @test_param = API.new do |context, params| - context.response.print "params:#{params["id"]}, #{params["test_id"]}" - context - end + get "/params/:id/test/:test_id" do |context, params| + context.response.print "params:#{params["id"]}, #{params["test_id"]}" + context + end - @post_test = API.new do |context, params| - context.response.print "ok" - context - end + post "/post_test" do |context, params| + context.response.print "ok" + context + end - @put_test = API.new do |context, params| - context.response.print "ok" - context + put "/put_test" do |context, params| + context.response.print "ok" + context + end end def initialize(@port : Int32) - draw(@route_handler) do - get "/", @index - get "/params/:id", @param - get "/params/:id/test/:test_id", @test_param - put "/put_test", @put_test - post "/post_test", @post_test - end end def run - @server = HTTP::Server.new(@port, [@route_handler]).listen + draw_routes + + @server = HTTP::Server.new(@port, [route_handler]).listen end def close From 397a71bffaf7ecc0d18929dedd22d9e38f88c1f8 Mon Sep 17 00:00:00 2001 From: tbrand Date: Fri, 3 Nov 2017 23:11:07 +0900 Subject: [PATCH 3/3] fix --- README.md | 1 - spec/mock_server.cr | 4 +--- spec/route_spec.cr | 2 -- src/router.cr | 6 +++--- src/router/handler/handler.cr | 11 ++--------- 5 files changed, 6 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index b5b17d2..df9460a 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,6 @@ class WebServer end end ``` -Note that `params` also includes query params such as `/user?id=3`. See [sample](https://github.com/tbrand/router.cr/blob/master/sample/sample.cr) and [tips]([sample](https://github.com/tbrand/router.cr/blob/master/sample/tips.cr)) for details. diff --git a/spec/mock_server.cr b/spec/mock_server.cr index b6bcf04..d440064 100644 --- a/spec/mock_server.cr +++ b/spec/mock_server.cr @@ -13,9 +13,7 @@ class MockServer end get "/params/:id" do |context, params| - result_body = "params:#{params["id"]}" - result_body += ", query_params:#{params["q"]}" if params.has_key?("q") - context.response.print result_body + context.response.print "params:#{params["id"]}" context end diff --git a/spec/route_spec.cr b/spec/route_spec.cr index 20f3752..8e17171 100644 --- a/spec/route_spec.cr +++ b/spec/route_spec.cr @@ -21,8 +21,6 @@ describe Router do result.not_nil!.body.should eq("params:1") result = curl("GET", "/params/2") result.not_nil!.body.should eq("params:2") - result = curl("GET", "/params/2?q=hoge") - result.not_nil!.body.should eq("params:2, query_params:hoge") end it "#test_param" do diff --git a/src/router.cr b/src/router.cr index efbcfa7..3df0193 100644 --- a/src/router.cr +++ b/src/router.cr @@ -3,15 +3,15 @@ require "./router/version" module Router alias Action = HTTP::Server::Context, Hash(String, String) -> HTTP::Server::Context - record RouteContext, action : Action, params : Hash(String, String) + alias RouteContext = NamedTuple(action: Action, params: Hash(String, String)) + getter route_handler : RouteHandler = RouteHandler.new HTTP_METHODS = %w(get post put patch delete options) # Define each method for supported http methods {% for http_method in HTTP_METHODS %} - def {{http_method.id}}(path : String, - &block : Action) + def {{http_method.id}}(path : String, &block : Action) @route_handler.add_route("{{http_method.id.upcase}}" + path, block) end {% end %} diff --git a/src/router/handler/handler.cr b/src/router/handler/handler.cr index fb306df..6f4e61d 100644 --- a/src/router/handler/handler.cr +++ b/src/router/handler/handler.cr @@ -10,21 +10,14 @@ module Router method = context.request.method route = @tree.find(method.upcase + context.request.path) - # Merge query params into path params - context.request.query_params.each do |k, v| - route.params[k] = v unless route.params.has_key?(k) - end + return { action: route.payload, params: route.params } if route.found? - if route.found? - return RouteContext.new(route.payload, route.params) - end - nil end def call(context : HTTP::Server::Context) if route_context = search_route(context) - route_context.action.call(context, route_context.params) + route_context[:action].call(context, route_context[:params]) else call_next(context) end