ActiveRecord::Errors の Representation

リファクタリングに際して、REST のリソースプロバイダーを実装している部分にある程度の規約を設けたいなー、と思って設計中。


以下はだいたい決まっている。

  • RESTful なコントローラーの実装は、ResourceController を継承する
  • respond_to_resource(@resource) で XML, JSON, の表現を返すようにするために、@resouce は to_xml, to_json を実装しているべきである。(to_hash を実装していれば、XML/JSONには簡単に変換できる)
  • HTMLは、"現時点では"なるべく返さない設計が望ましい*1
  • ショートカット/コードを読みやすくするために、respond_to_ok(@resource) / 200 OK, respond_to_error(@resource) / 400 Bad Request を利用してリソースを返す。

で、次に、返すリソースの表現に関して、2XX で返す場合の8割はActiveRecordかCouchResourceなので、to_xml なり to_json なりをそのまま呼び出せばよい。

で、クライアントのパラメーターが原因でエラーを起こす場合、400を返すのはよいとして、そのときに返すデータをどうすればよいのか、という問題と格闘中。大前提としてRails式のパラメーター渡を前提にしたとしても、正直これだ!という解は見つかっていない。

Rails から渡されるデータは、www-form-urlencoded であれ、XMLであれ、JSONであれ、とにもかくにもHash(Key,Valueのペア)である。以下三つは、同じように解釈される。

account[login_name]=foo&account[password]=bar
<account>
   <login_name>foo</login_name>
   <password>bar</password>
</account>
{ 
   "account": {
      "login_name" : "foo"
      "password"   : "bar"
   }
}

受け取り側のコントローラーでは

@account = Account.new
@account.login_name = params[:account][:login_name]
@account.password   = params[:account][:password]

な具合で*2

こんな感じで、リクエストを受け付けるときは何ら問題なく、自然に実装可能。問題は、save メソッドが false を返すとき。ActiveRecord::Errors.to_xml は正直困る。

render :text => @account.errors.to_xml, :status => 400

これで、例えば、ログイン名の長さが不足していると、

<errors>
  <error>Login name must be longer than 4</error>
</errors>

もうね、どのパラメーターがエラーなのかの情報が(プログラムとして)欠落していて、目視で確認するしかない。

<errors>
  <error attr="login_name">Login name must be longer than 4  chars.</error>
</errors>

これならOK? しかし、これは2つ問題あり。

  • こっちは account[login_name]=foo で渡しているんだ。account が欠落しているよ。me[login_name]=foo&you[login_name]=bar で渡すとき、どっちがエラーかわかんないでしょ。
  • XMLだろうが、JSONだろうが、どう考えても、(Key,Value)ペアで処理するのが楽なんだから、属性はなるべく使わない方が吉、というかXMLは要らない仔。

ということで現在、以下のようになっている。

<errors>
  <error>
     <param>account</param>
     <attr>login_name</attr>
     <message>Login name must be longer than 4 chars.</message>
  </error>
</errors>

これだと、ネストが深くなるケース(ActiveRecord では所詮行のマップなのであり得ないけれど、CouchDBでは深いところにエラーが発生しうる)に対応できなさそうなので、以下のように変更するのがよいかどうか、迷っている。

<errors>
  <account>
     <login_name>Login name must be longer than 4 chars.</login_name>
  </account>
</errors>

と、ここまで書いて、あ、これいいかもしれない、と思った次第。親がerrorsであるオブジェクトの値はすべてエラーを示すものである、と。。。

修正。一つの属性に一つのエラーとは限らないことから以下の方が尚、よい。

<account>
   <login_name>
      <error>Login name must be longer than 4 chars.</error>
   </login_name>
</account>
{ 
   account: {
      login_name : [
         "Login name must be longer than 4 chars."
      ]
   }
}

となると、error_messages_for(*params) と同じで、error_resources_for(*params) というメソッドを作ればよい、という事に気がついて、ActiveRecord::Errors#to_xml に嘆く必要はない、と。。。

*1:HTML に関して、partial content を返すのか、から始まるすべてのコンテンツを返すのか、どのレイアウトを適用するのか、などの部分はContent-Negotiationで実装されるべき問題?どっかのブログで見たような気がするが、Content-Negotiation だと X-Layout とか使い、かつ User-Agent なども考慮して(iPhone用)、等々、実装が面倒なので後回し。

*2:Account.new(params[:account]) という書き方は、どのパラメーターが設定されるのかが見えなくなるので嫌い