エラーメッセージから学ぶRack - 最初の一歩
(追記:2012-12-25) 本記事およびこれに続くRackの記事(全4本)をまとめて電子書籍化しました。「Gumroad」を通して100円にて販売しています。内容についての追加・変更はありませんが、誤記の修正およびメディア向けの調整を行っています。
このリンクはGumroadにおける商品購入リンクになっています。クリックすると、オーバーレイ・ウインドウが立ち上がって、この場でクレジットカード決済による購入が可能です。購入にはクレジット情報およびメールアドレスの入力が必要になります。購入すると、入力したメールアドレスにコンテンツのDLリンクが送られてきます。
詳細は以下を参照して下さい。
購入ご検討のほどよろしくお願いします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
どうでしょう。
Browserにレスポンスが返ってきました。
以上のことをまとめます。
- rackupコマンドはWebサーバを起動する。
- その際、config.ruという設定ファイル(Rubyスクリプト)を読み込む。
- config.ruでは、Webアプリのインスタンスをrunメソッドに渡す。
- Webアプリのインスタンスは、1引数のcallメソッドを持っている必要がある。
- 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でアクセスします。
いいようです。
さて次に、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)
どうでしょうか。
タイトルとともにクライアントの環境情報がレンダリングされました。
次に環境変数におけるパス情報を使って、パスに応じたレスポンスを返すようにしてみます。
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
にアクセスします。
次に、用意されていない/ruby
にアクセスします。
うまくいっています。怒られるでしょうか。
さて、ここまで来たら、ルーティングは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の魔法
blog comments powered by Disqus