0.01
A long-lived project that still receives updates
Super-fast router class for Rack application, derived from Keight.rb. See for details.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

 Project Readme

Rack::JetRouter

($Release: 1.4.0 $)

Rack::JetRouter is crazy-fast router library for Rack application, derived from Keight.rb.

Rack::JetRouter requires Ruby >= 2.4.

Benchmark

Benchmark script is here.

Name Version
Ruby 3.2.2
Rack 2.2.8
Rack::JetRouter 1.3.0
Rack::Multiplexer 0.0.8
Sinatra 3.1.0
Keight.rb 1.0.0
Hanami::Router 2.0.2

(Macbook Pro, Apple M1 Pro, macOS Ventura 13.6.2)

JetRouter vs. Rack vs. Sinatra vs. Keight.rb vs. Hanami:

## Ranking                         usec/req  Graph (longer=faster)
(JetRouter)      /api/aaa01          0.2816 (100.0%) ********************
(Multiplexer)    /api/aaa01          1.2586 ( 22.4%) ****
(Hanami::Router) /api/aaa01          1.3296 ( 21.2%) ****
(JetRouter)      /api/aaa01/123      1.5861 ( 17.8%) ****
(Rack::Req+Res)  /api/aaa01/123      1.7369 ( 16.2%) ***
(Rack::Req+Res)  /api/aaa01          1.7438 ( 16.1%) ***
(Keight)         /api/aaa01          1.8906 ( 14.9%) ***
(Keight)         /api/aaa01/123      2.8998 (  9.7%) **
(Multiplexer)    /api/aaa01/123      2.9166 (  9.7%) **
(Hanami::Router) /api/aaa01/123      4.0996 (  6.9%) *
(Sinatra)        /api/aaa01         49.6862 (  0.6%)
(Sinatra)        /api/aaa01/123     54.3448 (  0.5%)
  • If URL path has no path parameter (such as /api/hello), JetRouter is significantly fast.
  • If URL path contains path parameter (such as /api/hello/:id), JetRouter becomes slower, but it is enough small (about 1.3 usec/req).
  • Overhead of JetRouter is smaller than that of Rack::Reqeuast + Rack::Response.
  • Hanami is slower than JetRouter, but quite enough fast.
  • Sinatra is too slow.

JetRouter vs. Rack::Multiplexer:

## Ranking                         usec/req  Graph (longer=faster)
(JetRouter)      /api/aaa01          0.2816 (100.0%) ********************
(JetRouter)      /api/zzz26          0.2823 ( 99.7%) ********************
(Multiplexer)    /api/aaa01          1.2586 ( 22.4%) ****
(JetRouter)      /api/aaa01/123      1.5861 ( 17.8%) ****
(Multiplexer)    /api/aaa01/123      2.9166 (  9.7%) **
(JetRouter)      /api/zzz26/456      3.5767 (  7.9%) **
(Multiplexer)    /api/zzz26         14.8423 (  1.9%)
(Multiplexer)    /api/zzz26/456     16.8930 (  1.7%)
  • JetRouter is about 4~6 times faster than Rack::Multiplexer.
  • Rack::Multiplexer is getting worse in promotion to the number of URL paths.

JetRouter vs. Hanami::Router

## Ranking                         usec/req  Graph (longer=faster)
(JetRouter)      /api/aaa01          0.2816 (100.0%) ********************
(JetRouter)      /api/zzz26          0.2823 ( 99.7%) ********************
(Hanami::Router) /api/zzz26          1.3280 ( 21.2%) ****
(Hanami::Router) /api/aaa01          1.3296 ( 21.2%) ****
(JetRouter)      /api/aaa01/123      1.5861 ( 17.8%) ****
(JetRouter)      /api/zzz26/456      3.5767 (  7.9%) **
(Hanami::Router) /api/zzz26/456      4.0898 (  6.9%) *
(Hanami::Router) /api/aaa01/123      4.0996 (  6.9%) *
  • Hanami is slower than JetRouter, but it has enough speed.

Examples

#1: Depends only on Request Path

require 'rack'
require 'rack/jet_router'

## Assume that welcome_app, books_api, ... are Rack application.
mapping = {
    "/"                       => welcome_app,
    "/api" => {
        "/books" => {
            ""                => books_api,
            "/:id(.:format)"  => book_api,
            "/:book_id/comments/:comment_id" => comment_api,
        },
    },
    "/admin" => {
        "/books"              => admin_books_app,
    },
}

