Ruby Freaks Lounge

第41回Sinatra 1.0の世界にようこそ

はじめに

SinatraはRubyで記述されたWebアプリケーションを素早く、簡単につくるためのDSL(ドメイン固有言語)です。

すでに第7回で、原悠さんにより小規模Webアプリのためのフレームワーク、Sinatraというかたちで紹介されています。

簡潔な文法で、高い表現力を持つSinatraは、アメリカのRubyコミュニティや企業を中心に、瞬く間にユーザを増やしました。また、Rubyという一言語に留まらず[1]⁠、Sinatraを模したフレームワークが多く作られている現状からも、SinatraはWebアプリケーション開発そのものにも大きな影響を与えたと言えます。

最近ではここ日本でも、個人利用を中心に、そこかしこでSinatraを使って開発をしている、という話を聞くようになりました。筆者自身、1ユーザとして現在業務で利用しており、1年前は知る人ぞ知る存在だったSinatraは、実際に商用でも使われるという段階にきています。こうして気運が高まる中、2010年3月23日、ついにSinatra初のメジャーバージョンである1.0がリリースされました。

今回は、この1.0リリースを踏まえ、公式ドキュメントをもとに、改めてSinatraの世界を概観していきます。また、本連載ではSinatraの基本的な使い方に関しては触れません。有志による、公式のREADMEの素晴らしい翻訳があります。そちらを一読することをおすすめし、それに代えさせていただきます。

Sinatra1.0

それでは、公式のFAQをもとにSinatra1.0のツアーをしていきましょう。

方向性

初のメジャーバージョンとなると、Railsのように目玉機能を期待してしまいますが、Sinatra1.0においては、見かけ上そこまでの大幅な変更はありません。新機能の追加というよりは、0.9xまでの大半のコードベースの動作を保証しつつ、これまで右往左往していた内部構造をきっちりと固める、という方向性のようです。

旧バージョンとの互換性

Sinatra1.0において、最も大きな変更は、0.3以降の互換性を維持するためにサポートされていた、非推奨機能の後方互換性を捨てるということです。これは先に述べた、内部構造を固めるという方針の反映でもあるでしょう。

それまでの安定バージョンである0.9.6までの0.9x系には、互換性保証のためのcompat.rbというライブラリが同梱され、非推奨機能の使用時に、"deprecated"(廃止予定)であるという旨のワーニングメッセージが通知されていました。1.0においては、ついにこれらの機能のサポートが明確に打ち切られるということになります。

ほとんどの機能には対応機能が存在しますが、ないものもありますので、移行の際には注意が必要です。

表1 Sinatra1.0で廃止となる機能と、推奨される方法・機能の対応表
廃止となる非推奨機能今後推奨される対応方法・機能
sinatra/testライブラリと、それに関連するモジュール、クラス、APIRack::Testの利用
sinatra/test/spec,
sinatra/test/bacon,
sinatra/test/rspec等のライブラリ
使用しない
Sinatra::DefaultクラスSinatra::Baseクラス(統合された)
:views_directoryオプション:viewsオプション
erb、buider、haml、sass等のテンプレート表示用ライブラリの自動ロード明示的なライブラリのロード
media_typeメソッドmime_typeメソッド
mimeメソッドmime_typeメソッド
send_dataメソッド使用しない
Sinatra::Event、Sinatra::EventContextクラス現状のextensionsAPIの利用
set_option、set_optionsメソッドsetメソッド
:envオプション:environmentオプション
stopメソッドhaltメソッド
entity_tagメソッドetagメソッド
headersメソッドresponse['Header-Name']への代入
Sinatra.applicationSinatra::Application
Sinatra.application=nilによるアプリケーションのリセット明示的なリセットは必要なし
Sinatra.defautl_optionsによるデフォルト設定Sinatra::Base.set(:key:value)での設定
Sinatra::ServerErrorすべて内部エラーとして扱われ、HTTPステータスコード500のエラーとして処理

中でも、移行時に直ぐに影響が出そうなのは、テンプレート表示用ライブラリを明示的にrequireする必要がある、という点でしょうか。

また、Sinatra::Defaultクラスが無くなり、Sinatra::Baseクラスと統合するのも、拡張モジュールの作者にとっては影響がある可能性があります。これについては「拡張モジュールの作成」で後述します。

