エラーメッセージから学ぶRack - Middlewareの魔法
前回の記事「エラーメッセージから学ぶRack - 最初の一歩」の続きです。
噂によるとRackにはMiddlewareなる魔法があるそうです。そしてRack古文書にはMiddlewareについて次のようにあります。
use
の呪文を唱えよ。さすれば扉は開かれん
準備
前回作った、なんちゃってWebフレームワーク「draque」を継続して使います。config.ru
が少し長くなったので、Webアプリの本体を別ファイルdraque.rb
に移します。
#draque.rb
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
end
Object.send(:include, Draque)
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
config.ru
は次のようになります。
# config.ru
require "./draque"
run method(:draque)
useの呪文
さて早速、use
の呪文を試してみます。config.ru
にuseを追加します。
# config.ru
require "./draque"
use
run method(:draque)
rackup
します。
% rackup
~/.rbenv/.../rack/builder.rb:77:in `use': wrong number of arguments (0 for 1) (ArgumentError)
from config.ru:4:in `block in <main>'
引数が足りないと言われました。おそらくmiddlewareを渡すものと思われます。しかしmiddlewareが何なのかわからないので、例によってまずは1
を渡してrackupしてみます。
# config.ru
require "./draque"
use 1
run method(:draque)
どうでしょうか。
% rackup
~/.rbenv/.../rack/builder.rb:82:in `block in use': undefined method `new' for 1:Fixnum (NoMethodError)
1にはnewメソッドが無いと言われました。これでクラスが期待されている、つまりmiddlewareはクラスであるということが分かりました1。それでは、差し当たりUpDownというクラスを作って渡してみます。
# config.ru
require "./draque"
class UpDown
end
use UpDown
run method(:draque)
どうでしょうか。
% rackup
~/.rbenv/.../rack/builder.rb:82:in `initialize': wrong number of arguments(1 for 0) (ArgumentError)
今度はinitializeの引数が足りないと言われました。では1引数のinitializeを定義します。
# config.ru
require "./draque"
class UpDown
def initialize(arg)
end
end
use UpDown
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
ポート9292でThin Webサーバが立ち上がりました。
Browserでhttp://localhost:9292 にアクセスしてみます。
>> Listening on 0.0.0.0:9292, CTRL+C to stop
NoMethodError: undefined method `call' for #<UpDown:0x00000101053fc8>
UpDownオブジェクトにcall
メソッドがないと言われました。では、UpDown#callを定義してみます。
# config.ru
require "./draque"
class UpDown
def initialize(arg)
end
def call
end
end
use UpDown
run method(:draque)
今度はどうでしょう。
ArgumentError: wrong number of arguments (1 for 0)
config.ru:10:in `call'
引数がないと言われました。ん?
では引数を付けてみます。
# config.ru
require "./draque"
class UpDown
def initialize(arg)
end
def call(arg)
end
end
use UpDown
run method(:draque)
どうでしょうか。
Rack::Lint::LintError: Status must be >=100 seen as integer
Statusは100以上の数でなければならないとのRack::Lint::LintErrorが吐かれました。これって…
いつかきた道..ですよね?
# config.ru
require "./draque"
class UpDown
def initialize(arg)
end
def call(arg)
200
end
end
use UpDown
run method(:draque)
Rack::Lint::LintError: headers object should respond to #each, but doesn't (got NilClass as headers)
# config.ru
require "./draque"
class UpDown
def initialize(arg)
end
def call(arg)
return 200, {'one' => '1'}
end
end
use UpDown
run method(:draque)
Rack::Lint::LintError: No Content-Type header found
# config.ru
require "./draque"
class UpDown
def initialize(arg)
end
def call(arg)
return 200, {'Content-Type' => 'text/html'}
end
end
use UpDown
run method(:draque)
!! Unexpected error while processing request: Response body must respond to each
127.0.0.1 - - [05/Aug/2012 18:03:10] "GET / HTTP/1.1" 200 - 0.0010
ほら!
第3返り値に#eachできるボディでしたね。
# config.ru
require "./draque"
class UpDown
def initialize(arg)
end
def call(arg)
return 200, {'Content-Type' => 'text/html'}, "Hello, from UpDown".chars
end
end
use UpDown
run method(:draque)
いいですね!
・ ・ ・
って、良くないです。runしたdraque
がレンダリングされないじゃないですか…
draqueを探す
さて、どうしますか。
そう言えばinitializeに渡した引数、あれは何でしょうね。p
してみましょうか。
# config.ru
require "./draque"
class UpDown
def initialize(arg)
p arg
end
def call(arg)
return 200, {'Content-Type' => 'text/html'}, "Hello, from UpDown".chars
end
end
use UpDown
run method(:draque)
どうでしょうか。
% rackup
#<Method: Rack::Builder(Draque)#draque>
>> 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
127.0.0.1 - - [05/Aug/2012 18:14:13] "GET / HTTP/1.1" 200 - 0.0012
127.0.0.1 - - [05/Aug/2012 18:14:14] "GET /favicon.ico HTTP/1.1" 200 - 0.0009
なんとinitializeにはdraque
が渡っていました。驚愕の事実。
そうすると、UpDown#callの中でdraqueをcallすれば、draqueがレンダリングされますか?やってみます。
# config.ru
require "./draque"
class UpDown
def initialize(app)
@app = app
end
def call(env)
@app.call(env)
end
end
use UpDown
run method(:draque)
どうでしょうか。
うまくいきました。
middlewareを書く
現状middleware UpDown
は、何もしないダメウェアですが、callのところでゴニョゴニョすれば何かできると想像できます。やってみます。
# config.ru
require "./draque"
class UpDown
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
[status, headers, body.reverse]
end
end
use UpDown
run method(:draque)
どうでしょうか。
天地反転の呪文が適用されました。
以上のことをまとめます。
- middlewareは#callメソッドを持ったクラスである。
- middleware#initializeにはrunに渡したWebアプリオブジェクトが渡される。
- middlewareの#callでWebアプリの#callを呼んで、ゴニョゴニョする。
従って、リクエストーレスポンスの流れは次のようになります。
- Rack(Webサーバ)はBrowserからリクエストが来ると、useされたmiddleware(UpDwon)の#callを呼ぶ。
- middlewareの#callはWebアプリ(draque)の#callを呼ぶ。
- Webアプリは#call呼び出しに対してリクエストに応じた、[status, headers, body]を返す。
- middlewareは#callでゴニョゴニョして、[status, headers, body]を返す。
- Rack(Webサーバ)はBrowserにHTTPレスポンスを返す。
2つ目のmiddlewareを書く
さて、もう一つmiddlewareを書いてみます。名前をFireとします。
# config.ru
require "./draque"
class UpDown
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
[status, headers, body.reverse]
end
end
class Fire
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
new_body = ["<div style='background-color:red'>"] + body + ["</div>"]
[status, headers, new_body]
end
end
use Fire
use UpDown
run method(:draque)
#callの中でゴニョゴニョしてるの分かりますか?
レスポンスを見てみます。
天地反転の呪文と炎の呪文が適用されました。
さて、ここで気になることが一つあります。Fireに渡されたappはWebアプリなのでしょうか、それとも…。p
で見てみます。
# config.ru
require "./draque"
class UpDown
def initialize(app)
p "#{app} in UpDown"
@app = app
end
def call(env)
status, headers, body = @app.call(env)
[status, headers, body.reverse]
end
end
class Fire
def initialize(app)
p "#{app} in Fire"
@app = app
end
def call(env)
status, headers, body = @app.call(env)
new_body = ["<div style='background-color:red'>"] + body + ["</div>"]
[status, headers, new_body]
end
end
use Fire
use UpDown
run method(:draque)
どうでしょうか。
% rackup
"#<Method: Rack::Builder(Draque)#draque> in UpDown"
"#<UpDown:0x000001008ee738> in Fire"
>> 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
UpDownにdraqueが渡されていることが分かります。そして、果たしてFireにはUpDownが渡されていました。
つまりこういうことです。
- 最初にuseされたmiddleware(Fire)のinitializeには、次にuseされたmiddleware(UpDown)のオブジェクトが渡される。つまりUpDownオブジェクトはFireオブジェクトでラップされる。
- 最後にuseされたmiddleware(UpDown)には、Webアプリオブジェクト(draque)が渡される。つまりdraqueオブジェクトはUpDownオブジェクトでラップされる。
- つまりRackというのは、棚ではなくて、マトリョーシカである。
従って、リクエストーレスポンスの流れは次のようになります。
- Rack(Webサーバ)はBrowserからリクエストが来ると、最初にuseされたFireの#callを呼ぶ。
- Fireの#callは次にuseされたUpDownの#callを呼ぶ。
- UpDownの#callはWebアプリ(draque)の#callを呼ぶ。
- Webアプリは#call呼び出しに対してリクエストに応じた、[status, headers, body]を返す。
- UpDownは#callでゴニョゴニョして、[status, headers, body]を返す。
- Fireは#callでゴニョゴニョして、[status, headers, body]を返す。
- Rack(Webサーバ)はBrowserにHTTPレスポンスを返す。
Fireを改良する
さてFireの呪文は強力過ぎます。これを一部の文字列にだけ適用するよう改良します。
対象文字列を指定する方法が必要になります。ところがFireオブジェクトはRackが生成するので、そのチャンスは一見なさそうです。仕方がないので、試しにuseに渡して、initializeで受けるようにしてみます。p
で出力を見ます。
class Fire
def initialize(app, pattern)
@app = app
p pattern
end
def call(env)
status, headers, body = @app.call(env)
new_body = ["<div style='background-color:red'>"] + body + ["</div>"]
[status, headers, new_body]
end
end
use Fire, "hello"
run method(:draque)
どうでしょうか。
% rackup
"hello"
>> 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
受け渡しができているようです。ついでにブロックもイケるか見てみます。
class Fire
def initialize(app, pattern)
@app = app
p pattern
yield
end
def call(env)
status, headers, body = @app.call(env)
new_body = ["<div style='background-color:red'>"] + body + ["</div>"]
[status, headers, new_body]
end
end
use Fire, "hello" do
p "hello from a block!"
end
use UpDown
run method(:draque)
% rackup
"hello"
"hello from a block!"
>> 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
ブロックも受けてくれるようです。
では、新しいFireを実装します。
# config.ru
require "./draque"
class UpDown
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
[status, headers, body.reverse]
end
end
class Fire
def initialize(app, pattern)
@app = app
@pattern = pattern
end
def call(env)
status, headers, body = @app.call(env)
replace = ->pat{ "<em style='background-color:red'>#{pat}</em>" }
new_body = body.inject([]) { |m, part| m << part.gsub(@pattern) { replace[$&] } }
[status, headers, new_body]
end
end
use Fire, /rack|draque/i
use UpDown
run method(:draque)
出力を見てみます。
いいですね!
Rack、次の一歩は踏み出せたでしょうか。
今回の結論:
Rackは実はマトリョーシカだった!
Joke Rack Web framework Draque
— Gist
(追記:2012-08-08) 続きを書きました。
Rackをminifyした僅か100行のLackで学ぶRackの中身
このリンクはGumroadにおける商品購入リンクになっています。クリックすると、オーバーレイ・ウインドウが立ち上がって、この場でクレジットカード決済による購入が可能です。購入にはクレジット情報およびメールアドレスの入力が必要になります。購入すると、入力したメールアドレスにコンテンツのDLリンクが送られてきます。
詳細は以下を参照して下さい。
購入ご検討のほどよろしくお願いしますm(__)m
いちごマトリョーシカ 5人姉妹 レッド×ピンク 【マトリョーシカ】
- #newが定義されていれば、もしかしたらクラスでなくてもいいのかもしれません ↩
blog comments powered by Disqus