/keight

Jet-speed webapp framework

Primary LanguageRubyOtherNOASSERTION

Keight.rb README

($Release: 0.0.0 $)

Overview

Keight.rb is the very fast web application framework for Ruby. It runs 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 4423 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
    return {"message"=>"Hello"}   # JSON
    #return "<h1>Hello</h1>"      # HTML
  end

  def do_show(id)
    ## 'id' or 'xxx_id' will be converted into integer.
    return {"id"=>id}
    #return "<h1>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'

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

run app

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

How do you like it? Try the following steps to generate your project.

$ mkdir gems                         # if necessary
$ export GEM_HOME=$PWD/gems          # if necessary
$ export PATH=$GEM_HOME/bin:$PATH    # if necessary

$ gem install keight    # or: gem install boilerpl8
$ k8rb project myapp1   # or: boilerpl8 github:kwatch/keight-ruby myapp1
## select CSS framework
**    1. None
**    2. Bootstrap
**    3. Pure (recommended)
** Which CSS framework do you like? [1-3]: 1

$ cd myapp1
$ export APP_MODE=dev                # 'dev', 'prod', or 'stg'
$ rake -T
$ ls public
$ rake server port=4423
$ open http://localhost:4423/
$ ab -n 10000 -c 100 http://localhost:4423/api/hello.json

Command k8rb

Keight.rb provides k8rb.

  • k8rb project myapp1 creates new project.
    (Note: This is equivalent to boilerpl8 github:kwatch/keight-ruby myapp1.)

  • k8rb cdnjs -d static/lib jquery 3.1.0 downloads jQuery files from cdnjs.com and stores into static/lib directory.
    (Note: This is equivalent to cdnget cdnjs jquery 3.1.0 static/lib.)

k8rb command is provided mainly for backward compatibility. You can use boilerpl8 and cdnget inead of k8rb project and k8rb cdnjs. k8rb is specific to Keight.rb, but both boilerpl8 and cdnget are available in any project.

CheatSheet

require 'keight'

