ラベル Ruby の投稿を表示しています。 すべての投稿を表示
ラベル Ruby の投稿を表示しています。 すべての投稿を表示

2014年7月19日土曜日

書籍「パーフェクトRuby」読了

書籍「パーフェクトRuby」(Rubyサポーターズ著、技術評論社)を読み終わりました。技術書として読み応えのある一冊であったと思います。

Rubyは基本的な文法をマスターすることはもちろんですが、次に組み込みクラスとモジュールの理解が重要です。組み込みクラスとモジュールをしっかり理解しているかどうかは、Rubyを使いこなす分かれ目にもなります。ここが網羅的に分かりやすく書いてあったのは大変勉強になりました。

加えて「パーフェクトRuby」を読むまではあまり知らなかったメタプログラミング系の知識が得られたのも大きかったと思います。Rubyにおけるクラスやオブジェクトの成り立ちや、動的なオブジェクトの操作についての理解は、現場でのOJT的な勉強だけだとなかなか身に付けづらいものがあり、こういった書籍での体系化された知識に触れることが理解に大いに役立ちます。

さらに、BundlerやYARDなどのよく使われる標準外のツールの説明や、実際のアプリケーション開発をコマンドラインアプリケーションとWebアプリケーションを具体例として解説してあるのもいいポイントでしたね。Webアプリケーションの方は、RackやSinatraといった業界標準のツールにも触れていて、僕も実際仕事で使っているものですが、知識を補完することが出来ました。

それにしても技術評論社のパーフェクトシリーズは良書が多いと思います。「パーフェクトPHP」「パーフェクトJavascript」に続き「パーフェクトRuby」もリファレンス本として手元においておく一冊にしようと思います。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年7月8日火曜日

vimdiffでより賢いアルゴリズム(patience,histogram)を使う

vimdiff使ってますか?差分を取る際には非常に便利ですよね。git difftoolに設定して使っている人も多いと思います。しかしgit diffは差分計算のアルゴリズムを選択できますが、vimdiffはデフォルトでは差分計算のアルゴリズムを選択できません。

git diffの差分アルゴリズムには標準のもの以外にpatienceやhistogramがあり、より人間に読みやすい差分を表示してくれる「賢い」アルゴリズムになっています。それぞれgit diffコマンドのオプション--patienceや--histogramを付けるか、または~/.gitconfig設定ファイルにアルゴリズムを指定することで利用できます。

具体的なアルゴリズムの詳細は僕も詳しくないですが、patienceアルゴリズムは「ファイル内でユニークかつ比較ファイル同士で一致する行をなるべく"変化していない行"と認識する」よう働きます。また、histogramアルゴリズムは「patienceアルゴリズムの基本的ルールを継承しつつ高速化を図ったアルゴリズム」で、多くのファイルでpatienceアルゴリズムと同一の結果が得られるようです。また、histogramアルゴリズムはEclipseに統合されたGitクライアントEGit(が使うJGit)の標準アルゴリズムです。僕はコマンドラインツールのgitにおいてもhistogramアルゴリズムを使ってます。

さて、vimdiffにおいてもhistogramアルゴリズムを使いたいところです。vimの設定ではvimdiffの差分計算に使用するコマンドをdiffexprという設定値で指定できます。そして、git diffコマンドも--no-indexオプションを指定することでdiffコマンドと同じようにスタンドアロンツールとして利用できます。じゃあ、diffexprにgit diff --no-indexを指定すればいいのか・・・というと実はそれだけでは失敗します。

理由は、vimがdiffexprに設定したコマンドから受け付ける差分形式が、diffコマンドのnormal形式かed形式でなければならないからです(vimのヘルプにはed形式とだけ書いてあるのですが、normal形式も受け付けます)。そして、git diffコマンドでは直接diffコマンドのnormal形式やed形式で出力することができずunified形式のみに対応しています。

そこで、unified形式をnormal形式に変換するrubyスクリプトを書いて使うことにしました。以下がその変換スクリプトです。利用にはrubyが必要です。

#!/usr/bin/env ruby
iwhite = ''
if (ARGV[0] == '-b')
  iwhite = ARGV.shift.dup << ' '
end

diffout = `git diff --no-index --no-color -U0 #{iwhite}#{ARGV[0]} #{ARGV[1]}`
diffout.sub!(/\A.*?@@/m, '@@')
diffout.gsub!(/^\+/, '> ')
diffout.gsub!(/^-/, '< ')
diffout.gsub!(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@.*/) do
  before_start = $1
  before_size = $2
  after_start = $3
  after_size = $4

  action = 'c'
  if (before_size == '0')
    action = 'a'
  elsif (after_size == '0')
    action = 'd'
  end

  before_end = ''
  if (before_size && before_size != '0')
    before_end = ",#{before_start.to_i + before_size.to_i - 1}"
  end

  after_end = ''
  if (after_size && after_size != '0')
    after_end = ",#{after_start.to_i + after_size.to_i - 1}"
  end

  "#{before_start}#{before_end}#{action}#{after_start}#{after_end}"
end
diffout.gsub!(/^(<.*)(\r|\n|\r\n)(>)/, '\1\2---\2\3')
print diffout

上記スクリプトを~/bin/git-diff-normal-formatとして保存し、~/binにPATHを通しておきます。そしたら、.vim設定ファイルに次の記述を追加します。

" diffのコマンド
set diffexpr=MyDiff()
function MyDiff()
  let opt = ""
  if &diffopt =~ "iwhite"
    let opt = opt . "-b "
  endif
  silent execute "!git-diff-normal-format " . opt . v:fname_in . " " . v:fname_new . " > " . v:fname_out
  redraw!
endfunction

最後に、git diffで使うアルゴリズムを指定します。下記記述ではgit difftoolで起動するコマンドにvimdiffを指定する設定も一緒に行っています。

[diff]
  tool = vimdiff
  algorithm = histogram

これでvimdiffやgit difftoolを実行した時にhistogramアルゴリズムが使えるようになります(patienceを使いたい場合は、上記設定のalgorithmの値をpatienceに変えてください)。

では具体的な効果を見てみましょう。サンプルとして用意した2つの比較対象のファイルをvimでウィンドウを分割して並べたものが下の画像です(クリックで拡大できます)。


まずは、今回の設定をする前、デフォルトのアルゴリズムでvimdiffによる差分をとった結果が以下です(クリックで拡大できます)。


そして、今回の設定をした後の、histogramアルゴリズムでvimdiffによる差分をとった結果が以下です(クリックで拡大できます)。


こちらのほうが、より差分内容も分かりやすく、かつ実際に手で行った変更の操作に近いものになっていますね。
※ちなみにこのvimdiffの色設定は過去に書いたこの記事を参照。

※2014/07/10更新:diffupdateを実行した際に画面の描画がおかしくなることがあったので、回避のためMydiff()にredraw!を追加

