RackとERBを使って、超シンプルなRubyのWebアプリを作る

Railsで使われているRackとERB。いつもRailsにお任せだから、たまにはRailsの皮をはいで生で触ってみたい!ということで、超基本的な部分(通常表示, リダイレクト, not found)だけいじったのでメモ。

・/にアクセスすると、/hogeにリダイレクトされる
・/hogeは変数nameとsexに格納した値をerbで表示する
・それ以外のURLにアクセスされたら404エラーを返す

という代物を作る

config.ru

#coding: utf-8                                                                                                                                 

run lambda {|env|
  request = Rack::Request.new(env)

  case request.path
  when '/' 
    Rack::Response.new {|r| r.redirect("/hoge")}
  when '/hoge'
    name, sex = 'maeharin', 'man'

    html = ERB.new(<<-EOF).result(binding)
      <html>
      <head><meta charset="utf-8"></head>
      <body>
      私の名前は<%= name %>。性別は<%= sex %>です。
      </body>
      </html>
    EOF

    Rack::Response.new(html)
  else
    Rack::Response.new("not found", 404)
  end 
}

サーバー起動

config.ruがあるディレクトリで

$ rackup

localhost:9292にアクセス

動いた!シンプルで素敵すなあ。色々いじってみよう。

※RackとERBの詳細は以下の記事などを参考。
第23回 Rackとは何か(1)Rackの生まれた背景:Ruby Freaks Lounge|gihyo.jp … 技術評論社
Rubyist Magazine - 標準添付ライブラリ紹介 【第 10 回】 ERB

Rubyの多重代入におけるto_aとto_aryの挙動

先日のエントリRubyの多重代入についてふれたのだが、教科書によってto_aryが呼ばれると書いてあったりto_aが呼ばれると書いてあったりで、なにが本当なのか分からなくなった。

回答らしきもの

調べてみたら、こちらに回答らしきものがあった。

OK, to_a means an explicit array conversion, so it should not be used
for implicit conversion a in "*y = x". On the other hand, I consider
"*x" as a form of explicit conversion, i.e. shorthand for "*(x.to_a)",
so that the current 1.9 behavior is intentional.


matz.

http://bugs.ruby-lang.org/issues/show/1393

どうやら、Ruby1.9においては、暗黙的なコンテキストではto_aryが呼ばれ、明示的なコンテキストではto_aが呼ばれるようだ。

1.9.2で試してみた

class C                                                                                                                                                                                              
  def to_ary; [1,2]; end
  def to_a; [3,4]; end
end
c = C.new
 
a, b = c # 暗黙的
d, e = *c # 明示的
 
puts a, b # 1,2
puts d, e # 3,4

確かに暗黙的なコンテキスト(a,b = c)ではto_aryが呼ばれており、明示的なコンテキスト(d,e = *c)ではto_aが呼ばれていることが分かる。ただ、これはto_aryとto_aの両方が定義されている場合の挙動であり、片方の場合には挙動が違った。上記においてto_aryのみを定義した場合、明示的コンテキストにおいてもto_aryが呼ばれる(to_aの代わりに)。しかしto_aのみを定義した場合は、暗黙的コンテキストにおいては通常の多重代入が行われる(to_aryの代わりにto_aが呼ばれることはない)。結構ややこしい。

1.8.7でも試してみた。

class C                                                                                                                                                                                              
  def to_ary; [1,2]; end
  def to_a; [3,4]; end
end
c = C.new
 
a, b = c # 暗黙的
d, e = *c # 明示的
 
puts a, b # 1,2
puts d, e # 1,2

さらにややこしいことに、1.8.7の場合、1.9.2とは挙動が違った。to_aryとto_a両方が定義されていると、暗黙的でも明示的でもto_aryが呼ばれる。片方だけ定義された場合でいうと、to_aryのみが定義されている場合、暗黙的でも明示的でもto_aryが呼ばれる。一方to_aのみが定義されている場合は、暗黙的コンテキストでは通常の多重代入が行われ、明示的コンテキストにおいてはto_aが呼ばれる。


まとめ

