Keight.rb README

($Release: 0.2.0 $)

Overview

Keight.rb is the very fast web application framework for Ruby. It is about 100 times faster than Rails and 20 times faster than Sinatra.

Keight.rb is under development and is subject to change without notice.

Benchmarks

Measured with app.call(env) style in order to exclude server overhead:

Framework Request usec/req req/sec
Rails GET /api/hello 738.7 1353.7
Rails GET /api/hello/123 782.2 1278.4
Sinatra GET /api/hello 144.1 6938.3
Sinatra GET /api/hello/123 158.4 6313.8
Rack GET /api/hello 9.9 101050.9
Keight GET /api/hello 7.6 132432.8
Keight GET /api/hello/123 10.6 94705.9
  • Ruby 2.2.3
  • Rails 4.2.4
  • Sinatra 1.4.6
  • Rack 1.6.4 (= Rack::Request + Rack::Response)
  • Keight 0.1.0

(Script: https://gist.github.com/kwatch/c634e91d2d6a6c4b1d40 )

Quick Tutorial

$ ruby -v     # required Ruby >= 2.0
ruby 2.2.3p173 (2015-08-18 revision 51636) [x86_64-darwin14]
$ mkdir gems
$ export GEM_HOME=$PWD/gems
$ export PATH=$GEM_HOME/bin:$PATH

$ gem install keight
$ vi hello.rb      # see below
$ vi config.ru     # != 'config.rb'
$ rackup -p 8000 config.ru

hello.rb:

# -*- coding: utf-8 -*-
require 'keight'

class HelloAction < K8::Action

  mapping ''       , :GET=>:do_index, :POST=>:do_create
  mapping '/{id}'  , :GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete

  def do_index
    #{"message"=>"Hello"}   # JSON
    "<h1>Hello</h1>"        # HTML
  end

  def do_show(id)
    ## 'id' or 'xxx_id' will be converted into integer.
    "<h1>Hello: id=#{id.inspect}</h1>"
  end

  def do_create    ; "<p>create</p>"; end
  def do_update(id); "<p>update</p>"; end
  def do_delete(id); "<p>delete</p>"; end

end

config.ru:

# -*- coding: utf-8 -*-
require 'keight'
require './hello'

mapping = [
  ['/api', [
    ['/hello'     , HelloAction],
    ## or
    #['/hello'    , "./hello:HelloAction"],
  ]],
]
app = K8::RackApplication.new(mapping)

run app

Open http://localhost:8000/api/hello or http://localhost:8000/api/hello/123 with your browser.

Do you like it? If so, try k8rb project myapp1 to generate project skeleton.

$ mkdir gems                         # if necessary
$ export GEM_HOME=$PWD/gems          # if necessary
$ export PATH=$GEM_HOME/bin:$PATH    # if necessary
$ gem install -N keight
$ k8rb help                          # show help
$ k8rb project myapp1                # create new project
$ cd myapp1/
$ rake setup                         # install gems and download libs
$ export APP_ENV=dev    # 'dev', 'prod', or 'stg'
$ k8rb help mapping
$ k8rb mapping                       # list urlpath mappings
$ k8rb mapping --format=javascript   # or jquery,angular,json,yaml
$ k8rb configs                       # list config parameters
$ rake server port=8000
$ open http://localhost:8000/
$ ab -n 10000 -c 100 http://localhost:8000/api/hello

CheatSheet

require 'keight'

class HelloAction < K8::Action

  ## mapping
  mapping '',              :GET=>:do_hello_world
  mapping '/{name:\w+}',   :GET=>:do_hello

  ## request, response, and helpers

  def do_hello_world
    do_hello('World')
  end

  def do_hello(name)

    ## request
    @req                   # K8::Request object (!= Rack::Request)
    @req.env               # Rack environment
    @req.method            # ex: :GET, :POST, :PUT, ...
    @req.request_method    # ex: "GET", "POST", "PUT", ...
    @req.path              # ex: '/api/hello'
    @req.query             # query string (Hash)
    @req.form              # form data (Hash)
    @req.multipart         # multipart form data ([Hash, Hash])
    @req.json              # JSON data (Hash)
    @req.params            # query, form, multipart or json
    @req.cookies           # cookies (Hash)
    @req.xhr?              # true when requested by jQuery etc
    @req.client_ip_addr    # ex: '127.0.0.1'

    ## response
    @resp                  # K8::Response object (!= Rack::Response)
    @resp.status_code      # ex: 200
    @resp.status           # alias of @resp.status_code
    @resp.headers          # Hash object
    @resp.set_cookie(k, v) # cookie
    @resp.content_type     # same as @resp.headers['Content-Type']
    @resp.content_length   # same as @resp.headers['Content-Length'].to_i

    ## session (requires Rack::Session)
    @sess[key] = val       # set session data
    @sess[key]             # get session data

    ## helpers
    token = csrf_token()   # get csrf token
    validation_failed()    # same as @resp.status_code = 422
    return redirect_to(location, 302, flash: "message")
    return send_file(filepath)

  end


  ## hook methods

  def before_action        # setup
    super
  end

  def after_action(ex)     # teardown
    super
  end

  def handle_content(content)   # convert content
    if content.is_a?(Hash)
      @resp.content_type = 'application/json'
      return [JSON.dump(content)]
    end
    super
  end

  def handle_exception(ex)   # exception handler
    meth = "on_#{ex.class.name}"
    return __send__(meth, ex) if respond_to?(meth)
    super
  end

  def csrf_protection_required?
    x = @req.method
    return x == :POST || x == :PUT || x == :DELETE
  end

end

## urlpath mapping
urlpath_mapping = [
  ['/'               , './app/page/welcome:WelcomePage'],
  ['/api', [
    ['/books'        , './app/api/books:BooksAPI'],
    ['/books/{book_id}/comments'
                     , './app/api/book_comments:BookCommentsAPI'],
    ['/orders'       , './app/api/orders:OrdersAPI'],
  ]],
]

## application
opts = {
  urlpath_cache_size:  0,    # 0 means cache disabled
}
app = K8::RackApplication.new(urlpath_mapping, opts)

## misc
p HelloAction[:do_update].method        #=> :GET
p HelloAction[:do_update].urlpath(123)  #=> "/api/books/123"
p HelloAction[:do_update].form_action_attr(123)
                                        #=> "/api/books/123?_method=PUT"

Topics

Make Routing More Faster

Specify urlpath_cache_size: n (where n is 100 or so) to K8::RackApplication.new().

urlpath_mapping = [
  ....
]
rack_app = K8::RackApplication.new(urlpath_mapping,
                                   urlpath_cache_size: 100)  # !!!

In general, there are two type of URL path pattern: fixed and variable.

  • Fixed URL path pattern doesn't contain any URL path parameter.
    Example: /, /api/books, /api/books/new.
  • Variable URL path pattern contains one or more URL path parameters.
    Example: /api/books/{id}, /api/books/new.{format:html|json}.

Keight.rb caches fixed patterns and doesn't variable ones, therefore routing for fixed URL path is faster than variable one.

If urlpath_cache_size: n is specified, Keight.rb caches latest n entries of request path matched to variable URL path pattern. This will make routing for variable one much faster.

Default Pattern of URL Path Parameter

URL path parameter {id} and {xxx_id} are regarded as {id:\d+} and {xxx_id:\d+} respectively and converted into positive interger automatically. For example:

class BooksAction < K8::Action
  mapping '/{id}', :GET=>:do_show
  def do_show(id)
    p id.class    #=> Fixnum
    ....
  end
end

URL path parameter {date} and {xxx_date} are regarded as {date:\d\d\d\d-\d\d-\d\d} and {xxx_date:\d\d\d\d-\d\d-\d\d} respectively and converted into Date object automatically. For example:

class BlogAPI < K8::Action
  mapping '/{date}', :GET=>:list_entries
  def list_entries(date)
    p date.class    #=> Date
    ....
  end
end

If you specify {id:\d+} or {date:\d\d\d\d-\d\d-\d\d} explicitly, URL path parameter value is not converted into integer nor Date object. In other words, you can cancel automatic conversion by specifing regular expression of URL path parameters.

Nested Routing

urlpath_mapping = [
    ['/api', [
        ['/books'                      , BookAPI],
        ['/books/{book_id}/comments'   , BookCommentsAPI],
    ]],
]

URL Path Helpers

p BooksAPI[:do_index].method          #=> :GET
p BooksAPI[:do_index].urlpath()       #=> "/api/books"

p BooksAPI[:do_update].method         #=> :PUT
p BooksAPI[:do_update].urlpath(123)   #=> "/api/books/123"
p BooksAPI[:do_update].form_action_attr(123)   #=> "/api/books/123?_method=PUT"

(Notice that these are availabe after K8::RackApplication object is created.)

Routing for JavaScript

Keight.rb can generate JavaScript routing file.

$ k8rb project myapp1
$ cd myapp1/
$ k8rb mapping --format=javascript | less  # or 'jquery', 'angular'
$ mkdir -p static/js
$ jsfile=static/js/urlpath_mapping.js
$ rm -f $jsfile
$ echo 'var Mapping = {'           >> $jsfile
$ k8rb mapping --format=javascript >> $jsfile
$ echo '};'                        >> $jsfile

Download JavaScript Libraries

Keight.rb can download Javascript or CSS libraries from cdnjs.com. It is good idea to make layout of JavaScript libraries to be same as CDN.

$ k8rb project myapp1
$ cd myapp1/
$ k8rb help cdnjs
$ k8rb cdnjs                 # list libraries
$ k8rb cdnjs 'jquery*'       # search libraries
$ k8rb cdnjs jquery          # list versions
$ k8rb cdnjs jquery 2.1.4    # download library
## or
$ k8rb cdnjs --basedir=static/lib jquery 2.1.4

FAQ

Why Keight.rb is so fast?

I don't think Keight.rb is so fast. Other frameworks are just too slow.

You should know that Rails is slow due to Rails itself, and not to Ruby.

How to setup template engine?

Try k8rb project myapp1; cd myapp1; less app/action.rb.

How to support static files?

Try k8rb project myapp1; cd myapp1; less app/action.rb.

How to setup session?

Try k8rb project myapp1; cd myapp1; less config.ru.

Can I use Rack::Request and Rack::Response instead of Keight's?

Try K8::REQUEST_CLASS = Rack::Request; K8::RESPONSE_CLASS = Rack::Response.

$License: MIT License $

$Copyright: copyright(c) 2014-2016 kuwata-lab.com all rights reserved $