Spring validator、kotlinでカスタムバリデータを作る

# kotlinでカスタムバリデータを作る
kotlinでOpenAPIを使ってサーバサイドアプリを作っていますが、よくある仕様としてRESTのStringの項目が、半角のみを許容する場合、全角のみを許容する場合があります。

これらのバリデーションは@Petternで正規表現を使って頑張って書けば書けないことはありませんが、出現回数が多くなります。

kotlinで、半角のみを許容する、全角のみを許容するカスタムバリデータを作成してみようと思います。

# 半角のみを許容するバリデータ
```kotlin
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
@Constraint(validatedBy = [HalfCharValidator::class])
annotation class HalfChar(
    val message: String = "半角のみ許可されます",
    val groups: Array<KClass<*>> = [],
    val payload: Array<KClass<out Payload>> = []
)

class HalfCharValidator: ConstraintValidator<HalfChar, String> {

    private val regex = Regex("^[ -~｡-ﾟ]*$")

    override fun isValid(value: String?, context: ConstraintValidatorContext?): Boolean {
        if (value == null) {
            return true
        }
        return value.matches(regex)
    }
}
```
アノテーションを定義するクラスとそのアノテーションの実装のクラスに分かれます。実際の入力値チェックはisValid関数の中身です。
ミソはnullだったらtrueでreturnすること。じゃないとnullでもこのValidatorが効いてエラーメッセージを出力します。
正規表現を使っているので正規表現はisValid関数の中に入れないでフィールドとして外に出します。正規表現のコンパイルはコストが高いので、これによって正規表現のコンパイルが1回に抑えられます。

groupsとpayloadは今回は使用していませんが、ドキュメントによると、

 * groups: 
   この属性は、特定の制約を特定のグループに関連付けるために使用されます。これにより、ある状況では一部の制約を適用し、別の状況では他の制約を適用するといった、制約のグループ化と選択的な適用が可能になります。例えば、あるエンティティが保存される前後で適用する制約を変更したい場合などに使用します。デフォルトでは、制約はDefaultグループに属します。  
* payload: 
  この属性は、制約違反に関する追加情報を提供するために使用されます。これは主にエラーレポートに役立つ情報を提供するために使用されます。payloadはPayloadインターフェースを実装する任意のクラスを指定できます。これにより、制約違反の重大度（例えば、情報、警告、エラーなど）を指定したり、制約違反が発生した場合の対応策を提供したりすることが可能になります。

らしいです。

# 全角のみを許容するバリデータ
```kotlin
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
@Constraint(validatedBy = [FullCharValidator::class])
annotation class FullChar(
    val message: String = "全角のみ許可されます",
    val groups: Array<KClass<*>> = [],
    val payload: Array<KClass<out Payload>> = []
)

class FullCharValidator: ConstraintValidator<FullChar, String> {

    private val regex = Regex("^[^ -~｡-ﾟ]*$")

    override fun isValid(value: String?, context: ConstraintValidatorContext?): Boolean {
        if (value == null) {
            return true
        }
        return value.matches(regex)
    }
}
```
全角の場合は半角と正規表現が反対になっています。それ以外は同じです。

# フィールドに定義する
バリデーションチェックはRESTのリクエストに設定することになります。
(レスポンスにもバリデーションチェックすることもあるらしいが・・・あまい大きな意味はないと思う。クライアントに対してこのバリデーションチェックを通過したものを返していますよ、と言う意味ぐらいしかない)
```kotlin
@Schema(description = "ログインリクエスト")
data class LoginRequest (
    @field:NotEmpty(message = "省略できません")
    @field:HalfChar(message = "半角文字で入力してください")
    @field:Size(max = 20, message = "最大20文字までで入力してください")
    @field:JsonProperty("loginUserId", required = true)
    @Schema(description = "ユーザーID", type = "string", example = "user01", required = true )
    var loginUserId: String,

    @field:NotEmpty(message = "省略できません")
    @field:HalfChar(message = "半角文字で入力してください")
    @field:JsonProperty("oldPassword", required = true)
    @Schema(description = "パスワード", type = "string", example = "password", required = true )
    var password: String,
)
```
kotlinのdata classの場合はフィールドはコンストラクタでもあり、フィールドでもあります。どちらに対するアノテーションなのかを示すため、頭に@fieldを付けます。
```kotlin
@field:HalfChar(message = "半角文字で入力してください")
```

# Controllerクラス
Controllerクラスのエンドポイントのメソッドの@RequestBodyの引数にもうひとつアノテーション、@Validを追加します
```kotlin
    @PostMapping(LOGIN_PATH)
    fun login(@Valid @RequestBody request: LoginRequest): ResponseEntity<LoginResponse> {

    ・・・・

}
    
```
これでカスタムバリデータが有効になります。