Sinatraメモ

本家

起動(クラシックスタイル)

ruby myapp.rb [-h] [-x] [-e ENVIRONMENT] [-p PORT] [-o HOST] [-s HANDLER]

オプション:
-h # ヘルプ
-p # ポート指定(デフォルトは4567)
-o # ホスト指定(デフォルトは0.0.0.0)
-e # 環境(development/test/production)を指定 (デフォルトはdevelopment)
-s # rackserver/handlerを指定 (デフォルトはthin)
-x # mutex lockを付ける (デフォルトはoff)
Sinatra: README (Japanese)

ちなみに環境指定は環境変数を直接書いてもOK。

RACK_ENV=production ruby my_app.rb

Sinatra: README (Japanese)

モジュラースタイルでは、 configure ブロック内で以下のような形式でポートなどを指定します。

set :environment, :development
set :server, :thin
set :bind, 'localhost'
set :port, 4567

ルーティング

get '/' do
  ...
end

post '/' do
  ...
end

など。書かれた順に判断され、パスと最初に合致したルーティングが処理される。

パスの取得
get '/hello/:name' do

とすると :name 部分のパスの値が params[:name] で取得出来る。

get '/hello/:name' do |n|

のようにブロックパラメータを付けると、パラメータ nparams[:name] が入る。

get '/say/*/to/*' do

のようにワイルドカードアスタリスク)を使うことも出来る。アスタリスクの値は params[:splat] (配列)で取得出来る。

get '/download/*.*' do |path, ext|

のようにこれもブロックパラメータで受けることが可能。

get %r{/hello/([\w]+)} do

のように正規表現も使える。正規表現で括弧指定された値は params[:captures] (配列)で取得出来る。

get %r{/hello/([\w]+)} do |c|

のように、やはりこれもブロックパラメータで受けることが可能。

複数パスをDRYに表記その1
異なるルートで同じ対応をしたい

DRYを実践したい

require 'sinatra'

['/one', '/two', /'three'].each do |route|
  get rounte do
    "Triggered #{route} via GET"
  end

  post route do
    "Triggered #{route} via POST"
  end
end

Ruby - Sinatraの使い方あれこれ - Qiita

複数パスをDRYに表記その2
get '/foo' do
  p 'hi'
end
post '/foo' do
  p 'hi'
end

Sinatra::MultiRoute で DRY に。

$ gem install sinatra-contrib

して

require 'sinatra'
require "sinatra/multi_route"

route :get, :post, '/foo' do
    p 'hi'
end

Sinatra GET/POSTメソッドを一つのアクションで実装する方法 | HAPPY*TRAP

もちろんパスを複数取ることも出来る。

require 'sinatra'
require "sinatra/multi_route"

get '/foo', '/bar' do
  # ...
end

Sinatra::MultiRoute (part of Sinatra::Contrib)

パラメータ取得
RESTリクエストのパラメータを受け取りたい

例えばこんなリクエス

http://my.server.address/function?key1=value1&key2=value2&key3=value3

こんなふうに受けることができる

require 'sinatra'

get '/function' do
  params['key1']
  params['key2']
  params['key3']
end

Ruby - Sinatraの使い方あれこれ - Qiita

postやputでクライアント側から渡されるパラメータ

form の post で送られるパラメータはやはり params ハッシュに入れられる。ハッシュキーは input タグの name の値(または値のシンボル)。当然ですよね。
つまり

<html>
  <head>
    <title>Params Test</title>
  </head>
  <body>
    <form action="http://localhost:4567/login" method="post">
      username: <input type="text" name="username"><br>
      password: <input type="text" name="password"><br>
    <input type="submit">
    </form>
  </body>
</html>

に対して

require 'sinatra'

post '/login' do
  username = params[:username]
  password = params[:password]
end

で、 post で送られた username と password を取得出来ます。
ただしチェックボックス(<input type="checkbox">)では name に [] を必ず付ける必要があります。 [] を付けると返値が配列になります。しかし付けないと単一の文字列しか返りません。そのため、複数項目をチェックしてもチェック項目の最後の値しか返されません。
参照:はじめてのSinatra - ぱろっと・すたじお の「パラメータの受け渡し」

ストリーミング

動画を送るわけじゃ無いです、文字列を流れ(ストリーム)として送りたい場合です。
サーバ側で処理をしつつ、その進行状況をクライアント側へ送りたい、というのはしばしばあるのではないかと思います。通常であれば結果となる文字列(HTMLなど)をレスポンスとして一括送信するのですが、進行状況を送るのであればちょっとずつちょっとずつ文字列を送ることになります。
このようなことを実現するために、ストリーミングレスポンス (Streaming Responses) という手段を用いることが出来ます。