router = Rack::JetRouter.new(mapping)
p router.lookup('/api/books/123.json')
    #=> [book_api, {"id"=>"123", "format"=>"json"}]

env = Rack::MockRequest.env_for("/api/books/123.json", method: 'GET')
status, headers, body = router.call(env)

#2: Depends on both Request Path and Method

require 'rack'
require 'rack/jet_router'

## Assume that welcome_app, book_list_api, ... are Rack application.
mapping = {
    "/"                       => {GET: welcome_app},   # not {"GET"=>...}
    "/api" => {
        "/books" => {            # not {"GET"=>..., "POST"=>...}
            ""                => {GET: book_list_api, POST: book_create_api},
            "/:id(.:format)"  => {GET: book_show_api, PUT: book_update_api},
            "/:book_id/comments/:comment_id" => {POST: comment_create_api},
        },
    },
    "/admin" => {
        "/books"              => {ANY: admin_books_app},   # not {"ANY"=>...}
    },
}

router = Rack::JetRouter.new(mapping)
p router.lookup('/api/books/123')
    #=> [{"GET"=>book_show_api, "PUT"=>book_update_api}, {"id"=>"123", "format"=>nil}]

env = Rack::MockRequest.env_for("/api/books/123", method: 'GET')
status, headers, body = router.call(env)

Notice that {GET: ..., PUT: ...} is converted into {"GET"=>..., "PUT"=>...} automatically when passing to Rack::JetRouter.new().

#3: RESTful Framework

require 'rack'
require 'rack/jet_router'

class API
  def initialize(request, response)
    @request  = request
    @response = response
  end
  attr_reader :request, :response
end

class BooksAPI < API
  def index(); ....; end
  def create(); ....; end
  def show(id); ....; end
  def update(id: nil); ....; end
  def delete(id: nil); ....; end
end

mapping = {
    "/api" => {
        "/books" => {  # not {"GET"=>..., "POST"=>...}
            ""      => {GET:    [BooksAPI, :index],
                        POST:   [BooksAPI, :create]},
            "/:id"  => {GET:    [BooksAPI, :show],
                        PUT:    [BooksAPI, :update],
                        DELETE: [BooksAPI, :delete]},
        },
    },
}
router = Rack::JetRouter.new(mapping)
dict, args = router.lookup("/api/books/123")
p dict   #=> {"GET"=>[BooksAPI, :show], "PUT"=>[...], "DELETE"=>[...]}
p args   #=> {"id"=>"123"}
klass, method_name = dict["GET"]
handler = klass.new(Rack::Request.new(env), Rack::Response.new)
handler.__send__(method_name, args)

Topics

Nested Hash v.s. Nested Array

URL path mapping can be not only nested Hash but also nested Array.

## nested Array
mapping = [
    ["/api", [
        ["/books", [
            [""      , book_list_api],
            ["/:id"  , book_show_api],
        ]],
    ]],
]

## nested Hash
mapping = {
    "/api" => {
        "/books" => {
            ""      => book_list_api,
            "/:id"  => book_show_api,
        },
    },
}

When using nested Hash, request method mappings should be {GET: ...} instead of {"GET"=>...}, because with the latter it is hard to distinguish between URL path mapping and request method mapping.

## OK
mapping = {
    "/api" => {
        "/books" => {
            ""      => {GET: book_list_api, POST: book_create_api},
            "/:id"  => {GET: book_show_api, PUT: book_update_api},
        },
    },
}

## Not OK
mapping = {
    "/api" => {
        "/books" => {
            ""      => {"GET"=>book_list_api, "POST"=>book_create_api},
            "/:id"  => {"GET"=>book_show_api, "PUT"=>book_update_api},
        },
    },
}

URL Path Parameters

In Rack application, URL path parameters (such as {"id"=>"123"}) are available via env['rack.urlpath_params'].

BookApp = proc {|env|
  p env['rack.urlpath_params']   #=> {"id"=>"123"}
  [200, {}, []]
}

Key name can be changed by env_key: keyword argument of JetRouter.new().

router = Rack::JetRouter.new(mapping, env_key: "rack.urlpath_params")

