フロントエンド向けvalidator: favalidの紹介
はじめに
少し前にフロントエンドのjs向けvalidatorフレームワークの favalid というものを作ったので、その紹介記事を書きます。
favalidの特徴
フロントエンドというかjs向けvalidatorといえば joi や yup 、v8nといったようなライブラリがありますが、それらに比べると機能を大幅に削り、とにかく軽量なのと関数型ライクなAPIがfavalidの特徴です。デフォルトのバリデーションメッセージすら入ってないので自分で定義しないとダメです。 ユースケースやコンフィグによりますが、最小構成でwebpackでbundleすると、1kbくらい。機能を色々盛り込んでも~10kbくらいになるかと思います。gzipを効かせれば容量は更に削減されるかと思います。
使い方
ここからは favalid
の使い方を説明します。
インストール
普通に npm
ないしは yarn
で入ります。
$ npm install favalid
基本機能の紹介
READMEにのせている使い方をベースに基本機能を説明します。 *以下に載せている正規表現等はあくまでサンプルです。実際に使用する場合は注意してください。
tester
favalid
のコアは tester
という関数です。 tester
は以下の関数を引数にとる高階関数です。
- 第1引数にvalidation対象の値を受け取り、validation errorが発生していないかどうかのbool値を返す関数
- 第2引数にvalidation対象の値を受け取り、validation error messageの文字列を返す関数 (= この関数を
messager
と呼んでいます)
tester
の返り値はvalidation対象の値を受け取り、 {error: boolean; message: string}
のオブジェクトを返す関数です(= この関数をfavalidの文脈で validator と呼びます)。
第1引数で渡された関数を元に値を検証し、 error=true
であれば第2引数の関数を実行してerror messageを取得します。
以下にシンプルな例を載せます。ES2015以降の文法で記載します。
import { tester } from 'favalid'; const validator = tester((num) => { return num < 10 }, (num) => { return `${num}は10より小さい数字である必要があります。` }); console.log(validator(10)); // <= {error: true, message: '10は10より小さい数字である必要があります。'} console.log(validator(9)); // <= {error: false, message: ''}
combine
tester
だけであればただの使いづらいAPIですが、 favalid
は combine
という関数を提供しています。
combine
は複数の tester
の返り値のvalidatorをまとめてvalidatorを返す関数です。
以下に例を示します。
import { tester, combine } from 'favalid'; const validator1 = tester((num) => { return num < 10 }, (num) => { return `${num}は10より小さい数字である必要があります。` }); const validator2 = tester((num) => { return num > 2 }, (num) => { return `${num}は2より大きい数字である必要があります。` }); const combined = combine(validator1, validator2); console.log(combined(10)) // <= {error: true, message: '10は10より小さい数字である必要があります。'} console.log(combined(1)) // <= {error: true, message: '1は2より大きい数字である必要があります。'}
combine
された validator
は validator
なので、さらに combine
することができます。
// ... 前の続き const validator3 = tester((num) => { return num % 2 === 0; }, (num) => { return `${num}は2で割り切れる必要があります。` }); const combined2 = combine(combined, validator3); console.log(combined2(10)) // <= {error: true, message: '10は10より小さい数字である必要があります。'} console.log(combined2(1)) // <= {error: true, message: '1は2より大きい数字である必要があります。'} console.log(combined2(5)) // <= {error: true, message: '5は2で割り切れる必要があります。'}
このように、 favalid
では tester
を用いて細かい粒度でvalidatorを実装し、 combine
を用いてユースケースに応じたvalidatorを構成していくことが基本となります。
pre-defined validators
すべて自分で tester
を使って実装するのは大変なので、よく使うルールについてはフレームワーク側で提供しています。これらを pre-defined validators
と呼びます。
これらの validator
は自分で作った validator
と組み合わせることも可能です。
これらを使って、 10文字以上100文字以内のemailアドレス のルールを持ったvalidatorを作ります。
import { combine, maxLength, minLength, regexp } from "favalid"; const minLengthMessage = () => 'メールアドレスは10文字以上です。'; const maxLengthMessage = () => 'メールアドレスは100文字以内です。'; const regexpMessage = () => 'メールアドレスのフォーマットが不正です。'; const mailRegexp = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i; const emailValidator = combine(minLength(10, minLengthMessage), maxLength(100, maxLengthMessage), regexp(mailRegexp, regexpMessage, {}));
このようにいくつか実装がeasyになるようなAPIを提供しています。
冒頭にも解説しましたが、validation messageは favalid
では提供していません。なので、全て自分で定義する必要があります。
regexp
のpre-defined validatorの実装を見てみましょう。実装ファイルは これ です。
export default ( regex: RegExp, messager: Messager, { exclude = false }: IRegExpOption ): Validator => { if (exclude) { return tester((target: string) => !regex.test(target), messager); } else { return tester((target: string) => regex.test(target), messager); } };
optionとしてexclude
オプションを取れるようになっており、正規表現にマッチしたのか、してないのかをルールで出し分けることができます。
内部実装としては tester
を用いて実装していますが、これは pre-defined validator
全てに共通する特徴です。
tester
を用いて実装しているので、共通のインターフェイスを保つことができ、全て combine
によりvalidator同士を結合することができます。
pre-defined validator
は ここ に定義されています。必要なものを探していただければと思います。
応用的な使い方
上記の基本機能を踏まえた上で、以下に応用的な使い方を載せていきます。
Object Schema Validation
favalid
の基本機能では値のバリデーションしかサポートしていませんが、objectのvalidationを可能にする shape
という関数を提供しています。
objectを引数にとり、 key
に結びついたvalidatorを用いて値のチェックを行えます。
import { combine, maxLength, minLength, regexp, shape // <= これ } from "favalid"; /** ここからは上のemail validatorと同じ */ const emailMinLengthMessage = () => 'メールアドレスは10文字以上です。'; const emailMaxLengthMessage = () => 'メールアドレスは100文字以内です。'; const emailRegexpMessage = () => 'メールアドレスのフォーマットが不正です。'; const mailRegexp = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i; const emailValidator = combine( minLength(10, emailMinLengthMessage), maxLength(100, emailMaxLengthMessage), regexp(mailRegexp, emailRegexpMessage, {}) ); /** ここまで上と同じ **/ const passMinLengthMessage = () => 'パスワードは8文字以上です。'; const passMaxLengthMessage = () => 'パスワードは16文字以内です。'; const passRegexpMessage = () => 'パスワードは大文字小文字数字記号が含まれていなければいけません。'; const passRegexp = /^(?=.*?[a-z])(?=.*?\d)(?=.*?[!-\/:-@[-`{-~])[!-~]/i; const passwordValidator = combine( minLength(8, passMinLengthMessage), maxLength(16, passMaxLengthMessage), regexp(passRegexp, passRegexpMessage, {}) ); const userValidator = shape({ email: emailValidator, password: passwordValidator, }); /** これが出力される { email: { error: false, message: '' }, password: { error: true, message: 'パスワードは8文字以上です。' } } **/ console.log(userValidator({ email: 'hoge@hogehoge.com', password: '1234', })); /** これが出力される { email: { error: true, message: 'メールアドレスのフォーマットが不正です。' }, password: { error: true, message: 'パスワードは大文字小文字数字記号が含まれていなければいけません。' } } **/ console.log(userValidator({ email: 'hogehogehoge.com', password: 'Hoge12345', }));
ErrorReducerを使う
favalid
のdefaultの挙動は、 combine
された validator
を combine
された順に実行していき、error messageは最初にvalidation errorが発生したときのものを採用しています。
例えば、上記のemailValidatorの場合、 aaa
という文字列を入れたとき、 minLength
のルールと regexp
のルール両方に違反することになるのですが、error messageの値として得られるのは最初に実行される minLength
のルールのものになります。
console.log(emailValidator('aaa')); // { error: true, message: 'メールアドレスは10文字以上です。' } が出力される
この挙動をユーザが任意に変更できます。combineする際に、errorをどう扱うかを定義できる reducer
を渡すことができる combineWithReducer
というAPIがあります。
例えば、messageをすべて配列に格納し、違反したメッセージをすべて取得するような reducer
を定義します。
const reducer = (prevResult, currentResult) => { if (currentResult.error) { prevResult.error = true; prevResult.message.push(currentResult.message); } return prevResult; }; const emailValidatorWithReducer = combineWithReducer( [ minLength(10, emailMinLengthMessage), maxLength(100, emailMaxLengthMessage), regexp(mailRegexp, emailRegexpMessage, {}), ], reducer, {error: false, message: []} ); /* 以下のように表示される { error: true, message: [ 'メールアドレスは10文字以上です。', 'メールアドレスのフォーマットが不正です。' ] } */ console.log(emailValidatorWithReducer('aaa'));
combineWithReducer
の第1引数にvalidatorの配列、第2引数に reducer
、第3引数にerrorの初期値を渡します。
reducer
は第1引数に前回までのvalidationの結果が格納され、第2引数に現在のvalidationの結果が入ってきます。validationの結果を任意に加工して、returnすることができます。
値を複数とるvalidator
パスワードの確認フォームで、前に入力されたパスワードの値と同一かどうかをチェックするvalidatorなど、値を複数とるvalidatorが存在します。 favalid
では tester
の第1引数の関数および第2引数の関数は可変長の値を取ることができます。
tester
および messager
が引数を一つしかとらないように定義されている場合、第2引数以降のinputは無視されます。
以前作った passwordValidator
を再利用します。
const confirmValidator = tester((passwordConfirm, password) => { return passwordConfirm === password }, (passwordConfirm, password) => { // あくまでサンプルです、ユーザが入力したパスワードをそのままメッセージに使うのは絶対にやめましょう! return `${passwordConfirm}は${password}と一致しません。` }); const passwordConfirmValidator = combine(passwordValidator, confirmValidator); console.log(passwordConfirmValidator('123', 'Passw0rd!123')) // { error: true, message: 'パスワードは8文字以上です。' } console.log(passwordConfirmValidator('password!', 'Passw0rd!123')) // { error: true, message: 'パスワードは大文字小文字数字記号が含まれていなければいけません。' } console.log(passwordConfirmValidator('passw0rD!123', 'Passw0rd!123')) // { error: true, message: 'passw0rD!123はPassw0rd!123と一致しません。' }
非同期のvalidation
emailの重複チェックなど、一度サーバサイドに問い合わせてvalidationしたい場合があります。
favalid
のデフォルトは同期の挙動ですが、 async
版の asyncTester
と asyncCombine
と同期のvalidatorを非同期に変換する toAsync
というAPIを提供しています。
先程作ったemailのvalidatorを再利用して、サーバサイドにメールの重複を問い合わせる版のemailのvalidatorを作ってみます。
asyncTester
の第一引数はPromiseを返す関数で、 resolve
した際に true/falseの値を返すことを期待します。 asyncTester
の返り値は非同期のvalidatorとなります。
import { asyncCombine, asyncTester, toAsync } from 'favalid'; const USERDB = { "example@hello.com": true }; // API requestをstubする。 const apiRequest = async (email) => { if (USERDB[email]) { return false } return true }; const asyncEmailValidator = asyncCombine( toAsync(emailValidator), // 以前作ったemailValidatorを非同期化 asyncTester(apiRequest, (email) => `${email} はすでに存在しています`) ); async function testing() { console.log(await asyncEmailValidator('hoge.com')); // { error: true, message: 'メールアドレスは10文字以上です。' } console.log(await asyncEmailValidator('email@hoge.com')); // { error: false, message: '' } console.log(await asyncEmailValidator('example@hello.com')); // { error: true, message: 'example@hello.com はすでに存在しています' } } testing();
今後の機能改善の予定
- 型を付ける
- せっかくtypescriptで書いているのに、genericsなどの機能が使いこなせておらず、残念な実装になっています。そこをもうちょいなんとかする予定です。
- ramda.jsのリプレイス
- 依存でramda.jsが入っていますが、ごくごく一部の機能しか使っていないので、除去する予定です。
- ドキュメント
- がんばります...
- i18n
それ以外にも機能のリクエストなどありましたら教えていただけると幸いです。
まとめ
frontend javascript向けvalidator favalid
の紹介記事を書きました。
githubのREADMEがあまりにも適当なので、使い方を補完させていただきました。応用的な使い方では様々な機能を紹介しましたが、不要なものは import
しなければ tree-shaking
が効くはずなので、自分が必要なものだけを import
するように使っていただければと思います。
以上となります、読んでいただきありがとうございました!