class HelloAction < K8::Action

  ## mapping
  mapping ''             , :GET=>:do_hello_world
  mapping '/{name:str}'  , :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.meth              # ex: :GET, :POST, :PUT, ...
    @req.request_method    # ex: "GET", "POST", "PUT", ...
    @req.path              # ex: '/api/hello.json'
    @req.path_ext          # ex: '.json'
    @req.query_string      # query string (String)
    @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 or form (not multipart nor 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_code=(i)  # ex: i=200
    @resp.status_line      # ex: "200 OK"
    @resp.status           # alias of @resp.status_code
    @resp.status=(i)       # alias of @resp.status_code=(i)
    @resp.headers          # Hash object
    @resp.set_cookie(k, v) # cookie
    @resp.content_type        # same as @resp.headers['Content-Type']
    @resp.content_type=(s)    # same as @resp.headers['Content-Type'] = s
    @resp.content_length      # same as @resp.headers['Content-Length']
    @resp.content_length=(i)  # same as @resp.headers['Content-Length'] = i.to_s

    ## 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 = 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
    proc_ = WHEN_RAISED[ex.class]
    return self.instance_exec(ex, &proc_) if proc_
    super
  end

  WHEN_RAISED = {}
  WHEN_RAISED[NotFound]     = proc {|ex| ... }
  WHEN_RAISED[NotPermitted] = proc {|ex| ... }

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

end

## urlpath mapping
urlpath_mapping = [
  ['/'               , './app/page/welcome:WelcomePage'],
  ['/api', [
    ['/books'        , './app/api/books:BooksAPI'],
    ['/books/{book_id}/comments'
                     , './app/api/books: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].meth          #=> :PUT
p HelloAction[:do_update].path(123)     #=> "/api/books/123"
p HelloAction[:do_update].form_action_attr(123)
                                        #=> "/api/books/123?_method=PUT"

URL Mapping

Rails-like mapping:

class BookAPI < K8::Action

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

  def do_index()   ; {"list": []}; end
  def do_create()  ; {"list": []}; end
  def do_new()     ; {"item": id}; end
  def do_show(id)  ; {"item": id}; end
  def do_edit(id)  ; {"item": id}; end
  def do_update(id); {"item": id}; end
  def do_delete(id); {"item": id}; end
  # (Note: 'do_' prefix is NOT mandatory.)

end

Data type and pattern:

class BookAPI < K8::Action

  ##
  ## data type and pattern
  ##
  mapping '/{name:str<[^/]+>}' , :GET=>:do_show1
  mapping '/{id:int<\d+>}'     , :GET=>:do_show2
  mapping '/{birthday:date<\d\d\d\d-\d\d-\d\d>}', :GET=>:do_show3

  ##
  ## default pattern
  ##  - '/{name:str}' is same as '/{name:str<[^/]+>}'
  ##  - '/{id:int}' is same as '/{id:int<\d+>}'
  ##  - '/{birthday:date}' is same as '/{birthday:date<\d\d\d\d-\d\d-\d\d>}'
  ##
  mapping '/{name:str}'        , :GET=>:do_show1
  mapping '/{id:int}'          , :GET=>:do_show2
  mapping '/{birthday:date}'   , :GET=>:do_show3

  ##
  ## default data type
  ##  - 'id' and '*_id' are int type
  ##  - 'date' and '*_date' are date type
  ##  - others are str type
  ##
  mapping '/{name}'      , :GET=>:do_show1   # same as '{name:str}'
  mapping '/{id}'        , :GET=>:do_show2   # same as '{id:int}'
  mapping '/{birth_date}', :GET=>:do_show3   # same as '{birth_date:date}'

  ##
  ## pattern with default data type
  ##
  mapping '/{code:<[A-Z]{3}>}', ...  # same as '/{code:str<[A-Z]{3}>'
  mapping '/{id:<[1-9]\d*>}'  , ...  # same as '/{id:int<\d{4}>}'

end

Path extension (such as '.json' or '.html'):

class FooAPI < K8::Action

  ## ex: '.*' is same as '{_:<(?:\w+)?>}' which matches to any extension.
  mapping '.*'       , :GET=>:do_index

  ## ex: '/{id}.*' is same as '/{id}{_:<(?:\w+)?>}' which matches to any extension.
  mapping '/{id}.*'  , :GET=>:do_show

  def do_index
    p @req.path_ext    #=> ex: '.json', '.html', and so on
  end

  def do_show(id)
    p @req.path_ext    #=> ex: '.json', '.html', and so on
  end

end

URL mapping helper:

## for example
p BookAPI[:do_index].meth        #=> :GET
p BookAPI[:do_index].path        #=> "/api/books"
p BookAPI[:do_create].meth       #=> :POST
p BookAPI[:do_create].path       #=> "/api/books"
p BookAPI[:do_show].meth         #=> :GET
p BookAPI[:do_show].path(123)    #=> "/api/books/123"
p BookAPI[:do_update].meth       #=> :PUT
p BookAPI[:do_update].path(123)  #=> "/api/books/123"
p BookAPI[:do_delete].meth       #=> :DELETE
p BookAPI[:do_delete].path(123)  #=> "/api/books/123"

p BookAPI[:do_index ].form_action_attr()   #=> "/api/books"
p BookAPI[:do_create].form_action_attr()   #=> "/api/books"
p BookAPI[:do_show  ].form_action_attr(9)  #=> "/api/books/9"
p BookAPI[:do_update].form_action_attr(9)  #=> "/api/books/9?_method=PUT"
p BookAPI[:do_delete].form_action_attr(9)  #=> "/api/books/9?_method=DELETE"

Show URL mappings:

$ k8rb project myapp1   # or: boilerpl8 github:kwatch/keight-ruby myapp1
$ cd myapp1
$ rake mapping:text       # list url mapping
$ rake mapping:yaml       # list in YAML format
$ rake mapping:json       # list in JSON format
$ rake mapping:jquery     # list for jQuery
$ rake mapping:angularjs  # list for AngularJS

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 types 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 cache variable ones, therefore routing for a fixed URL path is faster than a variable one.

If urlpath_cache_size: n is specified, Keight.rb caches the 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:int<\d+>} and {xxx_id:int<\d+>} respectively and converted into positive integer 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:date<\d\d\d\d-\d\d-\d\d>} and {xxx_date: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 don't like auto-convert, specify data type and pattern explicitly. For example, {id:str<\d+>} or {date:str<\d\d\d\d-\d\d-\d\d>}.

Nested Routing

urlpath_mapping = [
    ['/api', [
        ['/books'                    , "./app/api/books:BookAPI"],
        ['/books/{book_id}/comments' , "./app/api/books:BookCommentsAPI"],
    ]],
]

URL Path Helpers

p BooksAPI[:do_index].meth         #=> :GET
p BooksAPI[:do_index].path()       #=> "/api/books"
p BooksAPI[:do_update].form_action_attr(123)   #=> "/api/books/123"

p BooksAPI[:do_update].meth        #=> :PUT
p BooksAPI[:do_update].path(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.new() is called.)

Routing for JavaScript

Keight.rb can generate JavaScript routing file.

$ k8rb project myapp1   # or: boilerpl8 github:kwatch/keight-ruby myapp1
$ cd myapp1
$ rake mapping:text         # list URL path mapping
$ rake mapping:yaml         # list in YAML format
$ rake mapping:json         # list in JSON format
$ rake mapping:jquery       # list for jQuery
$ rake mapping:angularjs    # list for AngularJS

Download JavaScript Libraries

Install cdnget gem in order to download client-side libraries such as jQuery or Bootstrap.

$ gem install cdnget
$ cdnget                                 # list CDN
$ cdnget cdnjs                           # list libraries
$ cdnget cdnjs 'jquery*'                 # search libraries
$ cdnget cdnjs jquery                    # list versions
$ cdnget cdnjs jquery 2.2.4              # list files
$ cdnget cdnjs jquery 2.2.4 static/lib   # download files
static/lib/jquery/2.2.4/jquery.js ... Done (257,551 byte)
static/lib/jquery/2.2.4/jquery.min.js ... Done (85,578 byte)
static/lib/jquery/2.2.4/jquery.min.map ... Done (129,572 byte)

$ ls static/lib/jquery/2.2.4
jquery.js	jquery.min.js	jquery.min.map

FAQ

Why is Keight.rb so fast?

I don't think Keight.rb is that 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; less myapp1/app/action.rb.
(or boilerpl8 github:kwatch/keight-ruby myapp1; less myapp1/app/action.rb).

How to support static files?

Try k8rb project myapp1; less myapp1/app/action.rb.
(or boilerpl8 github:kwatch/keight-ruby myapp1; less myapp1/app/action.rb).

How to setup session?

Try k8rb project myapp1; less myapp1/app/config.ru.
(or boilerpl8 github:kwatch/keight-ruby myapp1; less myapp1/app/config.ru).

Is it necessary to add a do_ prefix to action methods?

No. You can define index() or show(id) instead of do_index() or do_show(id), like Ruby on Rails.

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

Try K8::RackApplication::REQUEST_CLASS = Rack::Request and K8::RackApplication::RESPONSE_CLASS = Rack::Response.

Why does @req.multipart return two Hash objects?

Because in order NOT to mix string objects and file objects in a hash.

## str_params contains only string or array of string.
## file_params contains only file object or array of file.
str_params, file_params = @req.multipart
p str_params['name'].strip
    #=> no error because str_params['name'] is a string

## It is easy to merge two hash objects (but not recommended).
p str_params.merge(file_params)

In contrast, Rack::Request#POST() may contain both string and file.

req = Rack::Request.new(env)
p req.POST['name'].strip
    #=> will raise error when req.POST['name'] is file object uploaded

Why does @req.params raise error when multipart form data?

Because @req.multipart returns two Hash objects. See above section.

Why does @req.params raise error for JSON data?

Because both @req.query and @req.form return a Hash object containing only string values, but @req.json returns a Hash object containing non-string values such as integer or boolean.

Use @req.json instead of @req.params for JSON data.

License and Copyright

$License: MIT License $

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