(追記:2012-12-25) 本記事およびこれに続くRackの記事(全4本)をまとめて電子書籍化しました。「Gumroad」を通して100円にて販売しています。内容についての追加・変更はありませんが、誤記の修正およびメディア向けの調整を行っています。

Rack Ebook

電子書籍「エラーメッセージから学ぶRack」EPUB版

このリンクはGumroadにおける商品購入リンクになっています。クリックすると、オーバーレイ・ウインドウが立ち上がって、この場でクレジットカード決済による購入が可能です。購入にはクレジット情報およびメールアドレスの入力が必要になります。購入すると、入力したメールアドレスにコンテンツのDLリンクが送られてきます。

詳細は以下を参照して下さい。

電子書籍「エラーメッセージから学ぶRack」EPUB版をGumroadから出版しました!

購入ご検討のほどよろしくお願いしますm(__)m


Rackがわかりません。

Rackのサイトには、Rackについて次のように書いてあります。

Rack provides a minimal interface between webservers supporting Ruby and Ruby frameworks.

Rackは、Ruby向けWebサーバとRuby製フレームワークとの間の最小のインタフェースを提供します。

やっぱりよくわかりませんが、たぶんそれは、Ruby製Webフレームワークを作る人にとっては仮想Webサーバであり、またRuby向けWebサーバを作る人にとっては仮想Webフレームワークになるものだと理解します。

エラーメッセージから学ぶ

古くからの格言の一つに「Rackのことはrackupに聞け」というものがあります。Rackがわからないので、この格言に従いrackupに聞いてみることにします。

昨日はドラクエの発売日だったので、draqueというディレクトリを作って、ここでrackupを実行します。因みに私はドラクエは一度もやったことはありません。やっぱりそれは不幸なことですか?

% mkdir draque
% cd draque
% rackup
configuration config.ru not found

config.ruという設定ファイルがないと言われました。それでは、これを作って再度rackupします。

% touch config.ru
% rackup
~/.rbenv/..../rack/builder.rb:129:in `to_app': missing run or map statement (RuntimeError)
        from config.ru:1:in `<main>'

今度はrunまたはmapが見つからないと言われたので、config.ruにrunと書いてもう一度やってみます。

% echo run > config.ru 
% rackup              
~/.rbenv/.../rack/builder.rb:99:in `run': wrong number of arguments (0 for 1) (ArgumentError)
        from config.ru:2:in `block in <main>'

今度は引数が足りないと言われました。runは恐らくWebアプリを走らせるコマンドでしょうから、Webアプリのインスタンスを渡せばよさそうです。試しに1を渡してみます。

# config.ru
run 1
% rackup      
>> Thin web server (v1.3.1 codename Triple Espresso)
>> Maximum connections set to 1024
>> Listening on 0.0.0.0:9292, CTRL+C to stop

ポート9292でThin Webサーバが立ち上がりました。

Browserでhttp://localhost:9292 にアクセスしてみます。

NoMethodError: undefined method `call' for 1:Fixnum

callメソッドがないと言われました。では、Fixnum#callを定義してみます。

# config.ru
class Fixnum
  def call
  end
end

run 1

今度はどうでしょう。

ArgumentError: wrong number of arguments (1 for 0)
        config.ru:3:in `call'

引数がないと言われました。引数を付けてみます。

# config.ru
class Fixnum
  def call(arg)
  end
end

run 1

どうでしょう。

Rack::Lint::LintError: Status must be >=100 seen as integer

Statusは100以上の数でなければならないとのRack::Lint::LintErrorが吐かれました。ではcallメソッドが200を返すようにしてみます。

# config.ru
class Fixnum
  def call(arg)
    200
  end
end

run 1

どうでしょう。

Rack::Lint::LintError: headers object should respond to #each, but doesn't (got NilClass as headers)

headersオブジェクトはNilClassだから#eachできないと言われました。では第2返り値として#eachできるオブジェクト[1]を渡してみます。

# config.ru
class Fixnum
  def call(arg)
    return 200, [1]
  end
end

run 1

どうでしょう。

Rack::Lint::LintError: header key must be a string, was Fixnum

今度はヘッダーキーが文字列じゃないと言われました。これで#eachできるオブジェクトがHashとわかりました。キーが文字列のHashオブジェクトを返してみます。

# config.ru
class Fixnum
  def call(arg)
    return 200, {'one' => 1}
  end
end

run 1

どうでしょう。

Rack::Lint::LintError: a header value must be a String, but the value of 'one' is a Fixnum

今度は値も文字列じゃないとダメだと言われたので、これに対応してみます。

# config.ru
class Fixnum
  def call(arg)
    return 200, {'one' => '1'}
  end
end

run 1

どうでしょう。

Rack::Lint::LintError: No Content-Type header found

Content-Typeヘッダーがないと言われました。用意します。