すごく混乱したので、まとめておく。あまりこの違いで悩むことは無いだろうけど。。。

Ruby バージョン 暗黙的コンテキスト 明示的コンテキスト
1.9.2 to_ary to_a(なければto_ary)
1.8.7 to_ary to_ary(なければto_a)

Rack::Responseオブジェクトを多重代入すると、暗黙的にfinishが呼ばれる

環境:ruby-1.9.2、rack-1.4.1

RAILSCASTのRackの章を見ていたら、以下のような構文が出てきて、不思議に思った。

def call(env)
  Rack::Response.new("hoge")
end

Rackアプリの呼び出し側(例えば、lintミドルウェア)では、このように書かれている

status, headers, @body = @app.call(env)

上記の場合、@app.call(env)を評価した結果はRack::Responseオブジェクトであるはず。なのに、それを多重代入できるのはなぜか?配列じゃないといけないのでは?

多重代入の仕様が怪しいので、調べる。

4.5.5.3 複数の左辺、1 個の配列の右辺

左辺は複数あるが、右辺は 1 つしかない場合、Ruby はその右辺を代入すべき値のリストに展開しようとす
る。右辺が配列なら、Rubyは配列を展開し、個々の要素が独自の右辺になるようにする。右辺が配列でなくても、to_aryメソッドを実装する場合は、to_aryを呼び出して配列を作ってから、その配列を展開して代入する。

プログラミング言語 Ruby P102

どうやら、上記のような状況下で多重代入を行うと、右辺のオブジェクトにto_aryメソッドがある場合、その実行が行われるようだ。ということは、おそらくRack::Responseクラスのインスタンスメソッドにto_aryが定義されているはず。Rackのソースコードを見てみる。

# rack-1.4.1/lib/rack/response.rb
def finish(&block)                                                                                                                                                                                          
  @block = block

  if [204, 205, 304].include?(status.to_i)
    header.delete "Content-Type"
    header.delete "Content-Length"
    [status.to_i, header, []] 
  else
    [status.to_i, header, self]
  end 
end 
alias to_a finish           # For *response
alias to_ary finish         # For implicit-splat on Ruby 1.9.2

to_aryメソッドはfinishのエイリアスになっていた!なので、Rack::Responseオブジェクトが暗黙的に多重代入されると、finishメソッドの実行結果が代入されるということになる。なるほどー!

※追記
Rubyの多重代入におけるto_aとto_aryの挙動について書いた

RubyのFile.expand_path('相対パス', __FILE__)の意味

RailsなどのRubyライブラリのソースコードを見ていると、よく

File.expand_path('相対パス', __FILE__)

という一文を目にする。ちょっと調べてみた。

File.expand_pathとは

riコマンドで調べてみる

$ ri File.expand_path

(from ruby core)
------------------------------------------------------------------------------
  File.expand_path(file_name [, dir_string] )  ->  abs_file_name

------------------------------------------------------------------------------

Converts a pathname to an absolute pathname. Relative paths are referenced
from the current working directory of the process unless
dir_string is given, in which case it will be used as the
starting point. The given pathname may start with a ``~'', which expands to
the process owner's home directory (the environment variable HOME must be set
correctly). ``~user'' expands to the named user's home directory.

       File.expand_path("~oracle/bin")           #=> "/home/oracle/bin"
       File.expand_path("../../bin", "/tmp/x")   #=> "/bin"

相対パス絶対パスに変換した文字列を返す
・第2引数を指定しない場合「プロセスのカレントワーキングディレクトリ」を相対パスの基準にする
・第2引数を指定した場合「第2引数で指定したディレクトリ」を相対パスの基準にする

検証用ディレクトリ構造

検証用に、以下のディレクトリを作った。a.rbからlib/を参照することを想定して、検証する。

/
└── Users/
    └── hidenorimaehara/
        └── xxx/
            └── yyy/
                └── a.rb
                └── lib/

File.expand_path('./lib')

まずは第2引数を指定しないでやってみる。

a.rb

プロセスのカレントワーキングディレクトリを確認するため、Dir.pwdの結果も表示することにする。