get '/' do
  stream do |out|
    out << "それは伝 -"
    sleep 0.5
    out << " (少し待つ) "
    sleep 1
    out << "- 説になる!"
  end
end

ストリーミングレスポンス (Streaming Responses)

このように stream ヘルパーを用いると、簡単に進行状況を逐一送ることが出来ます。

読み込み終えたらリロード

サーバ側でデータ取得を行うときに、その状況を stream ヘルパーで送信し、データ取得を終えたらリロードさせて本来の表示を行う、というやりかたをどうすれば良いのか悩みました。結局、 JavaScriptlocation.reload() を送って読み込ませるのが一番だと分かりました。なお location.reload(true) のように引数に true を設定しておけば、サーバから新規読み込みしてくれますので true にしておきましょう。

get '/' do
  stream do |out|
    out << "読み込み開始<br>"
    sleep 3
    out << "読み込み50%完了<br>"
    sleep 3
    out << "読み込み完了!"
    sleep 3
    out << '<script type="text/javascript">location.reload(true)</script>'
  end
end

ラシックスタイル対モジュラースタイル

Sinatra::Base - ミドルウェア、ライブラリおよびモジュラーアプリ

トップレベル(引用者註:クラシックスタイル?)は、軽量なアプリケーションのスタイルにおける設定(例えば、単一のアプリケーションファイル、./publicおよび./viewsディレクトリ、ロギング、例外詳細ページなど)を仮定しています。

ラシックスタイルの例(app.rb)
require 'sinatra'
# ... アプリケーションのコードを書く ...

ruby app.rbSinatra が起動します。

ラシックスタイルでの config.ru
require './app'
run Sinatra::Application

これで rackup すれば Sinatra が起動します。

モジュラースタイルの例
require 'sinatra/base'

class MyApp < Sinatra::Base
  # ... アプリケーションのコードを書く ...

  # Rubyファイルが直接実行されたらサーバを立ち上げる
  run! if app_file == $0
end

ruby my_app.rbSinatra が起動します。

モジュラースタイルでの config.ru
require './app'
run MyApp

これで rackup すれば Sinatra が起動します。

メリット/デメリット
モジュラースタイル vs クラッシックスタイル - Sinatra: README (Japanese)

一般的認識と違って、クラッシックスタイルを使うことに問題はなにもありません。
モジュラースタイルを使わずにクラッシックスタイルを使った場合の一番の不利な点は、Rubyプロセスごとにただ一つのSinatraアプリケーションしか持てない点です。

一方のスタイルから他方へ移行する場合、デフォルト設定がわずかに異なる点に注意が必要です。

set 設定 クラッシック モジュラー
Sinatra::Base
モジュラー
Sinatra::Application
app_file sinatraを読み込むファイル Sinatra::Baseをサブクラス化したファイル Sinatra::Applicationをサブクラス化したファイル
run $0 == app_file false false
logging true false true
method_override true false true
inline_templates true false true
static true false true
どちらを使う?

Rackとの関連でもこのスタイルのどちらを使うか悩んだりしますが。

config.ruはいつ使うのか? - Sinatra: README (Japanese)

config.ruファイルは、以下の場合に適しています。

  • 異なるRackハンドラ(Passenger, Unicorn, Herokuなど)でデプロイしたいとき
  • Sinatra::Baseの複数のサブクラスを使いたいとき
  • Sinatraミドルウェアとして利用し、エンドポイントとしては利用しないとき

モジュラースタイルに移行したという理由だけで、config.ruに移行する必要はなく、config.ruで起動するためにモジュラースタイルを使う必要はありません。(引用者強調)

ということで、まずはクラシックスタイルで作ってみて、問題が生じればモジュラースタイルへ移行する、というやり方でよいのではないでしょうか。

configure

どの環境でも起動時に1回だけ実行されます。
環境設定(RACK_ENV環境変数)が:productionに設定されている時だけ実行する方法:

configure :production do
  ...
end

環境設定が:productionか:testに設定されている時だけ実行する方法:

configure :production, :test do
  ...
end

設定したオプションにはsettingsからアクセスできます:

configure do
  set :foo, 'bar'
end

get '/' do
  settings.foo? # => true
  settings.foo  # => 'bar'
  ...
end

Sinatra: README (Japanese)

configure の引数を指定すればその環境だけ、指定無しならば全ての環境での設定になります。
configure ブロック内はアプリケーション/クラススコープ。