※参考記事
Making your Git Diff/Merge More Useful | The Interim Developer
Alfedenzo - Patience Diff, a brief summary
Bram Cohen's Journal - Patience Diff Advantages
HistogramDiff (JGit - Core 2.0.0.201206130900-r API)

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年6月17日火曜日

Ruby/LDAP 経由で LDAPS 接続

Rubyから手軽にLDAP接続するのに使えるRuby/LDAPライブラリですが、LDAPS接続をする方法について、あまり実際の例がないように思います。今回、実際にやってみたので、ポイントを書いておこうと思います。

ポイントは3つ。
  • LDAP::Connの代わりにLDAP::SSLConnを使うこと。
  • 接続先ポート番号を明示的に指定すること。
  • LDAPクライアント用設定(/etc/ldap/ldap.conf)で証明書検証設定を正しく設定すること。
以下、やってみた時の経緯です。
今までSSLなしのLDAP接続はできていました。単にLDAP::Conn を LDAP::SSLConn に置き換えればいいと思っていたんですが、エラーになってしまいます。例をirbで示してみますと、こんな感じです。

irb(main):001:0> require 'ldap'
=> true

irb(main):003:0> conn = LDAP::SSLConn.new('localhost')
=> #<LDAP::SSLConn:0x000000021130d0>

irb(main):004:0> conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
=> #<LDAP::SSLConn:0x000000021130d0>