puts Dir.pwd
puts File.expand_path('./lib')
yyyディレクトリで実行
$ ruby a.rb
/Users/hidenorimaehara/xxx/yyy
/Users/hidenorimaehara/xxx/yyy/lib
xxxディレクトリで実行
$ ruby ./yyy/a.rb
/Users/hidenorimaehara/xxx
/Users/hidenorimaehara/xxx/lib

プロセスのカレントワーキングディレクトリを相対パスの基準として絶対パスに変換していることが分かる。つまり、コマンドを実行するディレクトリによって結果が変わる。

File.expand_path('./lib', '/Users/hidenorimaehara/xxx/yyy/')

次に第2引数を与えてみる。yyyディレクトリの絶対パスを第2引数に指定してみる。

a.rb

さきほどのa.rbを以下のように変更。

puts Dir.pwd
puts File.expand_path('./lib', '/Users/hidenorimaehara/xxx/yyy/')
yyyディレクトリで実行
$ ruby a.rb
/Users/hidenorimaehara/xxx/yyy
/Users/hidenorimaehara/xxx/yyy/lib
xxxディレクトリで実行
$ ruby ./yyy/a.rb
/Users/hidenorimaehara/xxx
/Users/hidenorimaehara/xxx/yyy/lib

第2引数に指定したディレクトリを基準として相対パスを解析していることがわかる。

__FILE__

ここで一旦脇道にそれて、__FILE__について調べる。__FILE__は、現在のソースファイル名を返す変数。実際にどのような値を返しているのか調べてみる。

a.rb
puts __FILE__
yyyディレクトリで
$ ruby a.rb
a.rb
xxxディレクトリで
$ ruby ./yyy/a.rb
./yyy/a.rb

コマンドを実行したディレクトリからの相対パスでファイル名を返しているようだ。(こちらによると相対を返すか絶対を返すかは状況によって変わるらしい)

File.expand_path('./lib', __FILE__)

となるとFile.expand_pathの第2引数に__FILE__を指定すれば、現在のソースファイルを基点とできる。第1引数に'./lib'、第2引数に__FILE__を指定してみる。

a.rb
puts Dir.pwd
puts File.expand_path('./lib', __FILE__)
yyyディレクトリで
$ ruby a.rb
/Users/hidenorimaehara/xxx/yyy
/Users/hidenorimaehara/xxx/yyy/a.rb/lib
xxxディレクトリで
$ ruby ./yyy/a.rb 
/Users/hidenorimaehara/xxx
/Users/hidenorimaehara/xxx/yyy/a.rb/lib

ありゃ
./では、a.rb(ファイル名)自体もパスに入ってしまっている。

File.expand_path('../lib', __FILE__)

ということで、第1引数に../libにしてみる。

a.rb
puts Dir.pwd
puts File.expand_path('../lib', __FILE__)
yyyディレクトリで
$ ruby a.rb
/Users/hidenorimaehara/xxx/yyy
/Users/hidenorimaehara/xxx/yyy/lib
xxxディレクトリで
$ ruby ./yyy/a.rb 
/Users/hidenorimaehara/xxx
/Users/hidenorimaehara/xxx/yyy/lib

できた!

以下のように使う

こうすれば、プロセスのカレントワーキングディレクトリがどこであっても、相対的な位置関係を参照できる

# このソースファイルと同ディレクトリにあるb.rbをrequire
require File.expand_path('../b', __FILE__)

# このソースファイルと同ディレクトリにあるlibディレクトリをrequireのロードパスに追加
$: << File.expand_path('../lib', __FILE__)

rbenvのメモ

備忘のためメモ。必要に応じて編集予定。

ヘルプ

$ rbenv -h

主要コマンドの説明はこれで

特定のrbenvコマンドのヘルプを表示

$ rbenv help <コマンド>

現在のRubyバージョンを表示

$ rbenv version

rbenvが管理している全てのRubyバージョンを表示

$ rbenv versions

インストールできるversionを表示

$ rbenv install

特定のversionをインストール