新しい機能

追加機能に関しては、CHANGESドキュメントを参照すると概観できます。以下、順に見ていきましょう。

  • afterメソッド。beforeメソッドに対して、共通の後処理も可能に。
  • Tiltの採用。Tiltはテンプレートエンジンのインタフェースを統合するための抽象レイヤで、キャッシュ等の機能も提供される。また、それに伴いmustacheliquidほか、新たなテンプレートエンジンもサポートに。
  • ERB、Erubis、Hamlなどのコンパイルを初回起動時に行うことによる、パフォーマンス最適化。
  • settingsメソッド。setで設定した値を、classとrequest両方のスコープから参照できる。optionsのより良い代替手段となる。
  • オプション:reload_templates。trueにした場合、コンパイル済みのテンプレートのキャッシュが行われない。
  • erubisヘルパメソッド。Erubisをテンプレートエンジンとして選択可能になる。
  • cache_control、expiresメソッド。Cache-ControlヘッダによるHTTPキャッシュの制御が可能になった。
  • 起動時の-oオプション。アドレスを指定して起動出来るようになった。
  • Rack::Session::Cookieクラス。テスト実行環境において利用できる。
  • 拡張モジュールによって基底クラスに変更が加えられた場合も、Sinatra::Baseのサブクラス生成時に、動的にルーティングやbeforeフィルタ、テンプレート、エラー処理が解決されるようになった。
  • オプション:raise_errorsの動作の変更。trueの場合、以前までエラーはアプリケーションの外側でraiseされていたが、基底クラス内でraiseされるようになった。falseの場合は、errorブロックがあれば処理され、ステータスコード500のエラーとして処理される。
  • passメソッドを使用して、かつルーティングにマッチしない場合、または全てのルーティングがpassされた場合、X-Cascadeヘッダが付加されるようになった。
  • 静的ファイルを返す場合はフィルタ処理が機能しないようになった。
  • passメソッドの引数にブロックが渡せるようになった。passした後、その次のルーティングがマッチしない場合に実行される。

afterメソッドは初期のバーションから永らく姿を消しており、筆者の個人的には待望していたメソッドですが、やっと使えるようになりました。

目立たない変更ですが、起動時の-oオプションの追加により複数のSinatraアプリケーションが80番ポートで起動できるようになったことも、大変便利でしょう。

テンプレートに関する改善点も多いです。起動時にコンパイルされ、キャッシュもされることで、大幅にパフォーマンスが上がりました。しかし、これがデフォルトの動作になるため、キャッシュされたくないビューに関しては追加されたオプション:reload_templatesをtrueにする必要があり、注意する必要があります。

総評すると、派手さはありませんが、細かいところまで配慮が行き届き、商用利用をみこした改善が重ねられた、と言えるのではないでしょうか。

2つのSinatra

Sinatraに関してよく話題にされる一つに、⁠小規模な開発では使いやすそうだけど、中規模以上になると管理できなくなるのではないか」という問題があります。サンプルコードで見られるトップレベルの記述方法の印象が強いせいか、こうした感想が持たれやすいようです。

この問いに答えるため、実はSinatra0.9x系から、もう一つの記述方法が用意されています。それまでの方法"Classic"スタイルに対し、この新しい書き方は"Modular"スタイルと呼ばれています。

それでは、この2つのHelloWorldサンプルコードを見てみましょう。

リスト1 "Classic"スタイル
require 'rubygems'
require 'sinatra'
get '/' do
  'Hello world!'
end

これぞまさしくSinatra、"Classic"スタイルの呼び名にふさわしいコードです。個人的にSinatraの魅力の本質は、このコードに凝縮されていると思っています。

リスト2 "Modular"スタイル
require 'rubygems'
require 'sinatra/base'
class MyApp < Sinatra::Base
  get '/' do
    'Hello world!'
  end
end
MyApp.run! :host => 'localhost', :port => 9090

これが"Modular"スタイルのSinatraコードです。一見して、クラスベースのコードになっていることが分かります。

RubyKaigi2009におけるAaron Quintさんの講演の中で、繰り返し"Classy"(イケてる)なSinatraとして紹介されていたので、記憶されている方もいるでしょう。

