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 に嘆く必要はない、と。。。