(追記: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%cddraque%rackupconfiguration 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
と書いてもう一度やってみます。
%echorun > 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.rurun1
%rackup>>Thin web server(v1.3.1 codename Triple Espresso)>>Maximum connectionssetto 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.ruclassFixnumdefcallendendrun1
今度はどうでしょう。
ArgumentError: wrong number of arguments(1for0) config.ru:3:in`call'
引数がないと言われました。引数を付けてみます。
# config.ruclassFixnumdefcall(arg)endendrun1
どうでしょう。
Rack::Lint::LintError: Status must be >=100 seen as integer
Statusは100以上の数でなければならないとのRack::Lint::LintErrorが吐かれました。ではcallメソッドが200を返すようにしてみます。
# config.ruclassFixnumdefcall(arg)200endendrun1
どうでしょう。
Rack::Lint::LintError: headers object should respond to#each, but doesn't (got NilClass as headers)
headersオブジェクトはNilClassだから#each
できないと言われました。では第2返り値として#eachできるオブジェクト[1]
を渡してみます。
# config.ruclassFixnumdefcall(arg)return200,[1]endendrun1
どうでしょう。
Rack::Lint::LintError: header key must be a string, was Fixnum
今度はヘッダーキーが文字列じゃないと言われました。これで#eachできるオブジェクトがHashとわかりました。キーが文字列のHashオブジェクトを返してみます。
# config.ruclassFixnumdefcall(arg)return200,{'one'=>1}endendrun1
どうでしょう。
Rack::Lint::LintError: a header value must be a String, but the value of'one' is a Fixnum
今度は値も文字列じゃないとダメだと言われたので、これに対応してみます。
# config.ruclassFixnumdefcall(arg)return200,{'one'=>'1'}endendrun1
どうでしょう。
Rack::Lint::LintError: No Content-Type header found
Content-Type
ヘッダーがないと言われました。用意します。
# config.ruclassFixnumdefcall(arg)return200,{'one'=>'1','Content-Type'=>'text/plain'}endendrun1
どうでしょう。
!! Unexpected errorwhileprocessing request: Response body must respond to each127.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.ruclassFixnumdefcall(arg)return200,{'one'=>'1','Content-Type'=>'text/plain'},"Welcome to ONE".charsendendrun1
どうでしょう。
Browserにレスポンスが返ってきました。
以上のことをまとめます。
- rackupコマンドはWebサーバを起動する。
- その際、config.ruという設定ファイル(Rubyスクリプト)を読み込む。
- config.ruでは、Webアプリのインスタンスをrunメソッドに渡す。
- Webアプリのインスタンスは、1引数のcallメソッドを持っている必要がある。
- callメソッドは、3つの返り値、すなわち(1)100以上の数字からなるステータスコード、(2)少なくとも”Content-Type”をキーに、文字列を値に持つハッシュによるヘッダー、および(3)eachできるボディを返す。
さて、Webアプリが1
では発展性が無さそうです。もう少しマシなWebアプリを考えます。
call
メソッドを持っているオブジェクトと言えば、真っ先に思い浮かぶのはProcオブジェクトです。次に、思い浮かぶのはMethodオブジェクトです。ここでは後者を使ってみます。draqueメソッドを定義し、これをMethodオブジェクト化してrunに渡します。
# config.rudefdraque(arg)return200,{'one'=>'1','Content-Type'=>'text/plain'},"Welcome to the World of Draque!!".charsendrunmethod(:draque)
rackup
してBrowserでアクセスします。
いいようです。
さて次に、draqueに渡される引数について見てみます。まずはp
します。
# config.rudefdraque(arg)pargreturn200,{'one'=>'1','Content-Type'=>'text/plain'},"Welcome to the World of Draque!!".charsendrunmethod(:draque)
コンソールに次のような出力が得られました。
%rackup>>Thin web server(v1.3.1 codename Triple Espresso)>>Maximum connectionssetto 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.rudefdraque(env)return200,{'one'=>'1','Content-Type'=>'text/html'},body(env)enddefbody(env)["<h1>Welcome to the World of Draque!!</h1>"]+env.map{|k,v|"<p>%s => %s</p>"%[k,v]}endrunmethod(:draque)
どうでしょうか。
タイトルとともにクライアントの環境情報がレンダリングされました。
次に環境変数におけるパス情報を使って、パスに応じたレスポンスを返すようにしてみます。
case式でパスに応じてレスポンスを切り替えるようにします。
# config.rudefdraque(env)path=env['PATH_INFO']casepathwhen'/draque'then[200,headers,draque_body]when'/'then[200,headers,top_body(env)]else[404,headers,not_found]endenddefheaders{'Content-Type'=>'text/html'}enddeftop_body(env)["<h1>Welcome to the World of Draque!!</h1>"]+env.map{|k,v|"<p>%s => %s</p>"%[k,v]}enddefdraque_body["<img src='http://www.dqx.jp/storage/img/top/main_visual.png'>"]enddefnot_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'>"]endrunmethod(:draque)
/draque
にアクセスします。
次に、用意されていない/ruby
にアクセスします。
うまくいっています。怒られるでしょうか。
さて、ここまで来たら、ルーティングはsinatra風に書きたいです。getメソッドを定義して、パスに応じたレスポンスを登録できるようにします。
# config.ru@routes={get:{}}defdraque(env)path=env['PATH_INFO']ifres=@routes[:get][path]res.call(env)else[404,headers,not_found]endenddefget(path,&blk)@routes[:get][path]=blkendget'/draque'do[200,headers,draque_body]endget'/'do|env|[200,headers,top_body(env)]enddefheaders{'Content-Type'=>'text/html'}enddeftop_body(env)["<h1>Welcome to the World of Draque!!</h1>"]+env.map{|k,v|"<p>%s => %s</p>"%[k,v]}enddefdraque_body["<img src='http://www.dqx.jp/storage/img/top/main_visual.png'>"]enddefnot_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'>"]endrunmethod(:draque)
わかりづらくなってきたので、フレームワークの部分をモジュール化します。
# config.rumoduleDraque@@routes={get:{}}defdraque(env)path=env['PATH_INFO']ifres=@@routes[:get][path]res.call(env)else[404,headers,not_found]endenddefget(path,&blk)@@routes[:get][path]=blkenddefheaders{'Content-Type'=>'text/html'}endendObject.send(:include,Draque)get'/draque'do[200,headers,draque_body]endget'/'do|env|[200,headers,top_body(env)]enddeftop_body(env)["<h1>Welcome to the World of Draque!!</h1>"]+env.map{|k,v|"<p>%s => %s</p>"%[k,v]}enddefdraque_body["<img src='http://www.dqx.jp/storage/img/top/main_visual.png'>"]enddefnot_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'>"]endrunmethod(:draque)
なんちゃってWebフレームワークdraqueの完成です^ ^;
Rack、最初の一歩は踏み出せたでしょうか。
Joke Rack Web framework Draque — Gist
(追記:2012-08-06) 続きを書きました。
エラーメッセージから学ぶRack - Middlewareの魔法