$ rbenv install 1.9.3-rc1
$ rbenv rehash

versionの切り替え(global)

$ rbenv global 1.9.3-rc1

versionの切り替え(local)

$ rbenv local 1.9.3-rc1

すると、そのディレクトリに.rbenv-versionというファイルが作成される

ローカル設定を解除したい時は

$ rbenv local --unset

詳しくは

rbenv help local

誰得スクリプトで学ぶOAuthとRack(Rubyだよ)

元ネタ

ゆーすけべー先生が先日、以下のような誰得スクリプトを書いておられました。

Facebookのポスト内容をWebアプリケーションとして実行する - ゆーすけべー日記

まじ誰得w しかしよく見てみると勉強になる要素満載で、Rubyで書いたらOAuthとRackの勉強になるぞと思ったのでやってみた

スクリプト企画の全容

しかしそもそもこの謎スクリプト、企画の全容が謎w なので、まずは企画の全容を明らかにする。折角なので先生の著作Webサービスのつくり方に書かれている企画メソッドを借パクしてあてはめてみる。(名前は勝手にfacepostとする)

謎いw

内部設計(のようなもの)

折角なので内部設計(のようなもの)も真面目にやってみる。

1. ユースケース

こいつを最初に書いておくと「あれ?自分何作ってんだっけ?」という状態にならずに済むような気がする。今回の例の場合「誰の役にもたたない」という大変重要な点を見失わずに済む。

2. ユーザーシナリオ

次にユーザーがどういう体験をするのかを簡単に書く。ここまでやれば、大体作るもののイメージが明確になってくる。

3. 技術調査

作るにあたって、技術的に良くわからない箇所があるので、調査する。まず、facebookに投稿した内容を取得するにはどうすればよいのか?というのが良くわからないので、facebookのdeveloper siteを見る。

3-1. Facebook API

投稿に関する情報は以下のページ。どうやらaccess_tokenを取得する必要がある。
https://developers.facebook.com/docs/reference/api/post/

access_tokenを取得する方法は以下のページ。今回はlocalhostを使用するので、Server-side Loginの部分が該当箇所となる。数回リダイレクトしてaccess_tokenを取得することが分かった。
https://developers.facebook.com/docs/concepts/login/login-architecture/

3-2. Rack

今回はWebサービスとしてlocalhostで動かすので、WAFを使うか?使うなら何使うか?という調査・選択が必要。RailsSinatraを使うまでもない規模なので、その大元であるRackを単体で使ってみることにした。Rack単体で使うのは初めてなので、調査。調べてみると、Rackとは

・WebサーバとWAFをつなぐプログラム
・Rackアプリケーションに対してrackupコマンドを実行すれば、Webサービスが立ち上がる
RailsSinatraもRackアプリケーションとして動いている
・Rackアプリケーションとは、以下の用件を満たすもの

・callというメソッドを持っていること
・callメソッドの引数としてWebサーバからのリクエストを受けること
・callメソッドは,次の要素を含むレスポンスを返すること
  ・ステータスコード
  ・レスポンスヘッダ(Hash)
  ・レスポンスボディ(Array)

引用:第23回 Rackとは何か(1)Rackの生まれた背景:Ruby Freaks Lounge|gihyo.jp … 技術評論社

といったことが分かる。といってもよく分からないので、sandboxというフォルダを作って色々いじってみたです。

4. 簡単な仕様

設計の最後に、これから書くプログラムの簡単な仕様を書いた。ざっと以下のような感じ。

facebookの投稿idを引数にrackupするとlocalhost:9292が立ち上がる
・ユーザーがhttp://localhost:9292/にアクセスすると、OAuth認証が行われる
・コールバックURLはhttp://localhost:9292/callbackとする
・ユーザーがfacebookに投稿したコードは標準出力になんらかの文字列を出力するコードであることを期待する
・標準出力に出力された文字列をブラウザに表示する

実装

いよいよ実装。Rackアプリケーションの書き方や、標準入出力の扱い方など結構はまった。けど動いたよ!

config.ru
#coding: utf-8

