sinatraset :foo, 'bar'みたいなDSLを使って値を設定できる。でもこの値ってどこにどうやって保存されているんだろう?

普通、最終的にはインスタンス変数かクラス変数に格納されてるって思うよね。

(class << self; self; end).class_eval do
  undef_method(name) if method_defined? name
  String === content ? class_eval("def #{name}() #{content}; end") : define_method(name, &content)
end

sinatra/lib/sinatra/base.rb at v1.4.5 · sinatra/sinatra

ところがset :foo, 'bar'するとbarを返すfooというメソッドが定義されるという形で保存されている。

setするたびにundef_methodして定義し直すので変数みたいに使えるようだ。静的な頭で考えてたのでクラクラするような格納方法だ。

# hello.rb:
require 'rubygems'
require 'sinatra/base'

class Hello < Sinatra::Base
  get '/' do
    'hello'
  end
end

Hello.run! if __FILE__ == $0
$ ruby hello.rb

Sinatra::Baseと違ってSinatra::Applicationは便利な初期化をかなり色々やってくれるので1ファイルで書く時はやっぱり普通のがオススメ。

routesがシンプルなところ。

# routes.rb:
require 'rubygems'
require 'sinatra'
require 'pp'

get('/foo')  {}
post('/foo') {}
get('/bar')  {}
post('/bar') {}

pp Sinatra::Application.routes

exit
% ruby routes.rb 
{"GET"=>
  [[/^\/foo$/,
    [],
    [],
    #<Proc:0x000001009c5a58@/Users/komagata/.rvm/gems/ruby-1.9.2-p290@default/gems/sinatra-1.3.1/lib/sinatra/base.rb:1212>],
   [/^\/bar$/,
    [],
    [],
    #<Proc:0x0000010159c318@/Users/komagata/.rvm/gems/ruby-1.9.2-p290@default/gems/sinatra-1.3.1/lib/sinatra/base.rb:1212>]],
 "HEAD"=>
  [[/^\/foo$/,
    [],
    [],
    #<Proc:0x0000010159cd90@/Users/komagata/.rvm/gems/ruby-1.9.2-p290@default/gems/sinatra-1.3.1/lib/sinatra/base.rb:1212>],
   [/^\/bar$/,
    [],
    [],
    #<Proc:0x0000010159bee0@/Users/komagata/.rvm/gems/ruby-1.9.2-p290@default/gems/sinatra-1.3.1/lib/sinatra/base.rb:1212>]],
 "POST"=>
  [[/^\/foo$/,
    [],
    [],
    #<Proc:0x0000010159c868@/Users/komagata/.rvm/gems/ruby-1.9.2-p290@default/gems/sinatra-1.3.1/lib/sinatra/base.rb:1212>],
   [/^\/bar$/,
    [],
    [],
    #<Proc:0x0000010159ba08@/Users/komagata/.rvm/gems/ruby-1.9.2-p290@default/gems/sinatra-1.3.1/lib/sinatra/base.rb:1212>]]}

Hash, Array, Regex, Procの組み合わせに過ぎない。自由度MAX。

これはセクシーだ。

module Foo
  module Helpers
    def bar
      'unk'
    end
  end
end

こういうHelpersを

helpers do
  include Foo::Helpers
end

こういう風に使ってた場合。

RSpec.configure do |config|
  config.include Foo::Helpers
end
describe Foo::Helpers do
  context 'bar' do
    it 'should return unk' do
      bar.should eql('unk')
    end
  end
end

Spec::Runner.configでincludeするとテスト出来る。

LokkaはSinatraベースなので同じようにHelpersのテスト書ける。でもRspecややこしいな。config.includeのとことか。とTest::Unit, Shoulda信者が申しております。

同じrouteを2回定義する件。

routesも要はRegexpとブロックの配列。でも最初に定義した方が呼ばれるって何か変。こういうものって後に追加した方が呼ばれる設計にする方が自然な気がする。

そこでもしかして同じroutesとか関係なくて、単にファイル内で上から下に書いてく時の優先順位がそのまんまこの設計に反映されてる的な感じなんじゃないかと思ってpassしてみた。

# public/plugin/lokka-unk/lib/lokka/unk.rb:
module Lokka
  module Unk
    def self.registered(app)
      app.get '/' do
        puts 'unk'
        pass
      end
    end
  end
end
% bundle exec ruby lokka.rb
unk
localhost - - [17/Feb/2011:10:41:33 JST] "GET / HTTP/1.1" 200 38183
- -> /

普通にページは表示されつつ、標準出力にunk。予想通りだ。これは擬似beforeみたいにも使えそう。(普通にbefore '/' do ... endした方が良いが。)

sinatraはルールがシンプルだから自由度が高いなあ。

何だか感慨深い。Java(僕のJava知識はJ2SE1.4で止まってる)とかその他の大きめなフレームワークだとChain-of-responsibilityパターンとかいってFilterChainみたいな感じで実装すると思うんだよね。こういうの。

sinatraはちっちゃいからRegexpとブロックのオブジェクトを配列にもってるから単にナメればいいでしょ?みたいな雰囲気。

Filterクラスを継承したクラスを作るとか面倒。Objective-Cだとブロック対応してない環境のためにワザワザdelegate作るのでヘッダも必要だしとか・・・だるいよなあ(ただの愚痴です・・・)

require 'rubygems'
require 'sinatra'

get('/') { 'unk' }
get('/') { 'shit' }
% curl http://localhost:4567
unk

先に定義された方が実行される。

Railsを追いきれる自信が無かったから。Rails文化に引っ張られてアプリが一生完成しない気がしたから。あとアプリとしては問題無いのにベースのRailsのバージョンが低いだけで残念っぽくなってるアプリ(Redmineとか)を見たから。

半年やってみてSinatra面倒クセー!っていっぱいあったけど、(Sinatra本体の)ソースが短いので完全把握できる掌握感は独自のOSS作る上で心強かった。

pluginの事を考えるとbeforeが複数回定義されてもちゃんとそれぞれが実行されないと意味が無い。なので試してみた。

komagata's double_before at master - GitHub

require 'rubygems'
require 'sinatra'

before do
@name = 'Masaki'
end

before do
@name += ' Komagata'
end

get '/' do
"Hello, #{@name}"
end
require 'rubygems'
require 'test/unit'
require 'rack/test'
require 'shoulda'
require './double_before'

class DoubleBeforeTest < Test::Unit::TestCase
include Rack::Test::Methods

def app
Sinatra::Application
end

context "Access pages" do
should "show index" do
get '/'
assert_equal 'Hello, Masaki Komagata', last_response.body
end
end
end
% ruby double_before_test.rb 
Loaded suite double_before_test
Started
.
Finished in 0.030489 seconds.

1 tests, 1 assertions, 0 failures, 0 errors

おお、問題無い!これは期待してなかったので嬉しいですゾ!(@ムック)

- content_for :foo do
%p One
- content_for :foo do
%p Two
%h1 Hello, World!

Double content_for

2回同じ名前で呼んでも大丈夫みたいです。便利ー。

komagata's double-content-for at master - GitHub