わかった気になるRack Middleware

使いたい機能のみを有効にできるmiddlewareという仕組みがrackにはある。
たとえば、ロギングはmiddlewareで実装するためアプリケーションと疎結合な状態になる。

rackアプリケーションにmiddlewareを追加する方法は、config.ru などに有効にしたい機能をuseというキーワードで登録にしていく。
ちなみに、config.ruRack::Builderのインスタンスコンテキストで実行する。

# config.ru
use LoggerMiddleware
run lambda { |env| 
  [200, {'Content-Type' => 'text/plain'}, ['OK']] 
}

middlewareはミルフィーユみたいなもので、リクエストを受け取るとレスポンスを生成するために、middlewareを通過して最終的なレスポンスを評価する。
middlewareとして使うクラスにはcallというメソッドがあれば動作し、 middlewareを使った最小限なrackアプリケーションは下記な感じで表現できる。

class Middleware
  def initialize(app)
    @app = app
  end
  def call(env)
    pp 'hello'
    @app.call(env)
  end
end

app = Rack::Builder.app do
  use Middleware
  run lambda { |env| 
    [200, {'Content-Type' => 'text/plain'}, ['OK']] 
  }
end
server = Rack::Server.new(app: app)
server.start
curl 'http://localhost:8080' # => OK

middlewareの動かし方がわかったのでミルフィーユに深入りしてみる。
useキーワードによって登録されたmiddlewareクラスがどんな感じでミルフィーユ化するのかを調べたところ、Procでラップしたオブジェクトを配列に追加して、最後にinjectで数珠繋ぎにしている、ということがわかった。
下記は、ミルフィーユ化処理を簡単に書き直したコードである。

class Middle
  def initialize(app)
    @app = app
  end

  def call(env)
    puts 'hell middleware'
    @app.call(env)
  end
end

class Builder
  def initialize(&block)
    @use = []
    instance_eval(&block) if block
  end

  def use(klass, &block)
    @use << Proc.new { |app| klass.new(app) }
  end

  def run
    app = ->(x){ '200' }
    puts @use.inject(app) { |a, e| e.call(a) }.call('')
  end
end

Builder.new {
  use Middle
  use Middle
}.run
hell middleware
hell middleware
200

以上。