irb(main):006:0> conn.bind(‘cn=hoge,dc=foo,dc=localhost’,’pass')
LDAP::ResultError: Can't contact LDAP server
from (irb):6:in `bind'
from (irb):6
from /usr/bin/irb:12:in `<main>'

よく調べてみると、ポート番号があやしいことに気づきました。公式ドキュメントによると、デフォルト値が定数LDAP_PORTになっているんですが、LDAP::LDAP_PORTはSSLではない接続用のポート番号(389)が入った定数なのです。

実はLDAP::LDAPS_PORTの方がSSL接続用のポート番号(636)が入った定数なんですね。なぜデフォルト値がLDAPS_PORTじゃなくてLDAP_PORTなのかはよく分からないのですが、もしかして、LDAP::SSLConnはLDAP::Connのサブクラスだからかもしれません。※参考→http://ruby-ldap.sourceforge.net/rdoc/classes/LDAP/SSLConn.html

ということで、今度はポート番号にLDAP::LDAPS_PORTを指定して再試行してみます。

irb(main):008:0> conn = LDAP::SSLConn.new('localhost', LDAP::LDAPS_PORT)
=> #<LDAP::SSLConn:0x0000000211c180>

irb(main):009:0> conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
=> #<LDAP::SSLConn:0x0000000211c180>

irb(main):010:0> conn.bind('cn=admin,dc=test,dc=localhost','admin')
LDAP::ResultError: Can't contact LDAP server
from (irb):10:in `bind'
from (irb):10
from /usr/bin/irb:12:in `<main>’

しかし、まだ同じエラーです。slapd側のログを見ると、接続自体は確立されているようです。このようなログが残っています。

conn=1013 fd=13 TLS established tls_ssf=128 ssf=128

このログはポート番号修正前には記録されなかったので一歩前進です。LDAPのSSL接続について更に検索すると、こんな情報が見つかりました。→http://christian.hofstaedtler.name/blog/2009/05/ruby-ldap-ssl.html

要するに/etc/ldap/ldap.confにクライアント設定が原因かもしれないということ。試しに、/etc/ldap/ldap.conf の TLS_CACERT をサーバ側の cn=config で設定している
CA証明書と同じ証明書を指定するようにしたところ、接続に成功しました。

なるほど。サーバ証明書をCA証明書で検証していたわけですね。しかしそもそも、証明書検証が必要なければ省略する方法もあります。※参考→https://wiki.debian.org/LDAP/OpenLDAPSetup#Configuring_LDAPS

/etc/ldap/ldap.conf で TLS_CACERT をコメントアウトし、TLS_REQCERT を never に設定すればいいですね。今回はこちらを採用します。

ということで、irbでもう一度試してみます。

irb(main):001:0> require 'ldap'
=> true

irb(main):002:0> conn = LDAP::SSLConn.new('localhost',LDAP::LDAPS_PORT)
=> #<LDAP::SSLConn:0x0000000166b9e8>

irb(main):003:0> conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
=> #<LDAP::SSLConn:0x0000000166b9e8>

irb(main):004:0> conn.bind('cn=admin,dc=test,dc=localhost','admin')
=> #<LDAP::SSLConn:0x0000000166b9e8>

これで接続に成功することを確認できました。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年6月13日金曜日

RSpecの"-fs"オプションが無効になったことへの対策

Padrinoのプロジェクトにおいてrake 経由でRSpecのテストを実行しようとして、今まで起きていなかったエラーが突然起きました。

`find_formatter': Formatter 's' unknown - maybe you meant 'documentation' or 'progress'?. (ArgumentError)

スタックトレースを見てもソースコードの原因箇所が的確に示されていなかったので少々戸惑いましたが、結局原因はspec/spec.rakeファイルにありました。そこではspecコマンドのオプションとして"-fs —color"が指定されていたんですが、実はこの"-fs"が原因でした。"-fs"は"--format specdoc"の短縮形式で、以前はこのオプションは有効だったが無効になったみたいです。

現在はフォーマットとして指定できるのは"documentation"か"progress"になっていますが、詳細な情報を出力したいので"documentation"を指定することにします。この指定にあわせてspec/spec.rakeファイルを修正しようと思います。

Padrinoが自動生成する修正前のspec/spec.rakeファイルはこんな感じです。

begin
  require 'rspec/core/rake_task'

spec_tasks = Dir['spec/*/'].inject([]) do |result, d|
 result << File.basename(d) unless Dir["#{d}*"].empty?
 result
end

  spec_tasks.each do |folder|
    RSpec::Core::RakeTask.new("spec:#{folder}") do |t|
      t.pattern = "./spec/#{folder}/**/*_spec.rb"
      t.rspec_opts = %w(-fs --color)
    end
  end

  desc "Run complete application spec suite"
  task 'spec' => spec_tasks.map { |f| "spec:#{f}" }
rescue LoadError
  puts "RSpec is not part of this bundle, skip specs."
end

これを以下のように変更します。

begin
  require 'rspec/core/rake_task'

  spec_tasks = Dir['spec/*/'].inject([]) do |result, d|
    result << File.basename(d) unless Dir["#{d}*"].empty?
    result
  end

  spec_tasks.each do |folder|
    desc "Run spec suite under #{folder} directory"
    RSpec::Core::RakeTask.new("spec:#{folder}") do |t|
      t.pattern = "./spec/#{folder}/**/*_spec.rb"
      t.rspec_opts = %w(--format documentation --color)
      t.verbose = false
    end
  end

  desc "Run complete application spec suite"
  RSpec::Core::RakeTask.new("spec") do |t|
    t.pattern = "./spec/**/*_spec.rb"
    t.rspec_opts = %w(--format documentation --color)
    t.verbose = false
  end
rescue LoadError
  puts "RSpec is not part of this bundle, skip specs."
end

変更点としては、-fsオプションをやめて--format documentationにしたこと、
t.verbose = falseを追加したこと、"spec"タスクを"spec:フォルダ"の集合ではなく
新たに全specファイルを含むタスクとして定義したことです。

t.verbose = falseを指定するのは、長ったらしい実行コマンドの出力を省いて出力を見やすくするためです。また、"spec"タスクが"spec:フォルダ"の集合だと、何件成功して何件失敗したか、という結果がフォルダの実行ごとに表示されるので見づらくなるので、対象を全てにした1つのテストスイートにまとめなおしています。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年6月6日金曜日

Capybaraで入力欄・ボタン・リンクをCSSセレクタによって指定する

CapybaraはWebアプリケーションをテストする際に便利なライブラリです。ところで、フォームに入力するメソッドとしてはfill_inがありますが、要素の指定にCSSセレクタが使えません。click_button, click_linkも同様にCSSセレクタが使えません。ではXPathは使えるかというと、XPathも使えません。指定できるのは単なる文字列で、例えばfill_inの場合、その文字列がid属性またはname属性または対応するlabel要素の文字列に合致するものを探すことになっています。他のfindなどの要素指定メソッドではCSSセレクタを使うので、他のメソッドとの統一性がなくて分かりにくいし、細かい指定もできないので、CSSセレクタを使いたいところです。

単純な解決策としては、findメソッドで要素を絞り込んでsetやclickすればいいわけですが、それだけだともとの機能と同一にはになりません。というのは、fill_in, click_button, click_linkの特長として、それぞれのタイプに合致する要素だけを探してくれるのです。例えばfill_inならば入力可能フィールドのみを検索対象としてくれます。

これは、Capybaraのこのソースコードを見るとよくわかります。

例えばfill_inの場合、"fillable_field"という条件で探してsetしています。これと同じことをできればいいわけです。条件の詳細はXPathライブラリのこのソースコードを見るとわかります。

これによるとfill_inに使われる"fillable_field"の条件は、

  • input要素あるいはtextarea要素であり、
  • type属性が次のいずれでも「ない」:'submit', 'image', 'radio', 'checkbox', 'hidden', ‘file'

click_buttonに使われる"button"の条件は、

  • input要素かつ、type属性が次のいずれかである:'submit', 'reset', 'image', ‘button'
  • または、button要素である

click_linkに使われる"link"の条件は、

  • a要素かつ、href属性が存在する

となっています。これらの条件を満たすように、CSSセレクタに条件を付加してXPathに変換するメソッドを作ってみました。

# CSSセレクタをXPathに変換し、さらに入力可能フィールドの条件を付加する
# @param [String] css CSSセレクタ
# @return [String] 変換後のXPath
def convert_css_to_fillable_xpath(css)
  base_xpath = %(#{Nokogiri::CSS.xpath_for(css)[0]})
  # init_xpath = %(#{base_xpath}[../input or ../textarea])
  # new_xpath = ['submit', 'image', 'radio', 'checkbox', 'hidden', 'file'].inject(init_xpath) do |tmp_xpath, attr|
  #   %(#{tmp_xpath}[not(@type='#{attr}')])
  # end
  # new_xpath
  # 高速化のためベタ書き
  "#{base_xpath}[../input or ../textarea][not(@type='submit')][not(@type='image')][not(@type='radio')][not(@type='checkbox')][not(@type='hidden')][not(@type='file')]"
end

# CSSセレクタをXPathに変換し、さらにボタンの条件を付加する
# @param [String] css CSSセレクタ
# @return [String] 変換後のXPath
def convert_css_to_button_xpath(css)
  base_xpath = %(#{Nokogiri::CSS.xpath_for(css)[0]})
  # xpath_input = %(#{base_xpath}[../input][@type='#{['submit', 'reset', 'image', 'button'].join("' or @type='")}'])
  # xpath_button = %(#{base_xpath}[../button])
  # "(#{xpath_input} | #{xpath_button})”
  # 高速化のためベタ書き
  "(#{base_xpath}[../input][@type='submit' or @type='reset' or @type='image' or @type='button'] | #{base_xpath}[../button])"
end

# CSSセレクタをXPathに変換し、さらにリンクの条件を付加する
# @param [String] css CSSセレクタ
# @return [String] 変換後のXPath
def convert_css_to_link_xpath(css)
  base_xpath = %(#{Nokogiri::CSS.xpath_for(css)[0]})
  "#{base_xpath}[../a][@href]"
end

少々解説します。Nokogiri::CSS.xpath_for は、複数条件CSSに対応するため、返り値はXPath文字列を要素に持つ配列になります。入力欄を選ぶという状況的に、1つのみになると考えられるので最初の要素を選択すればよいから、Nokogiri::CSS.xpath_for(css)[0]としています。

変換したXPathの後ろに例えば [../input or ../textarea] といった要素指定を付加しています。これは、要素自身がinput要素あるいはtextarea要素であることを後から無理やり指定するためです。スマートではない形式ですが、CSSセレクタを変換して条件を付加するという状況なので仕方ありません。

最初はメソッド中のコメントアウトしてある部分のようにかなり動的にXPathを生成していましたが、テスト実行を少しでも早くするために、文字列をベタ書きしておくことにしました。

さて、使い方としてはCSSセレクタを渡してXPathに変換し、その条件でノードを取得して動作を行えばよいです。Cucumberによる例を挙げると、こんな感じでステップ定義に使えます。何やら余計なものが色々付いているように見えますが、その辺の詳細は以前に書いたこの記事を参照。

もし(/^".*?\((.*?)\)" 欄に "(.*?)" と入力(?:し(、)|した)(\(非同期\))?( debug)?$/) do |t_css, t_with, t_eos, t_async, debug|
  show_debug_info if debug
  xpath = convert_css_to_fillable_xpath(t_css)
  page.find(:xpath, xpath, :visible => true, :wait => t_async ? async_time : 0).set(t_with)
  puts '' if t_eos.blank?
end

もし(/^".*?\((.*?)\)" ボタンを押下(?:し(、)|した)(\(非同期\))?( debug)?$/) do |t_css, t_eos, t_async, debug|
  show_debug_info if debug
  xpath = convert_css_to_button_xpath(t_css)
  page.find(:xpath, xpath, :visible => true, :wait => t_async ? async_time : 0).click
  puts '' if t_eos.blank?
end

もし(/^".*?\((.*?)\)" リンクを押下(?:し(、)|した)(\(非同期\))?( debug)?$/) do |t_css, t_eos, t_async, debug|
  show_debug_info if debug
  xpath = convert_css_to_link_xpath(t_css)
  page.find(:xpath, xpath, :visible => true, :wait => t_async ? async_time : 0).click
  puts '' if t_eos.blank?
end

Gerkinによるfeature定義としては、例えば

もし "アカウント名(#account_name)" 欄に “foo" と入力し、
かつ "パスワード(#password)" 欄に “bar" と入力し、
かつ "ログイン(#login-button)" ボタンを押下した
ならば・・・(以下略)

のようになります。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年6月5日木曜日

Sinatra・PadrinoでセッションIDを変更する

Webアプリケーションでは例えばログインやログアウトしたタイミングなど、セッションハイジャックのリスクを下げるためにセッションIDを変更したほうがよい場面があります。

PHPですと、言語に組み込みでsession_regenerate_idという関数が用意されています。さすがWebに特化した言語ですね。

で、Rubyでは言語そのものにセッションIDを変更する方法はありません。各フレームワーク依存ですね。ということで、軽量フレームワークとしてよく使われるSinatraやその派生のPadrinoにおいてセッションIDを変更する方法を調べてみました。

ます、セッション内容を破棄して構わないのであれば、

session.destroy

と単にdestoryメソッドを呼んでセッションを破棄するだけです。自動的に新しいセッションIDでセッションが再生成されます。

セッション内容を引き継ぎたい場合は、事前にセッション内容を取得しておき、
新しいセッションの内容としてまるごと置き換えます。

old_session_hash = session.to_hash
session.destroy
session.replace(old_session_hash)

Padrinoの場合、以下の様な感じでヘルパーメソッドにしておくとよいでしょう。メソッド名はphpのマネです。ヘルパーを定義している部分に適当に追加すればいいと思います。

# セッションIDを再生成する
# @return [void]
def session_regenerate_id
  old_session_hash = session.to_hash
  session.destroy
  session.replace(old_session_hash)
end

上記 session_regenerate_id を呼べば、セッションIDが変更されてセッション内容が引き継がれます。

※2014/06/12追記
RackミドルウェアでCSRF対策をしている場合には、セッションIDの変更とともに、セッション内に保持しているCSRFトークンも変更したいと思うでしょう。そういう時は、引き継ぐセッション情報からCSRFトークンの情報を削っておくと、自動的にCSRFトークンを再生成してくれます。ということで修正したコードを載せておきます。

# セッションIDを再生成する
# @return [void]
def session_regenerate_id
  old_session_hash = session.to_hash
  old_session_hash.delete('csrf')
  session.destroy
  session.replace(old_session_hash)
end

上記の例はCSRFトークンをセッションに格納する際のキーが"csrf"である場合です。このキーは状況に応じて変更してください。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年6月4日水曜日

Capybaraでテスト実行中にセッションIDを取得する

テストにCapybaraを使ってるんですが、セッションIDが変化するかどうかをテストしたいと思いまして。ところが、セッションの中身を見る場合は rack_session_access っていうライブラリが使えるんですが、rack_session_accessではセッションID自体を取得する方法は提供してくれません。

Rack::Session::Cookieを使っている場合にはセッションの中身にセッションIDが入っているので、rack_session_access でセッションIDを取得できますが、Rack::Session::Cookie以外によるセッション機能を使っている場合はrack_session_accessではセッションIDが取得できません。

何とかできないものかと思案し、発想を転換しました。Rack::Session::Cookie以外によるセッション機能を使っている場合、ブラウザのCookieにはセッションIDそのものが入っているわけです。つまり、Cookie経由でセッションIDが取得できるはずです。

ということで調べると、Capybaraのドライバを呼び出して、ドライバ内部のCookieを取得するメソッドを呼び出せばいいみたいです。なお、ドライバ内部のCookieを取得するメソッドの呼び出し方法は、ドライバごとに異なります。次の例は、ドライバがPoltergeist の場合の例です。

@session_id =  page.driver.browser.cookies['rack.session'].value


などとすれば取得できます。

なお、セッションIDが変化するかどうかをテストしたい場合はセッションIDを保存しておく必要がありますが、単純にインスタンス変数に入れておけば、後のステップでそのままインスタンス変数として呼び出すことができます。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年6月3日火曜日

RSpecで外部コマンド実行結果を判定する

RSpecによる自動テスト中に、外部コマンドを実行して成功したかどうかを判定したいとします。そういう時は、実行ステータスを取得でき、なおかつ外部コマンドの終了を待ち合わせるような外部コマンドの実行方法を採用すればいいでしょう。

Rubyには外部コマンドの実行方法がいくつか存在しますが、Open3.capture2あたりが適しているように思います。コマンドの標準出力への出力結果はテストで使ったりするため取得したいことが多く、なおかつ標準エラー出力はそのままコンソール上に表示することでテスト中に外部コマンドでエラーが起きた際に原因が分かりやすくなります。

コマンドの実行が成功したかどうかを終了ステータスで判定すれば、エラーが起きた際に分かりやすくなるでしょう。コード例を挙げておきます。

stdout, status = Open3.capture2 %(some_command), :stdin_data => stdin_string
expect(status.exitstatus).to eq(0)

上記コードでは終了ステータスが0、つまりコマンドが成功することを期待しています。コマンドが異常終了して終了ステータスに0以外が返ると、テストは失敗となります。

なお、僕の場合実際には上記コードをRSpecから直接実行するのではなく、Cucumberのステップ定義に組み込んで使っています。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年5月30日金曜日

capybara-webkitからpoltergeist(phantomjs)に乗り換え

RubyによるWebアプリケーションの自動化テストに便利なCapybara。JavascriptによるDOM操作も、capybara-webkitドライバを経由してテストすることができます。当初はそれで良かったんですが、とある事情によりXサーバ関連ファイル群を入れないで動かしたい場合があったので、Xサーバ関連ファイル群を必要とする仮想フレームバッファ(xvfb)への依存を避けたくなりました。

Capybaraにはもう1つ、JavascriptによるDOM操作を含むテストを実行できる手段がありまして、それがPoltergeistドライバを経由してPhantomJSを動かす、というものです。PhantomJSは仮想ブラウザみたいなもので(乱暴な説明)、詳しい説明は他サイトに譲ります。CabybaraからPhantomJSを動かすためのドライバがPoltergeistです。

ということで、Capybara-webkitからPoltergeist(経由のPhantomJS)に乗り換えることにしました。

まず、PhantomJSをインストールします。僕の環境としてはDebian7ですが、公式サイトのビルド済みパッケージを取ってきたらそのまま動公式サイトにLinux用のビルド済みバイナリがあるので、入手してください。2014/05/29現在の最新版はver1.9.7であり、
"phantomjs-1.9.7-linux-x86_64.tar.bz2"というリンクからダウンロードできます。

解凍して出来たディレクトリの bin/phantomjs をPATHの通った適当な場所に移動します。

例)$HOME/bin に移動する場合

[user@host]% tar jxvf phantomjs-1.9.7-linux-x86_64.tar.bz2
[user@host]% cp phantomjs-1.9.7-linux-x86_64/bin/phantomjs ~/bin

なお、phantomjsの動作には fontconfig, freetype ライブラリが必要ですが、最近のLinuxデスクトップ環境ならば、ほとんどの場合にはすでに入っているでしょう。

ruby側の対応は巷のサイトに書いてあるとおりでOKです。まずGemfileへPolgergeistの記述を追加します。

gem 'poltergeist'

僕はCucumber経由でcapybaraを使ってますので、features/support/env.rb に必要ライブラリの require と default_driver, javascript_driverの変更を記述します。(Javascirpt関連テストの時だけ使いたい場合は、default_driver側の記述は不要です。)

require 'capybara/poltergeist'
Capybara.default_driver = :poltergeist
Capybara.javascript_driver = :poltergeist

なお、Cucumberのテスト自体は書き換えずに動作しました。まだそんなにテストの量がないせいか、速度の違いは感じられません。

PhantomJSは活発に開発されているようなので、今後も色々と改善が期待できますね。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年5月29日木曜日

Padrinoの起動時のロード順・フック実行について

今回のテーマは実は、Padrinoにおいてconfig/boot.rbにはbefore_loadフックとafter_loadフックの登録を行うコード記述があるけれど、config/boot.rbファイルのトップレベルに直接書くのと before_loadやafter_loadの中に書くのではどう違うのか、という疑問が切っ掛けでした。

Padrinoが生成するconfig/boot.rbは末尾で Padrino.load! を呼び出していますが、実はその中でbefore_loadとafter_loadを呼んでいます。Padino.load!の定義は、ソースコードを調べると次のようになってます。

def load!
  return false if loaded?
  began_at = Time.now
  @_called_from = first_caller
  set_encoding
  set_load_paths(*load_paths)
  Logger.setup!
  require_dependencies("#{root}/config/database.rb")
  Reloader.lock!
  before_load.each(&:call)
  require_dependencies(*dependency_paths)
  after_load.each(&:call)
  logger.devel "Loaded Padrino in #{Time.now - began_at} seconds"
  Thread.current[:padrino_loaded] = true
end

つまりbefore_loadとafter_loadの違いは、Padrinoの各種依存ファイルのrequireの前か後か、という違いです。

上記Padino.load!をよく見てみると、Loggerだけ扱いが別になっています。Logger関連の設定はbefore_loadやafter_loadの中に書いてもそれだけでは有効になりません。config/boot.rbのトップレベルかつPadrino.load! の前に記述するか、手動でLogger.setup!を呼ぶ必要があります。

で、ようやく本題ですが、上記のPadrino.load!を見ながら、実際フレームワーク全体がどう読み込まれるのか調べてみたくなりました。Loggerのログレベルをdevelに設定すると、起動時にロードしたファイルをログに出してくれるようになるので、ロードの順番がわかります。まとめるとPadrinoのロードの主要手順はだいたいこんな感じになっているようです。

  • Loggerの設定
  • config/database.rbのロード
  • before_loadフックの実行
  • lib/の中にあるファイルのロード
  • models/の中にあるファイルのロード
  • (shared/lib/の中にあるファイルのロード)※デフォルトでは存在しない
  • (shared/models/の中にあるファイルのロード)※デフォルトでは存在しない
  • config/apps.rbのロード
  • app/app.rbのロード
  • アプリケーション名::Appのインスタンス生成
  • after_loadフックの実行
  • app/controllers/の中にあるファイルのロード
  • app/helpers/の中にあるファイルのロード

なるほど。ちなみに意外だったのは、controllersとhelpersのロード順が予想と逆だったこと。でもどうせメソッド定義なので特にロード順に影響はないはずです。

ロードの順番に影響を受けるようなコードの書き方をすることは少ない(というかなるべく避けるべき)と思いますが、デバッグの役に立つこともありますので一応知っておくとよいと思われます。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年5月28日水曜日

Padrinoのhelperで特定インスタンスを使いまわす

Padrinoではコントローラやビューから呼び出すことのできるメソッドをhelperメソッドとして定義することができます。

helperメソッドはリクエスト中に何度も呼ばれたりするので、例えばサーバへの接続を行う場合、何度も接続インスタンスをnewせずに使いまわしたい場合がありますよね。

実はある特定のインスタンスを使いまわす方法は単純で、helperメソッド内でインスタンス変数に入れておけばいいです。例えば、LDAPサーバへの接続を取得するヘルパーメソッドを書くとしたら、

MyApplication::App.helpers do
  def ldap_conn
    @ldap_conn ||= LDAP::Conn.new('localhost')
  end
end

みたいな感じになりますね。見ての通り、@ldap_connが未定義の場合は新規接続を作成してそのインスタンスを@ldap_connに代入するとともに返し、すでに@ldap_connが定義されていれば@ldap_connの中身を返します。

ちなみにこのインスタンス変数@ldap_connはどのインスタンスに属するかというと、MyApplication::Appのインスタンスに属することになります。これは get や post メソッド内で定義したインスタンス変数と同じ扱いです。テンプレートからも参照できることからわかるように、これらのインスタンス変数は同じリクエスト中は直接呼んで使いまわすことが出来ます。

今回はLDAP接続を例にしましたが、DBサーバやキャッシュサーバなどの各種サーバへの接続など生成処理の重いインスタンス全般に使えると思います。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年5月27日火曜日

Redis中のRackセッションのデータを直接見る

RubyのフレームワークPadrinoで開発しているアプリケーションのセッション管理をRack::Session::Redisでやることにしたわけですけれども、検証作業中にRedisに保存したセッションのデータをRack::Session::Redisを介さずに直接見てみたくなりました。

というわけで、Redisに格納されているセッションのデータを直接見る方法です。
セッションIDが分かっていることが条件です。RackアプリケーションでのセッションID取得方法はこちらを参照。
irbからRedisクラスを直接叩いて見てみることにします(以下、見やすいように改行を加えてあります)。

[user@host]% bundle exec irb
irb(main):001:0> require 'redis'
=> true

irb(main):002:0> redis = Redis.new(:url => 'redis://127.0.0.1:6379/0')
=> #<Redis client v3.0.7 for redis://127.0.0.1:6379/0>

irb(main):003:0> Marshal.load(redis.get('rack:session:セッションID'))
=> セッションのデータ

という感じで直接見ることができます。

Rackアプリケーションのセッションデータは、デフォルト設定では rack:session という名前空間の下に収められています。この名前空間は変更可能ですがあまり変更することはないでしょう。

データはMarshal.#dumpでシリアライズされているようで、読みだす時はMarshal.#loadを使えばよいです。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年5月24日土曜日

やっぱりセッション管理はRack::Session::Redisで

昨日Rack::Session::Dalliを使うって言った舌の根も乾かぬうちに別のことを言い始めたよコイツ、とのツッコミを受けそうです。すいません。いや実は職場の人にRack::Session::Redisってのもあるよ、とアドバイスを頂きまして、調べてみたらRedisが結構良さそうだし面白そうなのでRedis使いたくなってしまったのですよ。

ちなみに、Rack::Session::Redisについて調べてみると、このクラスを提供しているライブラリが2つ見つかります。rack-session-redis と redis-rack の2つですね。

redis-rackのほうが新しいです。ソースコードも読んでみましたが、redis-rackのほうがよく整理されている印象を受けました。redisにデータを保存するための抽象化レイヤが redis-store として分離されていて、redis-rackはその上に乗っている構造のようです。

ということで、redis-rackを導入します。以下、Padrinoフレームワークを使っている前提です。

Gemfileに以下の記述を加えます。

gem ‘redis-rack’

app/app.rbに以下の記述を加えます。

set :protection, false
set :protect_from_csrf, false
disable :sessions
use Rack::Session::Redis, :expire_after => 30 * 24 * 60 * 60
use Rack::Protection
use Rack::Protection::AuthenticityToken, :authenticity_param => '_csrf_token'

セッションに使うライブラリを変更するため disable :sessions とするのですが、:protection と :protect_from_csrf は enable:sessions を前提にしておりエラーになるため事前に falseに設定し、その後手動で該当するミドルウェアである Rack::Protection と Rack::Protection::AuthenticityToken を use します。

なお、Redisの接続先はRack::Session::Redisのデフォルトのオプションで

 :redis_server => 'redis://127.0.0.1:6379/0/rack:session'

と定義されてます。特に変更の必要はないので今回はサーバを指定していません。オプションはセッションの有効期限だけを指定しています(上の例だと30日を秒数で指定)。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年5月23日金曜日

セッション管理は Rack::Session::Dalli で

昨日書いたように、Rack::Session::Pool使用中にセッションが破棄される という問題が発生してまして、その打開策として最初 Rack::Session::Memcache への移行を検討してました。

Rack::Session::Memcache はバックエンドに memcache-client パッケージを使うのですが、実は memcache-client のメンテを担当している開発者によって memcache-client の後継である dalli というパッケージが出ていました。

ここで、memcache-client と dalli という2つのパッケージが登場したので、他にもあるかと一応調べたら memdached という名前のパッケージもありますね(直球な名前だな)。これはCで書かれたAPIをRubyから呼ぶもので、速さだけで言ったら最速みたいですけれども、Rackに組み込む簡単な方法はパッと見つかりませんでした。やるとしたらRack::Session::Memcache を書き換えるのが一番現実的でしょうけど、あまりやりたくありません。

一方、dalli には Rack::Session::Dalli クラスが付属していて、これを使うと簡単に導入できるみたいでした。これは魅力的。さすがにdalliはPure Ruby実装なので速度面で劣るものの、それでもパフォーマンスは結構良好なようですので大丈夫でしょう。今回はそこまで速度重視の目的でもないですし。

ということで、 Rack::Session::Dalli を導入します。Padrinoフレームワークを前提に説明しますので、適宜読み替えてください。

Gemfileに

gem ‘dalli’

と加えます。そして、config/boot.rb に

require 'rack/session/dalli’

を追記します(Bundler.require では自動的にロードされないためです)。

そして、app/app.rb に

use Rack::Session::Dalli, :expire_after => nil, :memcache_server => '127.0.0.1:11211’

って感じに設定すればOKです。サーバはデフォルトで 'localhost:11211’ になってますが、IPアドレスを打たないときちんと認識しなかったので明示的に '127.0.0.1:11211’ を設定しています。

参考1→ http://stackoverflow.com/questions/12786976/how-to-specify-memcache-server-to-racksessionmemcache/12787492#12787492
参考2 →http://www.banana-systems.com/2010/09/rails3_memcached/

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年5月22日木曜日

Rack::Session::Pool使用中にセッションが破棄される

Rack::Session::Poolを使っていると、突然セッションIDが変わる、という問題にぶちあたりました。しかもセッションIDが変わってもセッションに保存した値は引き継がれず消えるので、どうやらセッションが破棄されているようです。

Cucumberでのテストでは引っかからないので今まで気付きませんでした。ブラウザで実際に手動で操作していて初めて気づいた問題です。普通に動作していると思ったらいきなりセッションが消えてログイン状態が無効になったりします。問題発生のタイミングや間隔もランダムで、原因は、正直よくわかりません。

ネット上を探してみると、Rack::Session::Poolを使用中に似たような症状を報告している人がいました。
http://d.hatena.ne.jp/libera-softvaro+dev/20110324/1300982695

上記URLのページではリクエストごとにCookieの値が変わると書いてあります。Rack::Session::Poolを使う場合Cookieの値はセッションIDなので、同じような症状に思えます。unicornだと発生しないと書いてありますが、僕はunicornを使っており、リクエストごとに変わるということはないものの、しかしランダムなタイミングで突然セッションIDが変わります(セッションが破棄されている)。

一応原因特定のため一時的に Rack::Session::Cookieに戻してみると、やはり症状は起きず、この問題は全く発生しなくなります。

う~ん、Rack::Session::Poolはダメなのか・・・。しかし、Rack::Session::Cookieだとセッションのデータも全部Cookieに保存するからセキュリティ的に避けたいところです。となると、やっぱ、Rack::Session::Memcacheしかない気がします。ということで、memcachedの導入を本格的に考えますかね。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年5月21日水曜日

Rubyのコメント形式とYARD

PHPの時はPhpDoc形式のコメントをIDEが読み取って連携してくれるという事情もあり、ソースコードにはPhpDoc形式のコメントをつけるようにしてました。このPhpDocでは様々なタグを付けることができて、例えば関数の引数は@paramタグで指定できたりします。

ではRubyの場合はどうかと思って、今さらながら調べてみました。Rubyの場合、ソースコード内に記述するコメントのパーサとしてはRDocが代表的みたいですね。

調べてみると、RDocのコメントには、特に関数の引数や引数の型を指定する形式とか、そういうのはないみたいです。ちょっとイメージが湧かないので、有名なライブラリのソースコードをあたってみました。

ActiveRecordの場合、クラスやモジュールの先頭にガッツリ長文で説明が例も交えて解説してあります。

Rackの場合、ドバっと書いてあるファイルと、全然書いてないファイルの差が激しいです。まあ、必要なとこだけ絞って書いてあるんでしょう。

正直、決まりとか慣例とかあまり無いように思いました。うーん、自由に書けばいいのかなぁ。

などと思っていたら、どうやら、PhpDocに似たYARDってのがあるらしいじゃないですか。
※詳細はリンク先参照。
参考1 http://d.hatena.ne.jp/kitamomonga/20110825/ruby_yardoc_tag
参考2 http://morizyun.github.io/blog/yard-rails-ruby-gem-document-html/

そうそう、こういうのを探してたわけですよ。これならPhpDocと同じような感覚で書けますね。YARD自体の解説は上記リンク先に素晴らしい解説があるのでここではしませんが、やっぱりソースコードにコメントを入れるものとしては引数と戻り値を指定するタグくらいはあって欲しいわけで、そういう意味ではYARDはバッチリですな。

というわけで、今後YARD形式でコメントを書いていこうと思います。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年5月20日火曜日

Padrinoで全コントローラ共通の処理を書く

SinatraベースのフレームワークPadrinoはMVC構造を用意してくれるので処理を分かりやすく書くことができます。コントローラは自動的に雛形が用意されるので、各コントローラに関する処理はその雛形を編集すればよく、編集すべきファイルが分かりやすいです。

では、例えばログインを必要とするシステムにおけるページ表示前の認証の確認処理など、全コントローラに共通の処理はどこにどのように書けばよいでしょうか。

Padrinoのプロジェクトルートから見て、app/app.rbに書けばよいと思います。この中のAppクラスの宣言文の中に、before〜endを使って書けば全コントローラの処理前に実行される処理が書けます。

例えば上で挙げたログイン判断の場合ですと、

before do
  if /^\/login/ =~ request.path_info
    # ログインコントローラの場合、ログイン済みならトップ画面へリダイレクト
    redirect url_for(:top, :index) if logged_in?
  else
    # ログインコントローラ以外の場合、未ログインならログイン画面へリダイレクト
    redirect url_for(:login, :index) unless logged_in?
  end
end

のような感じで書けますね。上記のようにURLによって処理を分けて書くこともできます。また、ヘルパーメソッドも使えます。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年5月17日土曜日

Cucumber(+Capybara)ステップ定義の実用的サンプル

現在のプロジェクトでは初めてBDDを採用して開発してます。具体的にはCucumberを使ってステップ定義を書いています。

Cucumberについて調べると、確かにサンプルとしてGherkinによるステップ定義の例は色々出てくるんですけれども、日本語によるステップ定義で実用的というかそのまま実務に使えるサンプルってネット上に出ているものが少ない気がします。実務的じゃないものは沢山見つかるんですけどね。

ということで、BDDはまだ初心者といえる私ですけれども、実務ですぐに使える日本語で書いたGherkinによるステップ定義のサンプルを晒してみたいと思います。なお、具体的なテストコードの記述にはCapybaraを利用しています。

ちょっと長いですが、実用的なサンプルということでご理解ください。ではどうぞ。

もし(/^".*?\((.*?)\)" 画面にアクセス(?:し(、)|した)( debug)?$/) do |t_path, t_eos, debug|
  show_debug_info if debug
  visit t_path
  puts '' if t_eos.blank?
end

もし(/^HTTPステータスコードが "(\d{3})" で(?:あり(、)|ある)( debug)?$/) do |t_code, t_eos, debug|
  show_debug_info if debug
  expect(page.status_code).to eq(t_code.to_i)
  puts '' if t_eos.blank?
end

もし(/^".*?\((.*?)\)" 欄に "(.*?)" と入力(?:し(、)|した)( debug)?$/) do |t_locator, t_with, t_eos, debug|
  show_debug_info if debug
  page.fill_in(t_locator, :with => t_with)
  puts '' if t_eos.blank?
end

もし(/^".*?\((.*?)\)" ボタンを押下(?:し(、)|した)( debug)?$/) do |t_locator, t_eos, debug|
  show_debug_info if debug
  page.click_button(t_locator)
  puts '' if t_eos.blank?
end

ならば(/^ページタイトルが "(.*?)" で(?:あり(、)|あること)( debug)?$/) do |t_title, t_eos, debug|
  show_debug_info if debug
  expect(page.title).to eq(t_title)
  puts '' if t_eos.blank?
end

ならば(/^".*?\((.*?)\)" が存在(?:し(、)|すること)(\(非同期\))?( debug)?$/) do |t_css, t_eos, t_async, debug|
  show_debug_info if debug
  expect(page).to have_css(t_css, :wait => t_async ? async_time : 0)
  puts '' if t_eos.blank?
end

ならば(/^".*?\((.*?)\)" が存在(?:せず(、)|しないこと)(\(非同期\))?( debug)?$/) do |t_css, t_eos, t_async, debug|
  show_debug_info if debug
  expect(page).to have_no_css(t_css, :wait => t_async ? async_time : 0)
  puts '' if t_eos.blank?
end

ならば(/^".*?\((.*?)\)" 画面に遷移(?:し(、)|すること)( debug)?$/) do |t_path, t_eos, debug|
  show_debug_info if debug
  expect(page.current_path).to eq(t_path)
  puts '' if t_eos.blank?
end

ならば(/^".*?\((.*?)\)" に "(.*?)" と表示(?:され(、)|されていること)(\(非同期\))?( debug)?$/) do |t_css, t_text, t_eos, t_async, debug|
  show_debug_info if debug
  target_node = page.find(t_css, :text => t_text, :visible => true, :wait => t_async ? async_time : 0)
  expect(target_node.text).to eq(t_text)
  puts '' if t_eos.blank?
end

ならば(/^".*?\((.*?)\)" に "(.*?)" と表示(?:されず(、)|されていないこと)(\(非同期\))?( debug)?$/) do |t_css, t_text, t_eos, t_async, debug|
  show_debug_info if debug
  target_node = page.find(t_css, :text => t_text, :visible => true, :wait => t_async ? async_time : 0)
  expect(target_node.text).not_to eq(t_text)
  puts '' if t_eos.blank?
end

def async_time
  Capybara.default_wait_time
end

def show_debug_info
  puts 'Respnse Headers: ' + page.response_headers.to_s
  puts 'Status Code: ' + page.status_code.to_s
  puts 'Current Url: ' + page.current_url
  puts "HTML:\n" + page.html
end

以上になります。いくつかポイントを解説します。

URLやボタンや入力欄などの条件はカッコ内に記述するようにしているものがあります。例えば画面へのアクセスは "画面名(URL)" という記述です。他にも "ボタン名(HTMLのID)" とか、 "探したい要素の呼び名(CSSセレクタ)" とかですね。これはテストの見た目のわかりやすさと同時に、ステップ定義を使いまわせることを実現するためです。

各ステップは末尾に " debug" (半角スペースの後ろにdebug) とつけると、そのステップ実行直前のレスポンスについて、レスポンスヘッダ、ステータスコード、URL、HTMLコードをデバッグ情報として出力します。テストをしながら失敗したステップの時の状態が知りたいことってあると思うので、こうしてステップ定義にデバッグ情報の出力オプションを付けておけば便利ですよね。

あと、末尾に "(非同期)" とつける事のできるステップもあります。これは、Capybaraの機能で、例えばレスポンス中に指定した要素が存在するか確認するコードの場合、見つからなくても非同期処理の可能性を考慮して一定時間待ってから再度探してくれる、という機能を切り替えるためのものです。Capybaraではデフォルトで毎回2秒待ってくれるのですが、非同期を想定していない場合、この待ち時間は無駄です。テスト駆動をする場合はテスト失敗を頻繁に起こすのでなおさら無駄な待ち時間は無くしたいです。ということで、末尾に "(非同期)" と付けない場合はCapybaraの待ち時間を0秒に設定することで、無駄な待ち時間が発生しないようにしています。

あと、細かい点になりますが、ステップ定義文の最後の語尾に読点(、)をつける書き方とそうでない書き方の2通りできるようにしてまして、後者(読点なし)の場合には、次の行に空行を出力するようにしています。これは、Cucumberの実行時に、ステップ定義のソースコード上での空白行を無視して出力してしまって見辛いので、強制的に空行を再現させる目的で付けています。言葉で説明するとわかりにくいと思いますが実行結果を見れば一発でわかると思います。こんな風になります。


空行が出力されるので、結果がぐっと見やすくなりますね。

もしかしたら今後開発を進めるうちにまた色々変わってきたらアップデート版を書くかもしれませんし、書かないかもしれませんが、参考になれば幸いです。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年5月15日木曜日

ruby-ldapでエントリを再帰的に削除する

※途中、試行錯誤の過程でボツにしたコードも載せてるので、結論だけ見たい方は途中の過程をすっ飛ばして下の方をご覧ください。

ruby-ldapを使ってrubyからLDAPの操作をしている時に子エントリを持つエントリを削除しようとするとエラーが発生し、"Operation not allowed on non-leaf” と言われます。

ruby-ldapの場合は特にエントリを再帰的に削除してくれるような便利なメソッドなんか用意されていません。ですので、先に全ての子エントリを削除するコードを自力で実装しないといけません。

最初に思いついたのは、エラーの発生を捕捉し、子エントリがあって処理に失敗した場合には各子エントリに対して再帰処理をかける方法です。

ということで最初に書いたコードがこちら。

# 指定DNを子も含めて再帰的に削除する
# ldap_connはLDAP接続を返すメソッドとして別途定義
def ldap_delete_r(dn)
  begin
    ldap_conn.delete(dn)
  rescue LDAP::ResultError
    if ldap_conn.err == 66 # "Operation not allowed on non-leaf"
      ldap_conn.search(dn,  LDAP::LDAP_SCOPE_ONELEVEL, '(objectclass=*)', ['dn']) do |entry|
        ldap_delete_r(entry.get_dn)
      end
    else
      raise
    end
  end
end

これで一応望み通りに動作しますが、ちょっとスマートではありません。
rubyに限らず一般的に例外処理のためのbegin〜rescue〜end節のような構文は処理が重いので、できれば例外捕捉をしなくて済む方法が望ましいです。
さらに、削除試行して失敗してから子エントリを検索していますが、そもそも子エントリを先に検索すればそ無駄に失敗する削除処理をしなくても済むはずです。

よって、削除処理の前に子エントリを検索しに行き、ヒットした各子エントリに対して再帰処理をかければよさそうです。子エントリがヒットしなければそのままエントリを削除します。

ということで修正したコードがこちら。

# 指定DNを子も含めて再帰的に削除する
# ldap_connはLDAP接続を返すメソッドとして別途定義
def ldap_delete_r(dn)
  ldap_conn.search(dn, LDAP::LDAP_SCOPE_ONELEVEL, '(objectclass=*)', ['dn']) do |entry|
    ldap_delete_r(entry.get_dn)
  end
  ldap_conn.delete(dn)
end

この方がスマートですね。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年5月14日水曜日

RackアプリケーションでセッションIDを取得する

Webアプリの開発をしていると、現在のセッションIDを取得したくなることがあります。PHPですとsession_idというその名もズバリの関数が言語標準で用意されてたりしますが、Rubyだと言語標準にはないです。そもそもPHPと違ってセッションの仕組み自体言語標準ではなく、よく使われるのはRack::Session系のライブラリです。これは以前紹介したように幾つか種類があります。

しかしRack::Session系のライブラリで、現在のセッションID取得をどうやるかについてはドキュメントに分かりやすく書いておらず、探しにくかったです。(Rack::Session::Cookieの場合のみ、セッションのデータ自体にセッションIDが格納されるようですが、それ以外ではIDは入りません。)結局ソースコード読んで見つけました。結論から先に言いますと、

env['rack.session'].id

で取得できます。SinatraやPadrinoを使っている場合はもっと簡潔に

session.id

でOKです。(Sinatraにおけるsessionメソッドはrequest.sessionを呼びだします。Rack::Request#sessionは@env['rack.session']を返します。Sinatraにおけるenvメソッドはインスタンス変数@envへのアクセサですので、結局同じことになります。)

なお、今回(メインで)参照したソースコードはこれ↓。
https://github.com/rack/rack/blob/master/lib/rack/session/abstract/id.rb

上記URLにはRack::Session::Abstract::SessionHashとRack::Session::Abstract::IDの定義が書かれています。

ここにセッションIDを取得していそうなメソッドを発見。Rack::Session::Abstract::ID#current_session_id です。こいつを定義している部分で、

def current_session_id(env)
  env[ENV_SESSION_KEY].id
end

とやってますね。ちなみにRack::Session::Abstractモジュールの冒頭の方で

ENV_SESSION_KEY = 'rack.session’.freeze

と定義されてます。よって、env[‘rack.session’].idを呼べばセッションIDが取得できるとわかります。

なお、env[‘rack.session’]の中身は何かというと、Rack::Session::Abstract::ID#prepare_session で

env[ENV_SESSION_KEY] = session_class.new(self, env)

としてまして、ここでsession_classが何かといえば、Rack::Session::Abstract::ID#session_class の定義が

def session_class
  SessionHash
end

となってます。要するに、env[‘rack.session’]に入っているのはRack::Session::Abstract::SessionHashのインスタンスなのでした。ちなみに、アプリケーション中で p env[‘rack.session’] などとするとハッシュに見える結果が返ってきますが、これは inspect メソッドが定義されているからです。

def inspect
  if loaded?
    @data.inspect
  else
    "#<#{self.class}:0x#{self.object_id.to_s(16)} not yet loaded>"
  end
end

インスタンス変数@data はハッシュですので、 inspect した結果がハッシュになるのですね。(Kernel.#p は内部的にObject#inspect を呼びます。)

ついでに、Rack::Session::Abstract::SessionHash#id を見てみますと

def id
  return @id if @loaded or instance_variable_defined?(:@id)
  @id = @store.send(:extract_session_id, @env)
end

となってまして、セッションIDはRack::Session::Abstract::SessionHashのインスタンス変数@idに保持されていることがわかります。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

※2014/05/22更新:Rack::Session::Cookieの場合のみ、セッションのデータ自体にセッションIDが格納される点について追記。
※2014/06/04更新:SinatraやPadrinoの場合のより簡潔な書き方を追記。