require 'open-uri'
require 'json'
require 'pit'

config = Pit.get("facebook.com")
app_id = config["app_id"]
app_secret = config["app_secret"]
post_id = ARGV[0]

map '/' do 
  run lambda {|env|
    uri = URI.escape("#{env['REQUEST_URI']}callback")
    redirect_url = "http://www.facebook.com/dialog/oauth?client_id=#{app_id}&redirect_uri=#{uri}&scope=read_stream"
    [302, {'Content-Type' => 'text', 'location' => redirect_url}, []]
  }
end

map '/callback' do 
  run lambda {|env|
    # get access_token
    req = Rack::Request.new(env)
    code = req[:code]
    uri = URI.escape("#{req.base_url}#{req.path}")
    token_url = "https://graph.facebook.com/oauth/access_token?client_id=#{app_id}&redirect_uri=#{uri}&client_secret=#{app_secret}&code=#{code}"
    access_token = ''
    open(token_url) {|f| access_token = f.read}

    # get facebook post
    post_url = "https://graph.facebook.com/#{post_id}?#{access_token}"
    json = ''
    open(post_url) {|f| json = f.read}
    fb_post = JSON.parse(json)
    fb_message = fb_post['message']

    # exec facebook post as code
    # treat stdout as response body
    body = ''
    IO.pipe {|r, w|
      w.instance_eval fb_message
      w.close
      body = r.read
    }

    # create response
    res = Rack::Response.new {|r|
      r.status = 200
      r['Content-Type'] = 'text/html;charset=utf-8'
      r.write body
    }
    res.finish
  }
end

Github

できあがり!

以下のような感じでFacebookに投稿したコードをlocalhostで実行できた!

1. facebookにコードを投稿。その投稿IDを控える

2. 上記config.ruのあるディレクトリで以下のコマンドを実行
$ rackup 控えたID config.ru
3. ブラウザでhttp://localhost:9292/にアクセス
4. ぐははは!

※用法容量をよく守ってお使いください
※元ネタと若干仕様が変わっております

勉強になりましたNE!

Rubyで「正規表現でマッチした部分を抽出する」を書いたのだが、なんかしょぼかったので復習

Rubyで「文字列から正規表現にマッチする部分だけぶっこ抜く」ってのをやろうとしてこんな風に書いたのだけど、もっとシンプルに書けるはずだったので復習。

str = 'http://instagram.com/p/hoge/' #hogeを抜き出したい
m = str.match(/http:\/\/instagram.com\/p\/(.+?)\//)
id = m[1]
puts id

まず、http:\/\/みたいにエスケープしまくるのが面倒。%rを使えば完結に書けた。%rは直後に来る文字を正規表現のデリミタとする。今回の例だと/が何度も出てくるので、/以外の文字をデリミタにすれば、何度もエスケープする必要がなくなる。

%r{http://instagram.com/p/(.+?)/}

デリミタにする文字は何でもよい。なので、これも等価。

%r!http://instagram.com/p/(.+?)/!

次にm=の部分。変数に代入する必要はない。str.match(%r{http://instagram.com/p/(.+?)/})はマッチするとMatchDataオブジェクトを返す。[]はMatchDataオブジェクトのメソッドなので、普通に繋げて書ける。結局、最初のコードは以下のように書ける。

str = 'http://instagram.com/p/hoge/'
id = str.match(%r{http://instagram.com/p/(.+?)/})[1]
puts id
# p (str.match(%r{http://instagram.com/p/(.+?)/})).class => MatchData

ちなみに、上記ではmatchメソッドを使ったが、=~演算子を使っても書ける。ただし、その場合マッチした結果返ってくる値はMatchDataオブジェクトではなく、マッチした部分の位置になる。MatchDataオブジェクトはどこにセットされているかというと、$~という変数にセットされている。

str = 'http://instagram.com/p/hoge/'
str =~ %r{http://instagram.com/p/(.+?)/}
id = $~[1]
puts id
# p (str =~ %r{http://instagram.com/p/(.+?)/}) => 0
# p $~.class => MatchData

正規表現苦手意識克服したい