# config.ru
class Fixnum
  def call(arg)
    return 200, {'one' => '1', 'Content-Type' => 'text/plain'}
  end
end

run 1

どうでしょう。

!! Unexpected error while processing request: Response body must respond to each
127.0.0.1 - - [02/Aug/2012 20:38:50] "GET / HTTP/1.1" 200 - 0.0010

GET / HTTP/1.1” 200“が返ってきました。しかし、レスポンスボディがeachできないと言っています。

それでは第3返り値として、eachできるbodyを返すようにしてみます。

# config.ru
class Fixnum
  def call(arg)
    return 200, {'one' => '1', 'Content-Type' => 'text/plain'}, "Welcome to ONE".chars
  end
end

run 1

どうでしょう。 draque1

Browserにレスポンスが返ってきました。

以上のことをまとめます。

  1. rackupコマンドはWebサーバを起動する。
  2. その際、config.ruという設定ファイル(Rubyスクリプト)を読み込む。
  3. config.ruでは、Webアプリのインスタンスをrunメソッドに渡す。
  4. Webアプリのインスタンスは、1引数のcallメソッドを持っている必要がある。
  5. callメソッドは、3つの返り値、すなわち(1)100以上の数字からなるステータスコード、(2)少なくとも”Content-Type”をキーに、文字列を値に持つハッシュによるヘッダー、および(3)eachできるボディを返す。

Rack Webフレームワーク

さて、Webアプリが1では発展性が無さそうです。もう少しマシなWebアプリを考えます。

callメソッドを持っているオブジェクトと言えば、真っ先に思い浮かぶのはProcオブジェクトです。次に、思い浮かぶのはMethodオブジェクトです。ここでは後者を使ってみます。draqueメソッドを定義し、これをMethodオブジェクト化してrunに渡します。

# config.ru
def draque(arg)
  return 200, {'one' => '1', 'Content-Type' => 'text/plain'}, "Welcome to the World of Draque!!".chars
end

run method(:draque)

rackupしてBrowserでアクセスします。

draque2

いいようです。

さて次に、draqueに渡される引数について見てみます。まずはpします。

# config.ru
def draque(arg)
  p arg
  return 200, {'one' => '1', 'Content-Type' => 'text/plain'}, "Welcome to the World of Draque!!".chars
end

run method(:draque)

コンソールに次のような出力が得られました。

