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.
どうやら、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を呼び出して配列を作ってから、その配列を展開して代入する。
どうやら、上記のような状況下で多重代入を行うと、右辺のオブジェクトに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
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
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
内部設計(のようなもの)
折角なので内部設計(のようなもの)も真面目にやってみる。
3. 技術調査
作るにあたって、技術的に良くわからない箇所があるので、調査する。まず、facebookに投稿した内容を取得するにはどうすればよいのか?というのが良くわからないので、facebookのdeveloper siteを見る。
投稿に関する情報は以下のページ。どうやら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を使うか?使うなら何使うか?という調査・選択が必要。RailsやSinatraを使うまでもない規模なので、その大元であるRackを単体で使ってみることにした。Rack単体で使うのは初めてなので、調査。調べてみると、Rackとは
・WebサーバとWAFをつなぐプログラム
・Rackアプリケーションに対してrackupコマンドを実行すれば、Webサービスが立ち上がる
・RailsやSinatraも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
できあがり!
以下のような感じでFacebookに投稿したコードをlocalhostで実行できた!
1. facebookにコードを投稿。その投稿IDを控える
2. 上記config.ruのあるディレクトリで以下のコマンドを実行
$ rackup 控えたID config.ru
3. ブラウザでhttp://localhost:9292/にアクセス
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
正規表現苦手意識克服したい