ユーザー入力のバリデーションにstructを利用する
これはClojure Advent Calendar 2017のために書かれた記事です。
とは言っても、そんなたいしたことを書くつもりは無くて、いつも書く記事の延長線上にある比較的実践的で軽いトピックです。本当は11月に書いて公開しようかなと思っていたのを、時期をずらして12月にしたっていうだけなので過度な期待はしないでください。
今日はユーザー入力などをバリデーションする話です。さて、バリデーションと言えばそろそろリリースされそうなClojure1.9の目玉である、clojure.specを思い浮かべる方も多いかと思います。ですが、clojure.specはどちらかと言えばプログラマのためのツールであり、これをユーザー入力をバリデーションしてエラーメッセージを返す用途に利用するのは無理がある、というのが僕の考えです。 特徴として以下の点があげられています。
マクロを使っていない: バリデーターはデータとして定義されている
バリデーターが相互に依存できる: 検証後のデータにアクセスできる
強制変換: 入力を他の型/値へ変換できる
例外を利用していない: バリデーションプロセスの中で例外を使っていない
圧倒的シンプルさでとても使いやすく作られています。
最初に簡単な使い方を紹介していきます。
code:clojure
(def my-form
{:username s/required
:password s/required})
(let [data {:username "Alice"
:password "foobar"}]
(s/validate data my-form))
(s/validate {:username "Alice"} my-form)
my-formがstructを使ったスキーマの定義になります。そしてrequiredがstructの用意しているバリデーターです。どのようなバリデーターがあるかは以下を参照してください。 validate関数を使うことで、エラーになった項目とその理由、それとエラーにならなかった項目とその値を知ることができます。また上の例だと必須項目の検証しかできていませんが、次の例のようにそれぞれの項目毎に複数の条件を検証することができます。
code:clojure
(def my-form'
{:username [s/required
:password [s/required
(let [data {:username "bob"
:password "foobarbaz"}]
(s/validate data my-form'))
複数のバリデーターを利用したい場合はこのようにベクターで指定し、複数のバリデーターを並べることで表現できます。それとmin-countなどのバリデーターのように引数を取るバリデーターもあり、それらはこのようにベクターでかこう必要があります。
さらにstructでは、フォームをバリデーションする際に利用したくなる値の変換も行なうことができます。以下の例では、文字列を数値に変換しています(integer-str)。
code:clojure
(def my-form''
{:age [s/integer-str
s/positive]})
(s/validate {:age "20"} my-form'')
さらにstructでは検証済みの値を他のバリデーターから参照することができます。次の例ではidentical-toというバリデーターを用いて、他の項目と値が一致するかどうかを検証しています。
code:clojure
(def my-form'''
[[:password
[:confirm-password
(let [data {:password "foobarbaz"
:confirm-password "foobarbax"}]
(s/validate data my-form'''))
(let [data {:password "foobarbaz"
:confirm-password "foobarbaz"}]
(s/validate data my-form'''))
この例では既に紹介したスキーマとは異なり、ベクタを用いてスキーマを記述しています。この例のように他のバリデーターが検証した後の値を利用したい場合は、検証する順序が重要になるためマップではなくベクタを用いて記述したほうがよいです(常にベクタで記述するようにしたほうがよりよい)。
ここまでが簡単なstructの紹介です。
その他の細かい使い方はドキュメントに詳しく書かれているのででそちらを読んでもらうとして、今回はよくある入力フォームをバリデーションしてみたいと思います。
以下のようなフォームをバリデーションすることを考えてみます。
よくあるSign Upフォーム
ユーザー名
文字数の下限と上限が決まってたりする
メールアドレス
パスワード
文字数の下限が決まってたりする
パスワードの確認と称して再入力を促す項目がある
年齢
入力は必須ではなかったりする
非負整数で入力してほしかったりする
居住地
入力は必須ではなかったりする
セレクトボックスで選択式だったりする
これをコードに落しこむと以下のようになります。
code:clojure
(def pattern
{:message "パターンとマッチしません"
:optional true
(and (string? v)
(instance? java.util.regex.Pattern regex)
(some? (re-matches regex v))))})
(def signup-form
[[:username
[:email
[:password
[:confirm-password
[:age
[:hometown
[s/member "関西" "関東" :message "セレクトボックスから選択してください"]]]) 幾つか説明していないものを利用しているので、説明しておきます。まず :message キーワードから。これは見ての通りなんですが、エラー時のメッセージを変更することができます。例えばこんな感じです。
code:clojure
(def x-form
:username s/required)
(def y-form
(s/validate {:username ""} x-form)
(s/validate {:username ""} y-form)
ちなみに既に説明したように、min-countのようなバリデーターは引数を取るのでベクターでかこう必要があると書きましたが、このように:messageを自分で指定したいというケースでもかこう必要があります。
次にpatternという関数が出ているので、それについて説明しましょう。structには標準で様々なバリデーターを提供してくれますが、どうしてもアプリケーションのニーズに合わなかったり、特殊なバリデーションを行ないたいケースが存在します。そのような場合にstructのユーザーは独自にバリデーターを定義することができます(ちなみにメールアドレスのバリデーターはemailとして存在しています)。
code:clojure
(def pattern
{:message "パターンとマッチしません"
:optional true
(and (string? v)
(instance? java.util.regex.Pattern regex)
(some? (re-matches regex v))))})
structの全てのバリデーターは関数ではなく、マップとして定義されています。pattern関数は一番オーソドックスな例になります。:messageはエラーのときのメッセージ、:optionalはrequiredバリデーター以外は基本的にtrueでよいもので、:validateが実際に検証するための関数になります。このとき:validateに定義する関数は例外を送出しないように工夫してあげる必要があります(例外を投げてしまうとstructのやり方から外れてしまい使い勝手が悪くなります/たぶん)。
他にも:coerceや:stateというキーがありますが、そんなに複雑なものでもないので実装を読むか以下のドキュメントを読んでください。
というわけで、Sign Upフォームのスキーマを定義することができました。実際に使ってみるとこんな感じになります。
code:clojure
(let [params {:username "Alice"
:email "alice@acme.com"
:password "foobar"
:confirm-password "foobar"
:age "20"
:hometown "大阪"}]
(s/validate params signup-form))
;; => [{:password "8文字以上で入力してください",
;; :confirm-password "8文字以上で入力してください",
;; :hometown "セレクトボックスから選択してください"}
;; {:username "Alice",
;; :email "alice@acme.com",
;; :age 20}]
そんな感じでユーザー入力を検証するのにstructを利用しましょう、という話でした :)
…で終わることができたら良かったんですが、ちょっとだけ問題がありました(ここまで書いたあとに見つけるっていう)。ネストしたオブジェクトのバリデーションが現状不安定です。なので、以下のようなフォームだとstructを使うと困ることがあるかもしれません。
code:clojure
;; これを検証しようとすると困るかも…
{:name {:first "Tanaka"
:last "Harumaki"}}
まあ、明日紹介する話で、もしかしたらある程度回避できるかもしれませんが、この問題についてはちょっと調査して修正できそうだったら修正するかもしれないですし、無理そうなら他のライブラリを探しているかもしれません。とりあえず、これを読んでいる人は明日の話を読んでほしい!
追記
ちなみにbouncerというstructより前からあるライブラリだと、この問題にぶつからなさそう。どうしてもこの問題にぶつかってしまう場合はそちらを利用した方がいいかも。 宣伝
ところでCybozu Startups, inc.では、Clojureを書きたいエンジニア募集してます。もし、Clojureを書きたくて興味があって、話だけでも聞いてみたいという方はayato-p.iconまでご連絡ください(/icons/twitter.iconのDMあたりで)。カジュアルにお寿司を食べながらお話しましょう :)