% rackup      
>> Thin web server (v1.3.1 codename Triple Espresso)
>> Maximum connections set to 1024
>> Listening on 0.0.0.0:9292, CTRL+C to stop
{"SERVER_SOFTWARE"=>"thin 1.3.1 codename Triple Espresso", "SERVER_NAME"=>"localhost", "rack.input"=>#<Rack::Lint::InputWrapper:0x00000100a156c0 @input=#<StringIO:0x000001009dab38>>, "rack.version"=>[1, 0], "rack.errors"=>#<Rack::Lint::ErrorWrapper:0x00000100a15648 @error=#<IO:<STDERR>>>, "rack.multithread"=>false, "rack.multiprocess"=>false, "rack.run_once"=>false, "REQUEST_METHOD"=>"GET", "REQUEST_PATH"=>"/", "PATH_INFO"=>"/", "REQUEST_URI"=>"/", "HTTP_VERSION"=>"HTTP/1.1", "HTTP_HOST"=>"localhost:9292", "HTTP_CONNECTION"=>"keep-alive", "HTTP_CACHE_CONTROL"=>"max-age=0", "HTTP_USER_AGENT"=>"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.3 (KHTML, like Gecko) Chrome/22.0.1221.0 Safari/537.3", "HTTP_ACCEPT"=>"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "HTTP_ACCEPT_ENCODING"=>"gzip,deflate,sdch", "HTTP_ACCEPT_LANGUAGE"=>"ja,en-US;q=0.8,en;q=0.6", "HTTP_ACCEPT_CHARSET"=>"UTF-8,*;q=0.5", "HTTP_COOKIE"=>"_gauges_unique_year=1; _gauges_unique=1; _blog_app_session=BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJTVjMmY2ZDU1ODBiNTUxMDY5NGY3ZDkxNzQ3ZmRkOWVkBjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMTQvY1IyYUpOaFBYUXpFYTNXOEU5SHlpYnVEU0ZEaDRxTmUwTzVINThmbmc9BjsARg%3D%3D--f9184f85f7974529836b4bce7c23a5f7132bf8df", "GATEWAY_INTERFACE"=>"CGI/1.2", "SERVER_PORT"=>"9292", "QUERY_STRING"=>"", "SERVER_PROTOCOL"=>"HTTP/1.1", "rack.url_scheme"=>"http", "SCRIPT_NAME"=>"", "REMOTE_ADDR"=>"127.0.0.1", "async.callback"=>#<Method: Thin::Connection#post_process>, "async.close"=>#<EventMachine::DefaultDeferrable:0x00000100a06c38>}127.0.0.1 - - [02/Aug/2012 21:39:21] "GET / HTTP/1.1" 200 - 0.0023

クライアントの環境情報がWebサーバからハッシュで渡されていました。これらの情報があれば、クライアントの環境に応じたレスポンスが構築できそうです。まずは、これらを一覧表示するレスポンスを書いてみます。Content-Typeもtext/htmlに変更します。

# config.ru
def draque(env)
  return 200, {'one' => '1', 'Content-Type' => 'text/html'}, body(env)
end

def body(env)
  ["<h1>Welcome to the World of Draque!!</h1>"] +
  env.map { |k,v| "<p>%s => %s</p>" % [k, v] }
end

run method(:draque)

どうでしょうか。

draque3

タイトルとともにクライアントの環境情報がレンダリングされました。

次に環境変数におけるパス情報を使って、パスに応じたレスポンスを返すようにしてみます。

case式でパスに応じてレスポンスを切り替えるようにします。

# config.ru
def draque(env)
  path = env['PATH_INFO']
  case path
  when '/draque' then [ 200, headers, draque_body ]
  when '/'       then [ 200, headers, top_body(env) ]
  else [ 404, headers, not_found ]
  end
end

def headers
  {'Content-Type' => 'text/html'}
end

def top_body(env)
  ["<h1>Welcome to the World of Draque!!</h1>"] +
  env.map { |k,v| "<p>%s => %s</p>" % [k, v] }
end

def draque_body
  ["<img src='http://www.dqx.jp/storage/img/top/main_visual.png'>"]
end

def not_found
  ["<img src='https://a248.e.akamai.net/assets.github.com/images/modules/404/parallax_octocat.png?1329921026'>", "<img src='https://a248.e.akamai.net/assets.github.com/images/modules/404/parallax_errortext.png?1329921026'>"]
end

run method(:draque)

/draqueにアクセスします。

draque4

次に、用意されていない/rubyにアクセスします。

draque5

うまくいっています。怒られるでしょうか。

さて、ここまで来たら、ルーティングはsinatra風に書きたいです。getメソッドを定義して、パスに応じたレスポンスを登録できるようにします。

# config.ru
@routes = { get:{} }

def draque(env)
  path = env['PATH_INFO']
  if res = @routes[:get][path]
    res.call(env)
  else
    [ 404, headers, not_found ]
  end
end

def get(path, &blk)
  @routes[:get][path] = blk
end

get '/draque' do
  [ 200, headers, draque_body ]
end

get '/' do |env|
  [ 200, headers, top_body(env) ]
end

def headers
  {'Content-Type' => 'text/html'}
end

def top_body(env)
  ["<h1>Welcome to the World of Draque!!</h1>"] +
  env.map { |k,v| "<p>%s => %s</p>" % [k, v] }
end

def draque_body
  ["<img src='http://www.dqx.jp/storage/img/top/main_visual.png'>"]
end

def not_found
  ["<img src='https://a248.e.akamai.net/assets.github.com/images/modules/404/parallax_octocat.png?1329921026'>", "<img src='https://a248.e.akamai.net/assets.github.com/images/modules/404/parallax_errortext.png?1329921026'>"]
end

run method(:draque)

わかりづらくなってきたので、フレームワークの部分をモジュール化します。

# config.ru
module Draque
  @@routes = { get:{} }

  def draque(env)
    path = env['PATH_INFO']
    if res = @@routes[:get][path]
      res.call(env)
    else
      [ 404, headers, not_found ]
    end
  end

  def get(path, &blk)
    @@routes[:get][path] = blk
  end

  def headers
    {'Content-Type' => 'text/html'}
  end
end

Object.send(:include, Draque)

get '/draque' do
  [ 200, headers, draque_body ]
end

get '/' do |env|
  [ 200, headers, top_body(env) ]
end

def top_body(env)
  ["<h1>Welcome to the World of Draque!!</h1>"] +
  env.map { |k,v| "<p>%s => %s</p>" % [k, v] }
end

def draque_body
  ["<img src='http://www.dqx.jp/storage/img/top/main_visual.png'>"]
end

def not_found
  ["<img src='https://a248.e.akamai.net/assets.github.com/images/modules/404/parallax_octocat.png?1329921026'>", "<img src='https://a248.e.akamai.net/assets.github.com/images/modules/404/parallax_errortext.png?1329921026'>"]
end

run method(:draque)

なんちゃってWebフレームワークdraqueの完成です^ ^;

Rack、最初の一歩は踏み出せたでしょうか。

Joke Rack Web framework Draque — Gist


(追記:2012-08-06) 続きを書きました。

エラーメッセージから学ぶRack - Middlewareの魔法


Rack [DVD] [Import]



blog comments powered by Disqus
ruby_pack8

100円〜で好評発売中!
M'ELBORNE BOOKS