この"Modular"スタイルのSinatraでもっとも注目すべき点は、

require 'sinatra/base'

と、Sinatra::Baseクラスのみがロードされ、継承されている点です。

Sinatra::Baseクラスはその名の通りSinatraのコアとなる基底クラスです。それ単体での利用は推奨されていません。

require 'sinatra'

とした"Classic"スタイルの場合、Sinatra::BaseクラスはSinatra::Applicationクラスに継承されます。

そして、Sinatra::Applicationクラス内で、Sinatra::Baseクラス内のいくつかの--Sinatraの代表的なAPI[2]がトップレベルに押し上げられます。

トップレベル側から見れば、Sinatra::Baseクラスに処理を委譲するかたちになります。これにより、不必要なAPIへのアクセスがトップレベルにおいてできなくなり、各メソッド間でのインスタンス変数などの共有も行える、おなじみのSinatraの使用感が実現されます。

しかし、トップレベルのみでは、中規模以上の開発ではやはりグローバル領域の汚染が問題となります。"Modular"スタイルではシンプルに、Sinatra::Baseクラスの直接継承によって名前空間を分け、これを解決しています。"Modular"スタイルのSinatraアプリケーションはRackのミドルウェアとなるため、config.ruファイル内でuseキーワードで複数を指定することもできます。

こうして、"Modular"スタイルの開発手法を用いる事で、アプリケーション単位で開発スコープの分割が可能になり、ある程度の開発規模でも、安心してSinataで開発することが可能になります。

拡張モジュールの作成

もう一つ、Sinatraを使う上で「Railsのようなプラグインの機能はないのか」ということがよく話題にのぼります。

Sinatraはとてもシンプルがゆえに、実務においては不足する機能が多く、意識せずに開発を進めると、思わぬ車輪の再発明をしがちです。

もちろん、シンプルなコアに必要な機能だけを足していく、Sinatraならではの開発スタイルは魅力的です。しかし、よくある認証系やヘルパーを、開発者がそれぞれの方法で持っているという状況は、あまり良いことではないと言えるでしょう。

Rackのミドルウェア開発というのは、この問いの答えの一つです。しかし、リクエストとレスポンスの言わばフィルタ的処理に限定されてしまうため、アプリケーションの動作に密に影響を与えるには、力不足の感は否めません。

そこで、Sinatraでは0.9xを通じて、開発者間のモジュールの共有化を推進するための方法が模索されてきました。現在はルールが定められるとともに、拡張用APIが提供されています。詳しくは公式ドキュメントの"Writing Extensions"に記載されていますので、その引用をメインに解説してみましょう。

拡張モジュール作成のルール

拡張モジュール作成には、以下のルールが定められています。ルールと言っても、"should"と表現されていることから、作成上のガイドラインと捉えると良いでしょう。

  1. Sinatra::Baseクラスをあらゆる手段(オープンクラス、include、extendなど)において、直接的変更をしないこと。
  2. 「require 'sinatra'」ではなく、⁠require 'sinatra/base'⁠⁠ を行うこと。
  3. 用意されたAPIをできるだけ用いること。つまりSinatra.registerSinatra.helpersをつかうこと。
  4. Sinatra::モジュール名の階層で命名すること。

ルール1のSinatra::Baseクラスへの直接的変更を避ける、という点はSinatraのコアの動作に関わる部分ですので、最も重要です。

次に、ルール3で登場した、"Sinatra.register"と"Sinatra.helpers"という2つのAPIを見ていきましょう。

Sinatra.helpers

helpersメソッドと言えば、ブロックを渡し、メソッドを定義する用途のほうがよく知られているかもしれません。そのときの仕組みはずばり、Module#class_evalで、Sinatra::Baseクラスのコンテキストでブロックは評価されます。しかし、拡張モジュールを作成する場合は、主にモジュールの指定に利用します。その場合は、Sinatra::BaseクラスにModule#includeされ、メソッドが追加されます。

このAPIを利用する局面は、Sinatraにグローバルなメソッドを追加するときです。

リスト3 helpersメソッドを用いた拡張モジュールの例: HTMLEscapeHelper
require 'sinatra/base'

