Sinatra学習の基礎としてRackを学ぶ

概要(歴史的な経緯など)

この3回のコードをまとめてくださってる(+最近のRackでも動くように変更してくださってる)のが

ちなみに元のコードで何がまずいかというと、一つは require のファイル指定。Ruby 1.9以降はパスで指定しないとNGになったから。もう一つはミドルウェアの neco_filter.rb で Content-Length ヘッダが書き替えられていないためにエラーが出る。

Unexpected error while processing request: Content-Length header was 97, but should be 103

Content-Length ヘッダは Rack::Lint で確認しています。

    def verify_content_length(bytes)
      if @head_request
        assert("Response body was given for HEAD request, but should be empty") {
          bytes == 0
        }
      elsif @content_length
        assert("Content-Length header was #{@content_length}, but should be #{bytes}") {
          @content_length == bytes.to_s
        }
      end
    end

簡潔なリファレンス(2011/5時点

簡潔な概要(2014/4の状況

エンドポイント、ミドルウエア、 Rack ハンドラという概念が Rack では大事だと分かります。

かなり初期バージョン(0.2か0.3)で少し詳しめの解説(2008/2時点

残念ながら古いので今のソースと合わない。
evalで評価してると書いてあるが、さすがに何年もそのまま放置しないよね。

config.ru の内容は Rack::Builder オブジェクトの中で評価される (instance_eval)。
「設定」を設計するための資料 - Joy Luck Crab

ruckupを使わずrubyスクリプトとして起動

もちろん可能なのだが、環境依存してしまうのでオススメしない。理由は以下(長い引用)。

単にRackアプリケーションを起動するためだけであれば,config.ru(rackupファイルと呼びます。拡張子のruはおそらくrackupの略でしょう)は必要ありません。…以下のようなコードをファイルの末尾に書き加えるとsimple_app.rb単体でアプリケーションを起動することができます。

if __FILE__ == $0
  require 'rack/handler/webrick'
  Rack::Handler::WEBrick.run SimpleApp.new, :Port => 9292
end

rackupから起動した場合と特にかわらない結果になったと思います。前回の構成でrackupコマンドを実行した場合,コマンドの内部でこのような処理をしているからです。このほうが「サーバを起動してアプリケーションを実行している」のがよくわかりますね。では,なぜ最初からこの形ではなく,rackupを使うやり方を紹介したのでしょうか。
一つは,この形式ではサーバ依存のコードが残ってしまっているからです。…残念なことに上記のコードでは「どのサーバによってどのように実行されるか」を明示的に記述してしまっているため,(些細なことではありますが)サーバを変更する度にアプリケーションのコードを修正する必要があります。これではRackのメリットが活かし切れていません。
MongrelWEBrickであればrackupコマンドが,ThinやPassengerであればサーバ側で,それぞれ「rackupファイルを読んでアプリケーションを初期化して,適切なハンドラで起動する」ところまではやってくれます。であれば,そちらに任せてしまえばそれぞれの差異を考慮する必要がないので,そうするべきでしょう。
もう一つは,rackupファイルでは,Rack::Builderを通してDSLでRackアプリケーションの構成を記述する仕組みが使えるということです。上記のコードのアプリケーションを起動する部分をRack::Builderを使うように書き換えると,以下のようになります。

if __FILE__ == $0
  require 'rack/builder'
  require 'rack/handler/webrick'
  app = Rack::Builder.new {
      run SimpleApp.new
  }
  Rack::Handler::WEBrick.run app, :Port => 9292
end

Rack::Builderのブロックの中身がrackupファイルと同じになっているのにお気付きでしょうか。rackupコマンドは,rackupファイルの中身をRack::Builderに渡しているわけです。この例ではあまり有難味はありませんが,後述するRack::URLMapやミドルウェアを使うようになると,Rack::Builderを使ってDSLで構成を記述する方がスマートです。
つまり,「基本的にはconfig.ruに色々書いて,アプリケーション側にはサーバ依存のコードは書かない」ものだと覚えてください。
第24回 Rackとは何か(2)Rackの使い方:Ruby Freaks Lounge|gihyo.jp … 技術評論社

ミドルウェアの作り方

何もしないミドルウェア
class Through
  def initialize(app)
    @app = app
  end
  
  def call(env)
    @app.call(env)
  end
end

これを use Through とすれば OK。

ミドルウェアの例(語尾がクマ語になるrack middlewareなど)
class KumaResponseFilter
  def initialize(app)
    @app = app
  end

  def call(env)
    res = @app.call env # sinatraに処理させる
    if res[1]["Content-Type"] =~ /^text\/.+/
      res[2] = res[2].map{|body_part|
        body_part.gsub(/([。!?])/){|s| "クマ#{s}"}
      }
      res[1]["Content-Length"] = res[2].map{|body_part| body_part.bytesize }.inject{|a,b| a+b }.to_s
    end
    return res
  end
end

rack middlewareを作ると、WebサーバーがWebアプリケーションフレームワーク(WAF)とやりとりするデータを書き換えたりできる。
rack::auth系の認証プラグインとか、rack::cache系のキャッシュ系が有名。
どちらもリクエストとレスポンスの間をいじる処理で、プラグインsinatrarailsで実装するよりもrackでやったほうが楽だと思った。

Rack::URLMapの上手い使い方

まず

Rack::URLMap

Rack::URLMapはRackの「機能」ではなくRack標準添付の「アプリケーション」…
 :
このアプリケーションはパスとアプリケーションのマッピングを保持しておき,パスに応じてリクエストを登録してあるアプリケーションに振り分けてくれます。また,Rack::Builderではこのアプリケーションへのショートカットになっているmapという記法が用意されています。
 :
Rack::URLMapでは複雑なURLの解決はできませんので,通常はアプリケーションやフレームワークの側で実装することになりますが,単機能の簡単なアプリケーションを複数立ち上げたいというような場合には手軽に設定できて便利です。
第24回 Rackとは何か(2)Rackの使い方:Ruby Freaks Lounge|gihyo.jp … 技術評論社

ということで普段は Rack アプリケーションで

map '/simple' { run SimpleApp.new }

てな感じで、指定された URL で使う app を振り分けます(Sinatra の get/put の類似)。
で。

あるSinatraアプリケーションが http://localhost:9292/ で動いている。config.ruは以下のような設定。

config.ru
require './app'
run Sinatra::Application

これを http://localhost.:9292/sub で動かしたい、そんな時は Rack::URLMap を使う。

config.ru
require './app'
run Rack::URLMap.new("/sub" => Sinatra::Application)

SinatraアプリケーションをサブURLで動かすためのRack::URLMap - Qiita

GitHubのRackのWiki

この中の
(tutorial) rackup howto · rack/rack Wiki
で興味ある内容を引用しておきます。

  • .ru:

The config file is treated as if it is the body of

app = Rack::Builder.new { ... config ... }.to_app

これは上記第24回 Rackとは何か(2)Rackの使い方:Ruby Freaks Loungeの「rackupとRack::Builder」にも載っていました。

Also, the first line starting with #\ is treated as if it was options, allowing rackup arguments to be specified in the config file. For example:

#\ -w -p 8765
 :

Would run with Ruby warnings enabled, and request port 8765 (which will be ignored unless the server supports the :Port option).

#\ の後に、 rubyコマンドラインオプションが指定できるようです。

Automatic Middleware

rackup will automatically use some middleware, depending on the environment you select, the -E switch, with development being the default:

  • development: CommonLogger, ShowExceptions, Lint
  • deployment: CommonLogger
  • none: none

通常は development モードでの起動。その際にはミドルウェアとして Rack::CommonLogger, Rack::ShowExceptions, Rack::Lint が自動的に使用されるようになっています。
ソースでは以下のようになっています。

module Rack
  class Server
    class << self
      def default_middleware_by_environment
        m = Hash.new {|h,k| h[k] = []}
        m["deployment"] = [
          [Rack::ContentLength],
          [Rack::Chunked],
          logging_middleware,
          [Rack::TempfileReaper]
        ]
        m["development"] = [
          [Rack::ContentLength],
          [Rack::Chunked],
          logging_middleware,
          [Rack::ShowExceptions],
          [Rack::Lint],
          [Rack::TempfileReaper]
        ]

        m
      end

      def middleware
        default_middleware_by_environment
      end

    end

    def middleware
      self.class.middleware
    end

    private
      def build_app(app)
        middleware[options[:environment]].reverse_each do |middleware|
          middleware = middleware.call(self) if middleware.respond_to?(:call)
          next unless middleware
          klass, *args = middleware
          app = klass.new(app, *args)
        end
        app
      end
  end
end

RackでWeb Application Frameworkを作った例

橋本商会 » SinatraっぽいWAFを作る、46行で

Rackの生ログをMongoDBに送る(2015/12/24追記)

ramolog gem という名前で公開されている。