---
title: YAVIのArguments Validatorについて
tags: ["Java", "YAVI"]
categories: ["Programming", "Java", "am", "ik", "yavi"]
date: 2021-06-07T05:57:49Z
updated: 2021-06-08T07:43:52Z
---

[YAVIでValue ObjectのValidation (Records対応)](/entries/645) の記事では説明を省いた [Arguments Validator](https://github.com/making/yavi#arguments-validator) が [YAVI 0.7.0](https://github.com/making/yavi/releases/tag/0.7.0) で大きな進化を遂げたので紹介します。

**目次**
<!-- toc -->

### Arguments Validatorとは
Arguments Validatorは名前の通り、引数に対するValidatorです。
Bean Validationを含む、通常のValidatorは値が設定されたObjectに対して、設定された値が適切かどうかを検証しますが、
Arguments ValidatorはObjectへ設定する引数が適切かどうかを検証します。

通常のValidatorでは検証前にObjectが作成されるので、一時的に不完全な状態のObjectを作成してしまう可能性があります。
例えばValidator側で`notNull()`の制約を課していても、コンストラクタ内でnullチェックが組み込まれている場合はValidatorによる検証実行前にこのnullチェックが働きます。
Arguments Validatorは検証後にObjectを作成するので、不完全なObjectを作らなくても済みます。

Arguments ValidatorはYAVI 0.3.0でサポートされました。0.7.0以前では次のように定義できます。

```java
public record Person(String name, String email, Integer age) {
    public static Arguments3Validator<String, String, Integer, Person> validator = ArgumentsValidatorBuilder
        .of(Person::new)
        .builder(b -> b
            ._string(Arguments1::arg1, "name", c -> c.notBlank().lessThanOrEqual(100)) // Person::newの第1引数に対する制約
            ._string(Arguments2::arg2, "email", c -> c.notBlank().lessThanOrEqual(100).email()) // Person::newの第2引数に対する制約
            ._integer(Arguments3::arg3, "age", c -> c.greaterThanOrEqual(0).lessThan(200))) // Person::newの第3引数に対する制約
        .build();
}
```
Arguments Validatorは次のように利用できます。
```java
Validated<Person> personValidated = Person.validator.validate("Jone Doe", "jdoe@example.com", 30);
// 0.6.0以前はEitherを返していたが、0.6.0でValidatedに変更
```

> `Validated`型に関しては[YAVIでValue ObjectのValidation (Records対応)](/entries/645) を参照してください。

または
```java
Person person = Person.validator.validated("Jone Doe", "jdoe@example.com", 30);
// 検証失敗時にはConstraintViolationsExceptionがスローされる
```

`Arguments1Validator`から`Arguments16Validator`まで用意されています。

Arguments Validatorは用途は有用なのですが、定義が少し面倒に感じるかもしれません。
Type-Safeなので型に合わない定義はできないのですが、型に合うようにパズルのようにはめ込むように感じられます。

### 小さなValidatorの定義のサポート
YAVI 0.7.0では [gakuzzzzさんの提案](https://github.com/making/yavi/issues/132) により、
小さなArguments Validatorの簡易的な定義とその合成がサポートされました。

例えば単一のStringやIntegerに対する`Arguments1Validator`を定義するだけであれば次のように記述できます。
```java
StringValidator<String> nameValidator = StringValidatorBuilder
    .of("name", c -> c.notBlank().lessThanOrEqual(100))
    .build();  // -> extends Arguments1Validator<String, String>

StringValidator<String> emailValidator = StringValidatorBuilder
    .of("email", c -> c.notBlank().lessThanOrEqual(100).email())
    .build();  // -> extends Arguments1Validator<String, String>

IntegerValidator<Integer> ageValidator = IntegerValidatorBuilder
    .of("age", c -> c.greaterThanOrEqual(0).lessThan(200))
    .build();  // -> extends Arguments1Validator<Integer, Integer>

Validated<String> nameValidated = nameValidator.validate("Jone Doe");
Validated<String> emailValidated = nameValidator.validate("jdoe@example.com");
Validated<Integer> ageValidated = nameValidator.validate(30);
```

`andThen`メソッドを使うことで検証後に任意のオブジェクトに変換できます。

```java
public record Name(String value) { /* ... */ }
public record Email(String value) { /* ... */ }
public record Age(Integer value) { /* ... */ }

StringValidator<Name> nameValidator = StringValidatorBuilder
    .of("name", c -> c.notBlank().lessThanOrEqual(100))
    .build()
    .andThen(Name::new); // -> extends Arguments1Validator<String, Name>

StringValidator<Email> emailValidator = StringValidatorBuilder
    .of("email", c -> c.notBlank().lessThanOrEqual(100).email())
    .build()
    .andThen(Email::new); // -> extends Arguments1Validator<String, Email>

IntegerValidator<Age> ageValidator = IntegerValidatorBuilder
    .of("age", c -> c.greaterThanOrEqual(0).lessThan(200))
    .build()
    .andThen(Age::new); // -> extends Arguments1Validator<Integer, Age>

Validated<Name> nameValidated = nameValidator.validate("Jone Doe");
Validated<Email> emailValidated = nameValidator.validate("jdoe@example.com");
Validated<Age> ageValidated = nameValidator.validate(30);
```

Listを扱いたい場合は次のように変換できます。

```java
Arguments1Validator<Iterable<String>, List<Email>> emailsValidator = ArgumentsValidators.liftList(emailValidator);
Validated<List<Email>> emailsValidated = emailsValidator.validate(List.of("foo@example.com", "bar@example.com"));
```

面白いことに`compose`メソッドを使うことで、各値の導出元のオブジェクトからの変換を含むValidatorを作成することもできるようになりました。

例えばServletで`name`と`email`と`age`がHTTPのリクエストパラメータとして`HttpServletRequest`オブジェクトから取得するケースでは次のように定義できます。
```java
Argument1Validator<HttpServletRequest, Name> requestNameValidator = nameValidator
    .compose(req -> req.getParameter("name"));
Argument1Validator<HttpServletRequest, Email> requestEmailValidator = emailValidator
    .compose(req -> req.getParameter("email"));
Argument1Validator<HttpServletRequest, Age> requestAgeValidator = ageValidator
    .compose(req -> Integer.valueOf(req.getParameter("age")));

HttpServletRequest request = ...;
Validated<Name> nameValidated = requestNameValidator.validate(request);
Validated<Email> emailValidated = requestEmailValidator.validate(request);
Validated<Age> ageValidated = requestAgeValidator.validate(request);
```

### Arguments Validatorの合成のサポート
小さなValidatorは定義できましたが、では`Person`オブジェクトの引数に対するValidatorはどうすれば良いでしょうか。

0.6.0では検証結果である`Validated`(または`Validation`)の合成がサポートされましたが、
0.7.0では検証するためのArguments Validatorの合成がサポートされました。

コンストラクタ`Person(String, String, Integer)`に対するValidatorは`Arguments3Validator<String, String, Integer, Person>`ですが、
これを`Arguments1Validator<String, String>`(`StringValidator<String>`)、`Arguments1Validator<String, String>`(`StringValidator<String>`)、`Arguments1Validator<Integer, Integer>`(`IntegerValidator<Integer>`)の
3つのValidatorに分割して合成します。

次のように定義できます。
```java
StringValidator<String> nameValidator = ...;
StringValidator<String> emailValidator = ...;
IntegerValidator<Integer> ageValidator = ...;

Arguments3Validator<String, String, Integer, Person> personValidator = ArgumentsValidators
    .split(nameValidator, emailValidator, ageValidator)
    .apply(Person::new);
// or
Arguments3Validator<String, String, Integer, Person> personValidator = nameValidator
    .split(emailValidator)
    .split(ageValidator)
    .apply(Person::new);
```

Value Objectに変換する場合も同じです。

```java
public record Person(Name name, Email email, Age age) { /* ... */ }

StringValidator<Name> nameValidator = ...;
StringValidator<Email> emailValidator = ...;
IntegerValidator<Age> ageValidator = ...;

Arguments3Validator<String, String, Integer, Person> personValidator = ArgumentsValidators
    .split(nameValidator, emailValidator, ageValidator)
    .apply(Person::new);
// or
Arguments3Validator<String, String, Integer, Person> personValidator = nameValidator
    .split(emailValidator)
    .split(ageValidator)
    .apply(Person::new);
```

使い方は最初の例と同じです。

```java
Validated<Person> personValidated = Person.validator.validate("Jone Doe", "jdoe@example.com", 30);
// or
Person person = Person.validator.validated("Jone Doe", "jdoe@example.com", 30);
```

先の例で`HttpServletRequest`からvalidateするケースを扱いましたが、この時に作ったValidatorは次のように`combine`メソッドで合成して`Person`オブジェクトを作成できます。

```java
Arguments1Validator<HttpServletRequest, Person> requestPersonValidator = ArgumentsValidators
    .combine(requestNameValidator, requestEmailValidator, requestAgeValidator)
    .apply(Person::new);
// or
Arguments1Validator<HttpServletRequest, Person> requestPersonValidator = requestNameValidator
    .combine(requestEmailValidator)
    .combine(requestAgeValidator)
    .apply(Person::new);

HttpServletRequest request = ...;
Validated<Person> personValidated = requestPersonValidator.validate(request);
```

なお、このValidatorは`Arguments3Validator<String, String, Integer, Person>`からも次のように変換可能です。

```java
Arguments1Validator<HttpServletRequest, Person> requestPersonValidator = personValidator
    .compose(req -> Arguments.of(req.getParameter("name"), req.getParameter("email"), Integer.valueOf(req.getParameter("age"))));
```

このようにValidatorを合成することでいろいろなパターンのValidatorを作り出すことができます。
小さなArguments Validatorを作っておけば、オブジェクト作成前に値を検証できるだけでなく、再利用性が高まります。

---
YAVI 0.7.0での改善の嬉しい点はドメイン層で定義したValidator(ここでは`Arguments3Validator<String, String, Integer, Person>`)を使って、
Web層のValidator(ここでは`Arguments1Validator<HttpServletRequest, Person>`。Formクラスを作成する場合は`Arguments1Validator<PersonForm, Person>`など。)を導出できる点です。

これまではWeb層で用意したValidatorから`Validated<PersonForm>`を作成し、`Person`オブジェクトに変換する(パターン1)か、
Web層の`PersonForm`は検証せず、`Person`オブジェクトに値を詰め替えてからドメイン層で用意したValidatorで`Validated<Person>`を作る(パターン2)必要がありました。
Bean Validationを使う場合は実質的にパターン1の場合が多いと思います。

```java
// パターン1
ApplicativeValidator<PersonForm> formValidator = PersonForm.validator; // Web層のValidator
PersonForm form = ...;
Validated<PersonForm> formValidated = formValidator.validate(form);
Validated<Person> personValidated = formValidated.map(form -> new Person(form.getName(), form.getEmail(), form.getAge()));

// パターン2
ApplicativeValidator<Person> personValidator = Person.validator; // ドメイン層のValidator
PersonForm form = ...;
Person person = new Person(form.getName(), form.getEmail(), form.getAge());
Validated<Person> personValidated = personValidator.validate(person);
```

YAVI 0.7.0からパターン3としてドメイン層のValidatorからWeb層のValidatorを作り、検証後に`Person`オブジェクトを返すことができるようになります。
個人的には検証ルールはできるだけドメイン層に持ちたいと思っていたので、このパターンが使えるのようになるのは嬉しいです。

```java
// パターン3
Arguments3Validator<String, String, Integer, Person> personValidator = Person.validator; // ドメイン層のValidator
Arguments1Validator<PersonForm, Person> formValidator = personValidator
    .compose(form -> Arguments.of(form.getName(), form.getEmail(), form.getAge())); // Web層のValidator
PersonForm form = ...;
Validated<Person> personValidated = formValidator.validate(form);
```

Web層にしか存在しないパラメータのバリデーションが必要な場合も、`Arguments1Validator`を合成することで対応できます。

```java
// チェックボックスにチェックが必要な例。ドメイン層には不要なルール(そもそもUIのみで閉じても良い...)
Arguments1Validator<PersonForm, Boolean> acceptedValidator = BooleanValidatorBuilder
    .of("accpected", c -> c.notNull().isTrue())
    .build()
    .compose(PersonForm::isAccepted);

Arguments1Validator<PersonForm, Person> formValidator = Person.validator // ドメイン層のValidator
    .<PersonForm>compose(form -> Arguments.of(form.getName(), form.getEmail(), form.getAge()))
    .combine(acceptedValidator)
    .apply((person, accepted) -> person); // Web層のValidator
```

> パターン3を実現するための`compose`メソッドは強力で、`ApplicativeValidator<T>`でも使えるように0.8.0で対応します。<br>
> https://github.com/making/yavi/pull/138 <br>
> YAVI 0.7.0では次のような変換が必要です。
> ```java
> Validator<T> validater = ...;
> Arguments1Validator<T, T> arguments1Validator = validator.applicative()::validate;
> ```

めちゃくちゃ強力だと思いませんか？
ぜひYAVIを使ってみてフィードバックをください。