set/enable/disable

参考:Sinatra: Configuring Settings
アプリケーション/クラススコープにおいて、リクエスト/インスタンススコープで使う任意の設定項目と設定値を登録したり、組込設定項目の設定値を登録して環境設定したりする。
設定値はリクエスト/インスタンススコープにおいて、 settings オブジェクトに生やされた、設定項目と同名のメソッドで取得可能。

アプリケーション/クラススコープで利用可能な機能
set
アプリケーション/クラススコープの変数を定義する。組み込みの変数を設定することで基本動作を変更できる他、独自の変数の定義もできる。設定した値は(引用者註:メソッドが作られているので)アプリケーション/クラススコープであればその名称で直接参照でき、リクエスト/インスタンススコープでは settings を使用し参照できる。


リクエスト/インスタンススコープで利用可能な機能
settings
アプリケーション/クラススコープの情報を参照できる。

Sinatra コンパクトリファレンス - 君の瞳はまるでルビー - Ruby 関連まとめサイト

set :foo, 'bar'
settings.foo #=> "bar"

Proc/lambda での登録も可能。

set :foo, 'bar'
set :baz, Proc.new { "Hello " + foo }
settings.baz #=>"Hello bar"

複数値同時設定も可能。

set :foo => 'bar', :baz => Proc.new { "Hello " + foo }
enable/disable

enable と disable はそれぞれ true/false の設定のシンタックスシュガー。
すなわち

enable  :sessions, :logging
disable :dump_errors, :some_custom_option

set :sessions, true, :logging, true
set :dump_errors, false, :some_custom_option, false

の意味。

Built-in Settings
  • :environment - configuration/deployment environment
  • :raise_errors - allow exceptions to propagate outside of the app
  • :dump_errors - log exception backtraces to STDERR
  • :show_exceptions - enable classy error pages
  • :sessions - enable/disable cookie based sessions
  • :logging - log requests to STDERR
  • :method_override - enable/disable the POST _method hack
  • :run - enable/disable the built-in web server
  • :server - handler used for built-in web server
  • :bind - server hostname or IP address
  • :port - server port
  • :app_file - main application file
  • :root - The application’s root directory
  • :views - view template directory
  • :lock - ensure single request concurrency with a mutex lock
  • :public_folder - static files directory
  • :static - enable/disable static file routes

Sinatra: Configuring Settings

初期設定値は sinatra/lib/sinatra/base.rb に記載されています。

module Sinatra
  class Base
    set :environment, (ENV['RACK_ENV'] || :development).to_sym
    set :raise_errors, Proc.new { test? }
    set :dump_errors, Proc.new { !test? }
    set :show_exceptions, Proc.new { development? }
    set :sessions, false
    set :logging, false

    set :method_override, false

    set :run, false                       # start server via at-exit hook?

    set :server, %w[HTTP webrick]
    set :bind, Proc.new { development? ? 'localhost' : '0.0.0.0' }
    set :port, Integer(ENV['PORT'] && !ENV['PORT'].empty? ? ENV['PORT'] : 4567)

    set :app_file, nil
    set :root, Proc.new { app_file && File.expand_path(File.dirname(app_file)) }
    set :views, Proc.new { root && File.join(root, 'views') }

    set :lock, false

    set :public_folder, Proc.new { root && File.join(root, 'public') }
    set :static, Proc.new { public_folder && File.exist?(public_folder) }
  end

  class Application < Base # クラシックスタイルでの初期設定
    set :logging, Proc.new { ! test? }
    set :method_override, true
    set :run, Proc.new { ! test? }
    set :session_secret, Proc.new { super() unless development? }
    set :app_file, nil
  end
end

日本語情報:set, enable, disable による組み込み変数 - Sinatra コンパクトリファレンス - 君の瞳はまるでルビー - Ruby 関連まとめサイト

スコープ

  • 「アプリケーション」スコープという「概念」は、「クラス」スコープという「手段によって実現」されている。
  • 「リクエスト」スコープという「概念」は、「インスタンス」スコープという「手段によって実現」されている。

Sinatra はリクエスト毎にアプリケーション自身のインスタンスを生成し、そのインスタンスにリクエストを処理させている

本家の README にも載っています。

アプリケーション/クラスのスコープ

クラスレベルではgetやbeforeのようなメソッドを持っています。 しかしrequestやsessionオブジェクトには、全てのリクエストに対する単一のアプリケーションクラスがあるだけなので、アクセスできません。

リクエスト/インスタンスのスコープ

