Skip to content

Commit

Permalink
Merge pull request #20 from tbrand/feature/api-to-action
Browse files Browse the repository at this point in the history
api to action
  • Loading branch information
tbrand authored Nov 3, 2017
2 parents 96afdd4 + 397a71b commit 87c9faa
Show file tree
Hide file tree
Showing 10 changed files with 171 additions and 206 deletions.
93 changes: 44 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
```

Expand All @@ -78,37 +98,12 @@ 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
Expand Down
78 changes: 34 additions & 44 deletions sample/sample.cr
Original file line number Diff line number Diff line change
@@ -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
35 changes: 21 additions & 14 deletions sample/tips.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
53 changes: 24 additions & 29 deletions spec/mock_server.cr
Original file line number Diff line number Diff line change
Expand Up @@ -6,45 +6,40 @@ 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|
context.response.print "params:#{params["id"]}"
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
Expand Down
2 changes: 0 additions & 2 deletions spec/route_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 15 additions & 2 deletions src/router.cr
Original file line number Diff line number Diff line change
@@ -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
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)
@route_handler.add_route("{{http_method.id.upcase}}" + path, block)
end
{% end %}
end
3 changes: 3 additions & 0 deletions src/router/handler.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
require "radix"
require "http/server"
require "./handler/handler"
Loading

0 comments on commit 87c9faa

Please sign in to comment.