RailsでURLのバリデーション

RailsActiveRecordでは、バリデーションはデータベースへの書き出し、Model#saveのときに走るので、次のような場合に困る。

open-uriを使ってフォームで渡されたURLを開き、このURLにアクセスした情報とフォームから入力された情報を使ってレコードを生成したい。ところが、URLとして渡された不正な文字列、例えばローカルファイルシステムディレクトリパスだったりすると、openしようとしてしまって困る。ということで、バリデーションはコントローラに書かざるを得ない、ような気がする。

 class Item < ActiveRecord::Base
   belongs_to :user
+  # let us do the url validation in the contorller
 end
   def create
     @user = User.find(params[:user_id])
     @item = @user.items.create(params[:item])
+
+    if @item.url !~ /^(#{URI::regexp(%w(http https))})$/ then
+      flash[:notice] = "Invalid URL!!"
+      redirect_to hoge_path
+      return
+    end
+
     populate @item
     if @item.save
       flash[:notice] = "Successfully created an item."

RailsのURLのバリデーションには、URI::regexpという正規表現が使える。URLの正規表現を検索する必要すらなくて、巨人の肩っぽい。以下のように、ちょっと必要以上に厳密で冗長な気がするけど、今の場合、正規表現ごとき長くても構わない。

% irb
 > require 'uri'
 => true 
 > URI::regexp("http")
 => /(?=(?-mix:http):)
        ([a-zA-Z][-+.a-zA-Z\d]*):                           (?# 1: scheme)
        (?:
           ((?:[-_.!~*'()a-zA-Z\d;?:@&=+$,]|%[a-fA-F\d]{2})(?:[-_.!~*'()a-zA-Z\d;\/?:@&=+$,\[\]]|%[a-fA-F\d]{2})*)                    (?# 2: opaque)
        |
           (?:(?:
             \/\/(?:
                 (?:(?:((?:[-_.!~*'()a-zA-Z\d;:&=+$,]|%[a-fA-F\d]{2})*)@)?        (?# 3: userinfo)
                   (?:((?:(?:(?:[a-zA-Z\d](?:[-a-zA-Z\d]*[a-zA-Z\d])?)\.)*(?:[a-zA-Z](?:[-a-zA-Z\d]*[a-zA-Z\d])?)\.?|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[(?:(?:[a-fA-F\d]{1,4}:)*(?:[a-fA-F\d]{1,4}|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|(?:(?:[a-fA-F\d]{1,4}:)*[a-fA-F\d]{1,4})?::(?:(?:[a-fA-F\d]{1,4}:)*(?:[a-fA-F\d]{1,4}|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}))?)\]))(?::(\d*))?))? (?# 4: host, 5: port)
               |
                 ((?:[-_.!~*'()a-zA-Z\d$,;:@&=+]|%[a-fA-F\d]{2})+)                 (?# 6: registry)
               )
             |
             (?!\/\/))                           (?# XXX: '\/\/' is the mark for hostport)
             (\/(?:[-_.!~*'()a-zA-Z\d:@&=+$,]|%[a-fA-F\d]{2})*(?:;(?:[-_.!~*'()a-zA-Z\d:@&=+$,]|%[a-fA-F\d]{2})*)*(?:\/(?:[-_.!~*'()a-zA-Z\d:@&=+$,]|%[a-fA-F\d]{2})*(?:;(?:[-_.!~*'()a-zA-Z\d:@&=+$,]|%[a-fA-F\d]{2})*)*)*)?                    (?# 7: path)
           )(?:\?((?:[-_.!~*'()a-zA-Z\d;\/?:@&=+$,\[\]]|%[a-fA-F\d]{2})*))?                 (?# 8: query)
        )
        (?:\#((?:[-_.!~*'()a-zA-Z\d;\/?:@&=+$,\[\]]|%[a-fA-F\d]{2})*))?                  (?# 9: fragment)
      /x 
 >