やってくるリクエストごとに、あなたのアプリケーションクラスの新しいインスタンスが作成され、全てのハンドラブロックがそのスコープで実行されます。

独自 logging

この方法はかなり非効率なので、もっといい方法がある気がしますが…。
とはいえルーティングブロックでいちいち Logger.new するよりは効率が良いと思います。

まず disable :logging で標準の log を止めます。そして set に Logger.new インスタンスをぶち込みます。

configure do
  disable :logging
  _log = Logger.new(STDOUT)
  _log.progname = 'log'
  set :_log, _log
end

アプリケーション/クラスのスコープで set にぶち込んだインスタンスは、リクエスト/インスタンスのスコープだと settings に生えたメソッドから抜き出せます。
ということで、ルーティングのブロックで使いたい場合には、 settings から logger インスタンスを抜き出してきて、それを使います。

helpers do
  def log(str)
    _log = settings._log
    _log.info(str)
  end
end

get '/' do
  log(str)
end

session (つまり Cookie )に情報を全て入れない為に

ネットなどでいちばん良く説明されているのが Rack::Session::Cookie だが、セッション情報の全てを Cookie へ入れてしまうので盗まれた場合の危険性が高い。 Rack::Session::Memcache では、セッション ID のみ cookie に、その他のセッション情報が memcached に保存される(もちろん memcached が動いている必要がある)。 Rack::Session::Pool でも同様に、セッション ID のみ cookie に、その他のセッション情報がインスタンス変数 @pool に保持される(当然ながらアプリを終えれば消える)。

Sinatra で enable :sessions すると使えるセッション情報保持ローカル変数 session は Rack::Session::Cookie に保存される。

Rack::Session::Pool を使う例('15/2/21追記)
use Rack::Session::Pool,
  path: '/',
  domain: nil,
  expire_after: 60 * 10, # Cookieの有効期限を10分に
  secret: Digest::SHA256.hexdigest(rand.to_s)

余談ですが Digest::SHA1.hexdigest(rand.to_s) はもう古いので止めましょうね。

非同期処理とかWorkerプロセスとか

HerokuのcedarスタックでRuby使う時はwebサーバーとしてThinが起動する。
ThinはEventMachineの中で動いてるので、EM::defer等が使える。
Herokuは1プロセス目は無料、2プロセス目を起動させると課金されるが、EventMachineでworkerをWebアプリのプロセス内に同居させればお金がかからなくなる。

上手い。
関連:Sinatra Recipes - Embed - Event-machine

アセットパイプラインをSinatra

アセットパイプラインとは

JavaScriptCSSを結合したり圧縮したりする。また、CoffeeScriptやSass、ERBの言語を使ってJavaScriptCSSを書くこともできる。

アセットパイプライン(Asset Pipeline) - Railsドキュメント

ということだそうです。
これを Sinatra で使うには sinatra-assetpack gem を使えば良いとのこと。

インストール
$ gem install sinatra-assetpack
Fetching: jsmin-1.0.1.gem (100%)
Fetching: sinatra-assetpack-0.3.3.gem (100%)
##################################################
#  NOTE FOR UPGRADING FROM PRE-3.0.0 VERSION     #
##################################################

Please note that sinatra-assetpack `img` helper method
no longer sets the image width and height. This default
behavior is not desirable with more recent CSS techniques
regarding high resolutions images for "retina" displays
and "responsive" design CSS using, for example, width:100%.

<img src="retina.jpg" width="1200" height="600">

Is now simply this (unless you explicitly set them) :

<img src="retina.jpg">

See https://github.com/rstacruz/sinatra-assetpack/pull/121
Successfully installed jsmin-1.0.1
Successfully installed sinatra-assetpack-0.3.3
2 gems installed

なにやら注意書きが入ってますね。画像サイズ指定を生成していたのに Retina 対応のため入れなくなった、ってことでしょうか。

Sinatraアプリケーションサンプル

橋本商会さん

ファイルの配置などが参考になる。そして、ルーティングを別ファイルにしておいて、それらを config.ru で呼び出した Bootstrap.init というメソッド(bootstrap.rb に設定してある)を使って読み込んでいるのも上手い。

Twitter Bootstrapの一例

Twitter Bootstrap を使った一例。なお app.rb は修正する必要あり。

set :public_folder, File.dirname(__FILE__) + '/public'
set :haml, :format => :html5

get '/' do の外へ出しておく。また get '/room' do にある

  name = request[:name]

  name = request[:name] || ""

に修正しないと名前未登録時に落ちる。