module Sinatra
  module HTMLEscapeHelper
    def h(text)
      Rack::Utils.escape_html(text)
    end
  end

  helpers HTMLEscapeHelper
end
リスト4 上記拡張モジュールの利用例
require 'sinatra'
require 'sinatra/htmlescape'

get "/hello" do
  h "1 < 2"     # => "1 < 2"
end

Sinatra.register

registerメソッドもモジュールを指定しますが、helpersメソッドと違うのはobject#extendにより、動作するアプリケーションのインスタンスに特異メソッドとして影響を与えるという点です。つまり、個々のアプリケーションの動作に影響を与える場合に利用します。

具体的には、認証用のアクションを追加する、特定のフィルタ処理を加える等が、利用シーンになるでしょう。また、registeredというメソッドをモジュールに定義した場合、アプリケーションのインスタンスを引数に取ることができますので、アプリケーション動作時の状態や、設定などに変更を加えることも可能です。

リスト5 registerメソッドを用いた拡張モジュールの例: SessionAuth
require 'sinatra/base'

module Sinatra
  module SessionAuth

    module Helpers
      def authorized?
        session[:authorized]
      end

      def authorize!
        redirect '/login' unless authorized?
      end

      def logout!
        session[:authorized] = false
      end
    end

    def self.registered(app)
      app.helpers SessionAuth::Helpers

      app.set :username, 'frank'
      app.set :password, 'changeme'

      app.get '/login' do
        "<form method='POST' action='/login'>" +
        "<input type='text' name='user'>" +
        "<input type='text' name='pass'>" +
        "</form>"
      end

      app.post '/login' do
        if params[:user] == options.username && params[:pass] == options.password
          session[:authorized] = true
          redirect '/'
        else
          session[:authorized] = false
          redirect '/login'
        end
      end
    end
  end

  register SessionAuth
end
リスト6 上記拡張モジュールの利用例
SessionAuth
require 'sinatra'
require 'sinatra/sessionauth'

set :password, 'hoboken'

get '/public' do
  if authorized?
    "Hi. I know you."
  else
    "Hi. We haven't met. <a href='/login'>Login, please.</a>"
  end
end

get '/private' do
  authorize!
  'Thanks for logging in.'
end

参照ドキュメント

公式ドキュメントは英語ですが、上記で引用したサンプルコードに加え、先述した"Modular"スタイルでの拡張モジュールの呼び出し方や、ディレクトリ構成等が解説されています。実際に拡張モジュールを作成する上では、最も重要なドキュメントですので、是非ご一読ください。

現状、日本では拡張モジュール開発の例は少ないようですが、海外では既に開発されている例も少なくありません。その一部はこちらの"Libraries and extensions"に紹介されています。海外でも粒度は様々ですので、日本国内でも今後どんどん、気軽に拡張モジュールが開発されるような状況になれば良いのにな、と個人的には思っています。

まとめ

Sinatraの最初のコミットを今見てみると、トップレベルに、かのシンプルなget、post、put、deleteといったメソッドのみが定義されています。ファイルは現在よりむしろ多く、構造は複雑で、マイクロフレームワークとしてのアイデンティティはそれほど感じられません。そして何より、コミットログに一言残された「this is it」という言葉が印象的です。

こうした発祥から、Sinatraとはつまるところ、⁠極限までにシンプルに、Webアプリケーションは記述できる」という発見だった、と言えるのではないでしょうか。

そして誕生から1.0への流れは、そのシンプルなアイディアを、核はそのままに、現実世界の中でゆるやかに発展させていくものです。

Sinatra::Baseクラスによる"Modular"スタイルのSinatraや、拡張用APIなどの仕組みは、今後Sinatraの可能性を大きく広げていくことになるはずです。もしかすると、始めのシンプルさが失われていき、替わりに強力な機能を手に入れるかもしれません。あるいは、まだ誰も見たことのない、全く新しい何かになるかもしれません。

Sinatraの先には、まだ地図がないと思います。筆者は、そこに強く興味を惹かれます。そこにはWebアプリケーションを記述する上での、自由が広がっています。

もちろん自由には責任がつきまといますが、そのことも含め、次回は筆者自身の業務経験上から、Sinatraを現実の開発でどのように使うかを紹介したいと思います。

おすすめ記事

記事・ニュース一覧