If you want to tweak URL path parameters, define subclass of Rack::JetRouter and override #build_param_values(names, values).

class MyRouter < JetRouter

  def build_param_values(names, values)
    return names.zip(values).each_with_object({}) {|(k, v), d|
      ## converts urlpath pavam value into integer
      v = v.to_i if k == 'id' || k.end_with?('_id')
      d[k] = v
    }
  end

end

File Path Type Parameters

If path parameter is *foo instead of :foo, that parameter matches to any path.

## Assume that book_api and staticfile_app are Rack application.
mapping = {
    "/api" => {
        "/books" => {
            "/:id"  => book_api,
        },
    },
    "/static/*filepath" => staticfile_app,   # !!!
}

router = Rack::JetRouter.new(mapping)
app, args = router.lookup("/static/images/logo.png") # !!!
p app    #=> staticfile_app
p args   #=> {"filepath"=>"images/logo.png"}

*foo should be at end of the URL path. For example, /static/*filepath is OK, while "/static/*filepath.html" or "/static/(*filepath)" raises error.

Integer Type Parameters

Keyword argument int_param: of JetRouter.new() specifies parameter name pattern (regexp) to treat as integer type. For example, int_param: /(?:\A|_)id\z/ treats id or xxx_id parameter values as integer type.

require 'rack'
require 'rack/jet_router'

rack_app = proc {|env|
  params = env['rack.urlpath_params']
  type = params["book_id"].class
  text = "params=#{params.inspect}, type=#{type}"
  [200, {}, [text]]
}

mapping = {
  "/api/books/:book_id" => rack_app,
}
router = Rack::JetRouter.new(mapping, int_param: /(?:\A|_)id\z/

env = Rack::MockRequest.env_for("/api/books/123")
tuple = router.call(env)
puts tuple[2]     #=> params={"book_id"=>123}, type=Integer

Integer type parameters match to only integers.

env = Rack::MockRequest.env_for("/api/books/FooBar")
tuple = router.call(env)
puts tuple[2]     #=> 404 Not Found

URL Path Multiple Extension

It is available to specify multiple extension of URL path.

mapping = {
    "/api/books" => {
        "/:id(.html|.json)"  => book_api,
    },
}

In above example, the following URL path patterns are enabled.

  • /api/books/:id
  • /api/books/:id.html
  • /api/books/:id.json

Notice that env['rack.urlpath_params']['format'] is not set because :format is not specified in URL path pattern.

Auto-redirection

Rack::JetRouter implements auto-redirection.

  • When /foo is provided and /foo/ is requested, then Rack::JetRouter redirects to /foo automatically.
  • When /foo/ is provided and /foo is requested, then Rack::JetRouter redirects to /foo/ automatically.

Notice that auto-redirection is occurred only on GET or HEAD methods, because browser cannot handle redirection on POST, PUT, and DELETE methods correctly. Don't depend on auto-redirection feature so much.

Variable URL Path Cache

It is useful to classify URL path patterns into two types: fixed and variable.

  • Fixed URL path pattern doesn't contain any urlpath paramters.
    Example: /, /login, /api/books
  • Variable URL path pattern contains urlpath parameters.
    Example: /api/books/:id, /index(.:format)

Rack::JetRouter caches only fixed URL path patterns in default. It is possible for Rack::JetRouter to cache variable URL path patterns as well as fixed ones. It will make routing much faster.

## Enable variable urlpath cache.
router = Rack::JetRouter.new(urlpath_mapping, urlpath_cache_size: 200)
p router.lookup('/api/books/123')   # caches even varaible urlpath

Custom Error Response

class MyRouter < Rack::JetRouter

  def error_not_found(env)
    html = ("<h2>404 Not Found</h2>\n" \
            "<p>Path: #{env['PATH_INFO']}</p>\n")
    [404, {"Content-Type"=>"text/html"}, [html]]
  end

  def error_not_allowed(env)
    html = ("<h2>405 Method Not Allowed</h2>\n" \
            "<p>Method: #{env['REQUEST_METHOD']}</p>\n")
    [405, {"Content-Type"=>"text/html"}, [html]]
  end

end

Above methods are invoked from Rack::JetRouter#call().

Todo

  • [_] support regular expression such as /books/{id:\d+}.

Copyright and License

$Copyright: copyright(c) 2015 kwatch@gmail.com $

$License: MIT License $