YAVI使い方メモ

# YAVIとは？
- Java のバリデーションライブラリ
- ラムダを使用し、型安全な API を提供する
- Yet Another Validation for Java の略で、読みは**ヤバイ**
- 日本人の [@making](https://x.com/making) さんが作っている
- Spring WebFlux.fn で使えるバリデーションが欲しいというのが動機で、 Bean Validation を置き換えるのが目的ではない
    - 詳しい話は [Java用Validatorライブラリ"YAVI"(ヤヴァイ)の紹介 - IK.AM](https://ik.am/entries/461) を参照のこと

# 環境
```
>gradle --version

------------------------------------------------------------
Gradle 8.8
------------------------------------------------------------

Build time:   2024-05-31 21:46:56 UTC
Revision:     4bd1b3d3fc3f31db5a26eecb416a165b8cc36082

Kotlin:       1.9.22
Groovy:       3.0.21
Ant:          Apache Ant(TM) version 1.10.13 compiled on January 4 2023
JVM:          21.0.3 (Eclipse Adoptium 21.0.3+9-LTS)
OS:           Windows 10 10.0 amd64
```

# Hello World
## 実装
```gradle:build.gradle
plugins {
    id "java"
}

java {
    sourceCompatibility = 21
    targetCompatibility = 21
}

compileJava.options.encoding = "UTF-8"

repositories {
    mavenCentral()
}

dependencies {
    implementation "am.ik.yavi:yavi:0.14.0"
}
```

```java:Hoge.java
package sandbox.yavi;

public record Hoge(String text, int number) {
}
```

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;

public class Main {

    public static void main(String[] args) {
        final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
                .constraint(Hoge::text, "text", c -> c.notBlank())
                .constraint(Hoge::number, "number",
                        c -> c.greaterThan(0).lessThanOrEqual(100))
                .build();

        printResult(validator, new Hoge("", 1));
        printResult(validator, new Hoge("a", 101));
        printResult(validator, new Hoge("a", 100));
    }

    private static <T> void printResult(Validator<T> validator, T target) {
        final ConstraintViolations violations = validator.validate(target);
        System.out.println(
            target + " > " + (violations.isValid() ? "valid" : "invalid"));
        if (!violations.isValid()) {
            violations.stream()
                    .map(e -> "  " + e.message())
                    .forEach(System.out::println);
        }
    }
}
```




## 実行結果
```text
Hoge[text=, number=1] > invalid
  "text" must not be blank
Hoge[text=a, number=101] > invalid
  "number" must be less than or equal to 100
Hoge[text=a, number=100] > valid
```

## 説明
```gradle:build.gradle
dependencies {
    implementation "am.ik.yavi:yavi:0.14.0"
}
```

- 依存関係として [am.ik.yavi:yavi](https://central.sonatype.com/artifact/am.ik.yavi/yavi) を設定する

```java:Main.java
        final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
                .constraint(Hoge::text, "text", c -> c.notBlank())
                .constraint(Hoge::number, "number",
                        c -> c.greaterThan(0).lessThanOrEqual(100))
                .build();
```

- `ValidatorBuilder` を使って `Validator` を作成する
- `of` メソッドの型引数で、バリデーション対象の型を指定する
- `constraint` メソッドで、バリデーション対象の型に対する制約を定義する
    - 第一引数に、オブジェクトから検査対象の値を取得するための関数を渡す
        - 多くの場合、これは Getter のメソッド参照を指定することで記述できる
    - 第二引数で、検査対象の項目名を指定する
    - 第三引数には、具体的な制約の内容を定義する関数を渡す
        - ここでは `text` が空でないことと、 `number` が 0 より大きく 100 以下であることを定義している

```java:Main.java
    private static <T> void printResult(Validator<T> validator, T target) {
        final ConstraintViolations violations = validator.validate(target);
        System.out.println(
            target + " > " + (violations.isValid() ? "valid" : "invalid"));
        if (!violations.isValid()) {
            violations.stream()
                    .map(e -> "  " + e.message())
                    .forEach(System.out::println);
        }
    }
```

- `Validator` の `validate` メソッドに検査対象のオブジェクトを渡すことで、違反情報が格納された `ConstraintViolations` が返される
- `isValid` メソッドで全体のバリデーション結果が分かる
- `ConstraintViolations` は `ConstraintViolation` の `List` となっており、個々の違反結果の詳細は `ConstraintViolation` から取得できる

# 基本
## Validator

```java
        final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
                .constraint(Hoge::text, "text", c -> c.notBlank())
                .constraint(Hoge::number, "number",
                        c -> c.greaterThan(0).lessThanOrEqual(100))
                .build();
        
        ...
        
        final ConstraintViolations violations = validator.validate(target);
```

- YAVI では、検査したいクラスごとに `Validator` を用意する
    - `Validator` のインスタンスはスレッドセーフで、様々な場所で再利用できるように設計されている
- `Validator` のインスタンスは、 `ValidatorBuilder` を使って作成する
    - `of` メソッドでビルダーの作成を開始する
    - このとき、型引数で検査対象のクラスを指定する
- `ValidatorBuilder` には制約を定義するための `constraint` メソッドが用意されている
    - 第一引数には、対象オブジェクトから検査対象の値を取得するための関数を渡す
        - 通常、ここには検査対象の値を取得するための Getter をメソッド参照で指定すればいい
    - 第二引数には、検査対象の値の名前を指定する
        - この名前は、エラーメッセージに利用される
        - [後述するアノテーションプロセッサー](#アノテーションプロセッサー)を利用すると、名前を文字列で指定せずに実装することもできるようになる
    - 第三引数には、具体的な制約の内容を定義する関数を渡す
        - この関数には、制約を定義するためのオブジェクト(`Constraint` およびそのサブタイプ)が渡される
        - `notNull` や `greaterThan` など、制約を定義するためのメソッドが多数用意されていて、これらをメソッドチェーンで繋げることによって制約を定義する
- `Validator` の `validate` メソッドに検査対象のオブジェクトを渡すことで、検査を実行できる

## ConstraintViolations, ConstraintViolation
```java
    private static <T> void printResult(Validator<T> validator, T target) {
        final ConstraintViolations violations = validator.validate(target);
        System.out.println(
            target + " > " + (violations.isValid() ? "valid" : "invalid"));
        if (!violations.isValid()) {
            violations.stream()
                    .map(e -> "  " + e.message())
                    .forEach(System.out::println);
        }
    }
```

- `Validator` の `validate` で検索を実行すると、違反情報が `ConstraintViolations` という型で返される
- `ConstraintViolations` は `ConstraintViolation` の `List` となっていて、 `ConstraintViolation` に個々の違反内容が格納されている
    - 違反が１つ以上あるかどうかは `isValid` メソッドで確認できる
    - 違反がゼロの場合は、空になっている
- `ConstraintViolation` には、以下のような個々の違反の詳細情報が格納されている
    - エラーになった項目の名前 (`name` メソッド)
    - エラーメッセージ (`message` メソッド)
    - エラーになった値 (`violatedValue` メソッド)

### 違反情報をシリアライズしやすい形にする
```java:Main.java
package sandbox.yavi;

import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;
import am.ik.yavi.core.ViolationDetail;

import java.util.Arrays;
import java.util.List;

public class Main {

    public static void main(String[] args) {
        final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
                .constraint(Hoge::text, "text", c -> c.notBlank())
                .constraint(Hoge::number, "number",
                        c -> c.greaterThan(0).lessThanOrEqual(100))
                .build();

        final ConstraintViolations violations = validator.validate(new Hoge("", 101));
        final List<ViolationDetail> details = violations.details();

        details.forEach(detail -> System.out.printf("""
            ViolationDetail {
                key = %s
                args = %s
                defaultMessage = %s
            }
            """,
            detail.getKey(),
            Arrays.toString(detail.getArgs()),
            detail.getDefaultMessage()
        ));
    }
}
```

```text:実行結果
ViolationDetail {
    key = charSequence.notBlank
    args = [text, ]
    defaultMessage = "text" must not be blank
}
ViolationDetail {
    key = numeric.lessThanOrEqual
    args = [number, 100, 101]
    defaultMessage = "number" must be less than or equal to 100
}
```

- `ConstraintViolations` の `details` または `ConstraintViolation` の `detail` メソッドを使用すると、違反情報を `ViolationDetail` に変換できる
- `ConstraintViolation` は内部に `MessageFormatter` を持っているなど、そのままシリアライズするには都合が悪い構造になっている
- `ViolationDetail` はただのデータオブジェクトなので、そのままシリアライズしやすくなっている
- Jackson で JSON にシリアライズしたい、といった場合などに使える


# null 値の扱い

```java
public class Main {
    public static void main(String[] args) {
        final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
                // ★ 「text は 10 文字未満」という制約を定義
                .constraint(Hoge::text, "text", c -> c.lessThan(10))
                .constraint(Hoge::number, "number", c -> c.greaterThan(0))
                .build();

        // ★ text に null を設定
        printResult(validator, new Hoge(null, 1));
        printResult(validator, new Hoge("a", 1));
    }
    
    private static <T> void printResult(Validator<T> validator, T target) {
        final ConstraintViolations violations = validator.validate(target);
        System.out.println(
            target + " > " + (violations.isValid() ? "valid" : "invalid"));
        if (!violations.isValid()) {
            violations.stream()
                    .map(e -> "  " + e.message())
                    .forEach(System.out::println);
        }
    }
}
```

```text:実行結果
Hoge[text=null, number=1] > valid
Hoge[text=a, number=1] > valid
```

- null は基本的に違反にはならない
- null を違反としたい場合は、 `notNull` や `notEmpty`, `notBlank` などで明示的に null が違反であることを定義する


# フェイルファースト
```java
public class Main {
    public static void main(String[] args) {
        final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
                .constraint(Hoge::text, "text", c -> c.notBlank())
                .constraint(Hoge::number, "number",
                        c -> c.greaterThan(0).lessThanOrEqual(100))
                .build();

        final Hoge hoge = new Hoge("", -1); // ★ 複数の項目が違反になるように定義
        final ConstraintViolations violations = validator.validate(hoge);
        System.out.println("valid=" + violations.isValid());
        for (ConstraintViolation violation : violations) {
            System.out.println("message=" + violation.message());
        }
    }
}
```

```text:実行結果
valid=false
message="text" must not be blank
message="number" must be greater than 0
```

- デフォルトでは、途中で違反が見つかったとしても全ての項目に対して検査が実施される
- 違反が見つかった場合、その時点で検査を中断させたい場合はフェイルファーストモードを有効にする

```java:フェイルファーストモードを有効にした場合
public class Main {
    public static void main(String[] args) {
        final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
                .constraint(Hoge::text, "text", c -> c.notBlank())
                .constraint(Hoge::number, "number",
                        c -> c.greaterThan(0).lessThanOrEqual(100))
                .failFast(true) // ★ failFast で true を設定する
                .build();

        final Hoge hoge = new Hoge("", -1);
        final ConstraintViolations violations = validator.validate(hoge);
        System.out.println("valid=" + violations.isValid());
        for (ConstraintViolation violation : violations) {
            System.out.println("message=" + violation.message());
        }
    }
}
```

```text:実行結果
valid=false
message="text" must not be blank
```

- `Validator` を作成するときに `failFast(true)` を指定することで、 `Validator` をフェイルファーストモードに変更できる
- フェイルファーストモードの `Validator` は、違反が見つかるとその時点で検査を中断するようになる

## 既存の Validator をフェイルファーストモードに切り替える
```java
public class Main {
    public static void main(String[] args) {
        final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
                .constraint(Hoge::text, "text", c -> c.notBlank())
                .constraint(Hoge::number, "number",
                        c -> c.greaterThan(0).lessThanOrEqual(100))
                .build();

        final Validator<Hoge> failFastValidator = validator.failFast(true);

        final Hoge hoge = new Hoge("", -1);

        System.out.println("=== Original validator ===");
        validate(validator, hoge);
        System.out.println("=== Fail fast validator ===");
        validate(failFastValidator, hoge);
    }

    private static void validate(Validator<Hoge> validator, Hoge hoge) {
        final ConstraintViolations violations = validator.validate(hoge);
        System.out.println("valid=" + violations.isValid());
        for (ConstraintViolation violation : violations) {
            System.out.println("message=" + violation.message());
        }
    }
}
```

```text:実行結果
=== Original validator ===
valid=false
message="text" must not be blank
message="number" must be greater than 0
=== Fail fast validator ===
valid=false
message="text" must not be blank
```

- `Validator` の `failFast` メソッドを使うことで、既存の `Validator` をフェイルファーストモードに切り替えた新しい `Validator` を取得できる

# ネストされたオブジェクトの制約
```java:NestedClass.java
package sandbox.yavi;

public record NestedClass(int number, String text) {
}
```

```java:EnclosingClass.java
package sandbox.yavi;

public record EnclosingClass(String text, NestedClass nested) {
}
```

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;

public class Main {
    public static void main(String[] args) {
        final Validator<NestedClass> nestedValidator
                = ValidatorBuilder.<NestedClass>of()
                .constraint(NestedClass::number, "number", c -> c.greaterThan(0))
                .constraint(NestedClass::text, "text", c -> c.notBlank())
                .build();

        final Validator<EnclosingClass> encloseingValidator
                = ValidatorBuilder.<EnclosingClass>of()
                .constraint(EnclosingClass::text, "text", c -> c.notBlank())
                .nest(EnclosingClass::nested, "nested", nestedValidator)
                .build();

        printResult(encloseingValidator,
            new EnclosingClass("enclosing", new NestedClass(-1, "nested")));
        printResult(encloseingValidator,
            new EnclosingClass("enclosing", new NestedClass(1, "nested")));
    }

    private static <T> void printResult(Validator<T> validator, T target) {
        final ConstraintViolations violations = validator.validate(target);
        System.out.println(
            target + " > " + (violations.isValid() ? "valid" : "invalid"));
        violations.stream()
                .map(e -> "  " + e.message())
                .forEach(System.out::println);
    }
}
```

```text:実行結果
EnclosingClass[text=enclosing, nested=NestedClass[number=-1, text=nested]] > invalid
  "nested.number" must be greater than 0
EnclosingClass[text=enclosing, nested=NestedClass[number=1, text=nested]] > valid
```


**説明**

```java:Main.java
        final Validator<NestedClass> nestedValidator
            = ValidatorBuilder.<NestedClass>of()
                .constraint(NestedClass::number, "number", c -> c.greaterThan(0))
                .constraint(NestedClass::text, "text", c -> c.notBlank())
                .build();

        final Validator<EnclosingClass> encloseingValidator
            = ValidatorBuilder.<EnclosingClass>of()
                .constraint(EnclosingClass::text, "text", c -> c.notBlank())
                .nest(EnclosingClass::nested, "nested", nestedValidator)
                .build();
```

- ネストされたオブジェクトの制約を定義する場合は、 `nest` メソッドを使用する
- ネストされたクラス自体の `Validator` は別途定義しておき、 `nest` メソッドの第三引数で渡す

## その場で制約定義する
```java
public class Main {
    public static void main(String[] args) {
        final Validator<EnclosingClass> encloseingValidator
                = ValidatorBuilder.<EnclosingClass>of()
                .constraint(EnclosingClass::text, "text", c -> c.notBlank())
                .nest(EnclosingClass::nested, "nested",
                    // ★その場で制約を定義
                    b -> b.constraint(NestedClass::number, "number",
                              c -> c.greaterThan(0))
                          .constraint(NestedClass::text, "text",
                              c -> c.notBlank())
                )
                .build();

        printResult(encloseingValidator,
                new EnclosingClass("enclosing", new NestedClass(-1, "nested")));
        printResult(encloseingValidator,
                new EnclosingClass("enclosing", new NestedClass(1, "nested")));
    }

    private static <T> void printResult(Validator<T> validator, T target) {
        final ConstraintViolations violations = validator.validate(target);
        System.out.println(
            target + " > " + (violations.isValid() ? "valid" : "invalid"));
        violations.stream()
                .map(e -> "  " + e.message())
                .forEach(System.out::println);
    }
}
```

```text:実行結果
EnclosingClass[text=enclosing, nested=NestedClass[number=-1, text=nested]] > invalid
  "nested.number" must be greater than 0
EnclosingClass[text=enclosing, nested=NestedClass[number=1, text=nested]] > valid
```

- `nest` メソッドの第三引数に、ネストされたクラスの制約を定義する関数を渡すことで、事前に定義した `Validator` ではなくその場で制約を定義することもできる

## ネストされた項目を null 可にする
```java:何もしない場合のデフォルトの動き
public class Main {
    public static void main(String[] args) {
        final Validator<NestedClass> nestedValidator
                = ValidatorBuilder.<NestedClass>of()
                .constraint(NestedClass::number, "number", c -> c.greaterThan(0))
                .constraint(NestedClass::text, "text", c -> c.notBlank())
                .build();

        final Validator<EnclosingClass> encloseingValidator
                = ValidatorBuilder.<EnclosingClass>of()
                .constraint(EnclosingClass::text, "text", c -> c.notBlank())
                .nest(EnclosingClass::nested, "nested", nestedValidator)
                .build();

        // nested に null を設定
        printResult(encloseingValidator,
                new EnclosingClass("enclosing", null));
        printResult(encloseingValidator,
                new EnclosingClass("enclosing", new NestedClass(1, "nested")));
    }
}
```

```text:実行結果
EnclosingClass[text=enclosing, nested=null] > invalid
  "nested" must not be null
EnclosingClass[text=enclosing, nested=NestedClass[number=1, text=nested]] > valid
```

- `nest` で定義した項目は、 null 不可扱いになる
- null 可としたい場合は `nestIfPresent` を使用する

```java:nestIfPresentを使った場合
public class Main {
    public static void main(String[] args) {
        final Validator<NestedClass> nestedValidator
                = ValidatorBuilder.<NestedClass>of()
                .constraint(NestedClass::number, "number", c -> c.greaterThan(0))
                .constraint(NestedClass::text, "text", c -> c.notBlank())
                .build();

        final Validator<EnclosingClass> encloseingValidator
                = ValidatorBuilder.<EnclosingClass>of()
                .constraint(EnclosingClass::text, "text", c -> c.notBlank())
                // ★ nestIfPresent を使用
                .nestIfPresent(EnclosingClass::nested, "nested", nestedValidator)
                .build();

        final EnclosingClass enclosing = new EnclosingClass("", null);

        final ConstraintViolations violations = encloseingValidator.validate(enclosing);

        violations.stream()
                .map(ConstraintViolation::message)
                .forEach(System.out::println);
    }
}
```

```text:実行結果
EnclosingClass[text=enclosing, nested=null] > valid
EnclosingClass[text=enclosing, nested=NestedClass[number=1, text=nested]] > valid
```

# コレクション・配列の制約
## List
```java:Hoge.java
package sandbox.yavi;

public record Hoge(String text) {
}
```

```java:Hoges.java
package sandbox.yavi;

import java.util.List;

public record Hoges(List<Hoge> list) {
}
```

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;

import java.util.List;

public class Main {
    public static void main(String[] args) {
        final Validator<Hoge> hogeValidator = ValidatorBuilder.<Hoge>of()
                .constraint(Hoge::text, "text", c -> c.notEmpty())
                .build();

        final Validator<Hoges> hogesValidator
                = ValidatorBuilder.<Hoges>of()
                .forEach(Hoges::list, "list", hogeValidator)
                .build();

        printResult(hogesValidator, new Hoges(List.of(
            new Hoge("a"),
            new Hoge("b"),
            new Hoge("")
        )));
        printResult(hogesValidator, new Hoges(List.of(
            new Hoge("a"),
            new Hoge("b"),
            new Hoge("c")
        )));
    }

    private static <T> void printResult(Validator<T> validator, T target) {
        final ConstraintViolations violations = validator.validate(target);
        System.out.println(
            target + " > " + (violations.isValid() ? "valid" : "invalid"));
        violations.stream()
                .map(e -> "  " + e.message())
                .forEach(System.out::println);
    }
}
```

```text:実行結果
Hoges[list=[Hoge[text=a], Hoge[text=b], Hoge[text=]]] > invalid
  "list[2].text" must not be empty
Hoges[list=[Hoge[text=a], Hoge[text=b], Hoge[text=c]]] > valid
```

- `List` や `Set`、配列の制約を定義する場合は、 `forEach` を使用する

```java
.forEach(Hoges::list, "list", hogeValidator)
```

- `forEach` の第三引数には、コレクション要素のクラス用の `Validator` を渡す
- もしくは、その場で要素用の制約を定義することもできる

```java:その場で制約を定義する場合
    public static void main(String[] args) {
        final Validator<Hoges> hogesValidator
                = ValidatorBuilder.<Hoges>of()
                .forEach(Hoges::list, "list",
                    // ★この場で Hoge の制約を定義する
                    b -> b.constraint(Hoge::text, "text", c -> c.notEmpty()))
                .build();

        printResult(hogesValidator, new Hoges(List.of(
            new Hoge("a"),
            new Hoge("b"),
            new Hoge("")
        )));
        printResult(hogesValidator, new Hoges(List.of(
            new Hoge("a"),
            new Hoge("b"),
            new Hoge("c")
        )));
    }
```

### String やラッパークラスのコレクションの制約をスマートに定義する方法は用意されていない

```java:Hoge.java
package sandbox.yavi;

import java.util.List;

public record Hoge(List<String> textList, List<Integer> numberList) {
}
```

- こんな感じで、 `String` や `Integer` などのラッパークラスのコレクションを持つ項目に対して制約を定義する場合を考える
- 無理やり書くと、以下のようになる

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;

import java.util.List;

public class Main {
    public static void main(String[] args) {
        final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
            .forEach(Hoge::textList, "textList",
                b -> b._string(s -> s, "", c -> c.notEmpty()))
            .forEach(Hoge::numberList, "numberList",
                b -> b._integer(i -> i, "", c -> c.lessThan(50)))
            .build();

        printResult(validator, new Hoge(List.of ("a", "b", ""), List.of(1, 10, 100)));
        printResult(validator, new Hoge(List.of ("a", "b", "c"), List.of(1, 10, 20)));
    }

    private static <T> void printResult(Validator<T> validator, T target) {
        final ConstraintViolations violations = validator.validate(target);
        System.out.println(
            target + " > " + (violations.isValid() ? "valid" : "invalid"));
        violations.stream()
                .map(e -> "  " + e.message())
                .forEach(System.out::println);
    }
}
```

```text:実行結果
Hoge[textList=[a, b, ], numberList=[1, 10, 100]] > invalid
  "textList[2]" must not be empty
  "numberList[2]" must be less than 50
Hoge[textList=[a, b, c], numberList=[1, 10, 20]] > valid
```

- `_string(s -> s, "", ...)` のようにして同じ値をそのまま渡すための記述が必要であったり、項目名を空文字にしてエラーメッセージが整合するようにしていて、やや無理やり感がある
- `String` やラッパークラスのコレクションに対する制約がこのような書き方になることについて Issue が建てられており、以下のような回答がある

> That's expected
> (訳)これは想定通りです。
> 
> [forEach does not work sensibly with primitive list · Issue #301 · making/yavi](https://github.com/making/yavi/issues/301)

- ということで、このような書き方になるのは仕様とのこと
- 特に説明がないが「`String` やプリミティブ型は必ず Value Object でラップすべき」という思想（前提）があるということだろうか？

## Map
```java:Hoge.java
package sandbox.yavi;

public record Hoge(String text) {
}
```

```java:Hoges.java
package sandbox.yavi;

import java.util.Map;

public record Hoges(Map<String, Hoge> map) {
}
```

```java
package sandbox.yavi;

import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;

import java.util.Map;

public class Main {
    public static void main(String[] args) {
        final Validator<Hoge> hogeValidator = ValidatorBuilder.<Hoge>of()
                .constraint(Hoge::text, "text", c -> c.notEmpty())
                .build();

        final Validator<Hoges> hogesValidator = ValidatorBuilder.<Hoges>of()
                .forEach(Hoges::map, "map", hogeValidator)
                .build();

        printResult(hogesValidator, new Hoges(Map.of(
            "a", new Hoge("A"),
            "b", new Hoge("B"),
            "c", new Hoge("")
        )));
        printResult(hogesValidator, new Hoges(Map.of(
            "a", new Hoge("A"),
            "b", new Hoge("B"),
            "c", new Hoge("C")
        )));
    }

    private static <T> void printResult(Validator<T> validator, T target) {
        final ConstraintViolations violations = validator.validate(target);
        System.out.println(
            target + " > " + (violations.isValid() ? "valid" : "invalid"));
        violations.stream()
                .map(e -> "  " + e.message())
                .forEach(System.out::println);
    }
}
```

```text:実行結果
Hoges[map={a=Hoge[text=A], b=Hoge[text=B], c=Hoge[text=]}] > invalid
  "map[2].text" must not be empty
Hoges[map={a=Hoge[text=A], b=Hoge[text=B], c=Hoge[text=C]}] > valid
```

- `Map` も、他のコレクションと同様に `forEach` を使って制約を定義できる
- ただし、制約の検査は `Map` の値に対して行われるものになる
- もし `Map` のキーに対して制約を定義したい場合は、 `keySet` でキーの `Set` を取り出して制約を定義する必要がある

```java:Mapのキーに対して制約を定義する
package sandbox.yavi;

import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;

import java.util.Map;

public class Main {
    public static void main(String[] args) {
        final Validator<Hoges> hogesValidator = ValidatorBuilder.<Hoges>of()
            // ★ keySet を取り出す形にする
            .forEach((Hoges hoges) -> hoges.map().keySet(), "map",
                b -> b._string(s -> s, "key", c -> c.notEmpty()))
            .build();

        printResult(hogesValidator, new Hoges(Map.of(
            "a", new Hoge("A"),
            "", new Hoge("B"),
            "c", new Hoge("C")
        )));
        printResult(hogesValidator, new Hoges(Map.of(
            "a", new Hoge("A"),
            "b", new Hoge("B"),
            "c", new Hoge("C")
        )));
    }

    private static <T> void printResult(Validator<T> validator, T target) {
        final ConstraintViolations violations = validator.validate(target);
        System.out.println(
            target + " > " + (violations.isValid() ? "valid" : "invalid"));
        violations.stream()
                .map(e -> "  " + e.message())
                .forEach(System.out::println);
    }
}
```

```text:実行結果
Hoges[map={c=Hoge[text=C], a=Hoge[text=A], =Hoge[text=B]}] > invalid
  "map[2].key" must not be empty
Hoges[map={c=Hoge[text=C], b=Hoge[text=B], a=Hoge[text=A]}] > valid
```

# カスタムの制約を定義する
```java:Hoge.java
package sandbox.yavi;

public record Hoge(String text) {
}
```

```java:MyConstraint.java
package sandbox.yavi;

import am.ik.yavi.core.CustomConstraint;

public class MyConstraint implements CustomConstraint<String> {

    @Override
    public boolean test(String s) {
        return s.startsWith("H");
    }

    @Override
    public String defaultMessageFormat() {
        return "\"{0}\" must be start with \"H\" but it is \"{1}\"";
    }

    @Override
    public String messageKey() {
        return "my.constraint";
    }
}
```

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;

public class Main {
    public static void main(String[] args) {
        final MyConstraint myConstraint = new MyConstraint();

        final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
                .constraint(Hoge::text, "text", c -> c.predicate(myConstraint))
                .build();

        printResult(validator, new Hoge("Hoge"));
        printResult(validator, new Hoge("Fuga"));
    }

    private static <T> void printResult(Validator<T> validator, T target) {
        final ConstraintViolations violations = validator.validate(target);
        System.out.println(
            target + " > " + (violations.isValid() ? "valid" : "invalid"));
        violations.stream()
                .map(e -> "  " + e.message())
                .forEach(System.out::println);
    }
}
```

```text:実行結果
Hoge[text=Hoge] > valid
Hoge[text=Fuga] > invalid
  "text" must be start with "H" but it is "Fuga"
```

**説明**

```java:MyConstraint.java
import am.ik.yavi.core.CustomConstraint;

public class MyConstraint implements CustomConstraint<String> {
```

- カスタムの制約を定義するには、 `CustomConstraint` インタフェースを実装したクラスを作成する

```java:MyConstraint.java
    @Override
    public boolean test(String s) {
        return s.startsWith("H");
    }

    @Override
    public String defaultMessageFormat() {
        return "\"{0}\" must be start with \"H\" but it is \"{1}\"";
    }

    @Override
    public String messageKey() {
        return "my.constraint";
    }
```

- `test` メソッドには検査対象の値が渡されるので、検査結果を `boolean` で返却する
    - 検査 OK なら `true` を返す
- `defaultMessageFormat` メソッドでは、デフォルトのメッセージフォーマットを返すように実装する
    - 埋め込まれるパラメータの先頭は、検査対象の項目名が埋め込まれる
    - 埋め込まれるパラメータの末尾は、検査対象の実際の値が埋め込まれる
    - デフォルトで埋め込まれるパラメータは「項目名」と「実際の値」の２つなので、 `{0}` が項目名になって `{1}` が実際の値になる
- `messageKey` メソッドでは、メッセージのキーを返すように実装する
    - メッセージの取り扱いについての[詳細は後述](#違反メッセージの構築をカスタマイズする)

```java:Main.java
        final MyConstraint myConstraint = new MyConstraint();

        final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
            .constraint(Hoge::text, "text", c -> c.predicate(myConstraint))
            .build();
```

- 自作の `CustomConstraint` は、制約を定義するときに `predicate` メソッドの引数に渡すことで使用できる

## メッセージに埋め込むパラメータを追加する
```java:MyConstraint.java
package sandbox.yavi;

import am.ik.yavi.core.CustomConstraint;

public class MyConstraint implements CustomConstraint<String> {

    @Override
    public boolean test(String s) {
        return s.startsWith("H");
    }

    @Override
    public Object[] arguments(String violatedValue) {
        return new Object[] { "Foo", "Bar" };
    }

    @Override
    public String defaultMessageFormat() {
        return "0={0}, 1={1}, 2={2}, 3={3}, 4={4}";
    }

    @Override
    public String messageKey() {
        return "my.constraint";
    }
}
```

```text:実行結果
Hoge[text=Hoge] > valid
Hoge[text=Fuga] > invalid
  0=text, 1=Foo, 2=Bar, 3=Fuga, 4={4}
```

- `arguments` メソッドをオーバーライドすることで、メッセージに埋め込むパラメータを追加できる
- 先頭の `{0}` は項目名、末尾の `{3}` は実際の値で変わらない
- `arguments` が返したパラメータは、これらの間に差し込まれた形でメッセージに埋め込まれる（`{1}` と `{2}`）


# 複数項目をまたがる制約
```java:Hoge.java
package sandbox.yavi;

public record Hoge(int from, int to) {
}
```

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;

public class Main {
    public static void main(String[] args) {
        final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
                .constraintOnTarget(hoge -> hoge.from() < hoge.to(),
                        "to", "to.isGreaterThanFrom",
                        "\"to\" must be greater than \"from\"")
                .build();

        printResult(validator, new Hoge(10, 1));
        printResult(validator, new Hoge(1, 10));
    }

    private static <T> void printResult(Validator<T> validator, T target) {
        final ConstraintViolations violations = validator.validate(target);
        System.out.println(
            target + " > " + (violations.isValid() ? "valid" : "invalid"));
        violations.stream()
                .map(e -> "  " + e.message())
                .forEach(System.out::println);
    }
}
```

```text:実行結果
Hoge[from=10, to=1] > invalid
  "to" must be greater than "from"
Hoge[from=1, to=10] > valid
```

- `constraintOnTarget` を使用すると、複数項目にまたがった制約を定義できる
- 第一引数に、満たされるべき制約を検証する関数を渡す
    - 関数には検査対象のオブジェクトが渡される
    - 検査が OK かどうかを boolean で返す（検査 OK なら `true` を返す）
- 第二引数には、項目の名前を渡す
- 第三引数には、メッセージのキーを渡す
- 第四引数には、デフォルトのメッセージを渡す

## カスタムの制約を渡す
```java:MyConstraint.java
package sandbox.yavi;

import am.ik.yavi.core.CustomConstraint;

public class MyConstraint implements CustomConstraint<Hoge> {

    @Override
    public boolean test(Hoge hoge) {
        return hoge.from() < hoge.to();
    }

    @Override
    public String defaultMessageFormat() {
        return "\"to\" must be greater than \"from\"";
    }

    @Override
    public String messageKey() {
        return "my.constraint";
    }
}
```

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;

public class Main {
    public static void main(String[] args) {
        final MyConstraint myConstraint = new MyConstraint();

        final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
                // ★ カスタムの制約を指定する
                .constraintOnTarget(myConstraint, "to")
                .build();

        printResult(validator, new Hoge(10, 1));
        printResult(validator, new Hoge(1, 10));
    }

    private static <T> void printResult(Validator<T> validator, T target) {
        final ConstraintViolations violations = validator.validate(target);
        System.out.println(
            target + " > " + (violations.isValid() ? "valid" : "invalid"));
        violations.stream()
                .map(e -> "  " + e.message())
                .forEach(System.out::println);
    }
}
```

```text:実行結果
Hoge[from=10, to=1] > invalid
  "to" must be greater than "from"
Hoge[from=1, to=10] > valid
```

- `constraintOnTarget` の第一引数にカスタムの制約を渡すことができる

# 特定の条件のときだけ制約を有効にする
```java:Hoge.java
package sandbox.yavi;

public record Hoge(boolean bool, String text) {
}
```

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;

public class Main {
    public static void main(String[] args) {
        final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
                .constraintOnCondition((hoge, group) -> hoge.bool(),
                    b -> b.constraint(Hoge::text, "text", c -> c.notEmpty()))
                .build();

        printResult(validator, new Hoge(false, ""));
        printResult(validator, new Hoge(true, ""));
    }

    private static <T> void printResult(Validator<T> validator, T target) {
        final ConstraintViolations violations = validator.validate(target);
        System.out.println(
            target + " > " + (violations.isValid() ? "valid" : "invalid"));
        violations.stream()
                .map(e -> "  " + e.message())
                .forEach(System.out::println);
    }
}
```

```text:実行結果
Hoge[bool=false, text=] > valid
Hoge[bool=true, text=] > invalid
  "text" must not be empty
```

- `constraintOnCondition` を使うことで、特定の条件が満たされたときだけ検証する制約を定義できる
- 第一引数の関数が `true` を返したときだけ、第二引数で定義した制約が有功になる

# 制約をグルーピングする
```java:Hoge.java
package sandbox.yavi;

public record Hoge(String hoge, String fuga, String piyo) {
}
```

```java:Group.java
package sandbox.yavi;

import am.ik.yavi.core.ConstraintGroup;

public enum Group implements ConstraintGroup {
    HOGE,
    FUGA,
}
```

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolation;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;

public class Main {
    public static void main(String[] args) {
        final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
                .constraintOnGroup(Group.HOGE,
                    b -> b.constraint(Hoge::hoge, "hoge", c -> c.notEmpty()))
                .constraintOnGroup(Group.FUGA,
                    b -> b.constraint(Hoge::fuga, "fuga", c -> c.notEmpty()))
                .constraint(Hoge::piyo, "piyo", c -> c.notEmpty())
                .build();

        final Hoge hoge = new Hoge("", "", "");

        printViolations(validator.validate(hoge, Group.HOGE));
        printViolations(validator.validate(hoge, Group.FUGA));
        printViolations(validator.validate(hoge));
    }

    private static void printViolations(ConstraintViolations violations) {
        System.out.println("valid=" + violations.isValid());
        violations.stream()
                .map(ConstraintViolation::message)
                .forEach(System.out::println);
    }
}
```

```text:実行結果
valid=false
"piyo" must not be empty
"hoge" must not be empty
valid=false
"piyo" must not be empty
"fuga" must not be empty
valid=false
"piyo" must not be empty
```

**説明**

```java:Group.java
import am.ik.yavi.core.ConstraintGroup;

public enum Group implements ConstraintGroup {
    HOGE,
    FUGA,
}
```

- グループを定義するために、 `ConstraintGroup` インタフェースを実装した `enum` を定義する

```java:Main.java
        final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
                .constraintOnGroup(Group.HOGE,
                    b -> b.constraint(Hoge::hoge, "hoge", c -> c.notEmpty()))
                .constraintOnGroup(Group.FUGA,
                    b -> b.constraint(Hoge::fuga, "fuga", c -> c.notEmpty()))
                .constraint(Hoge::piyo, "piyo", c -> c.notEmpty())
                .build();
```

- `constraintOnGroup` で、グループを指定して制約を定義できる
    - 第一引数に、先ほど定義したグループを表す `enum` を渡す
    - 第二引数で制約を定義する
- グループを指定していない制約は、**DEFAULT グループに所属しているものとして扱われる**

```java:Main.java
        printViolations(validator.validate(hoge, Group.HOGE));
        printViolations(validator.validate(hoge, Group.FUGA));
        printViolations(validator.validate(hoge));
```

- `validate` メソッドの第二引数でグループを指定する
- グループを指定して検証を行うと、そのグループか DEFAULT グループに所属している制約が検証される
- グループを指定せずに検証を行うと、 DEFAULT グループに所属している制約だけが検証される

<table>
  <tr>
    <th rowspan="2" colspan="2"></th>
    <th colspan="2">validate時のグループ指定</th>
  </tr>
  <tr>
    <th>指定あり</th>
    <th>指定なし</th>
  </tr>
  <tr>
    <th rowspan="2">制約定義時の<br>グループ指定</th>
    <th>指定あり</th>
    <td>○</td>
    <td>×</td>
  </tr>
  <tr>
    <th>指定なし</th>
    <td>○</td>
    <td>○</td>
  </tr>
</table>

※○は検証が実施される

# 違反メッセージを上書きする
```java:Hoge.java
package sandbox.yavi;

public record Hoge(String text) {
}
```

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;

public class Main {
    public static void main(String[] args) {
        final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
            .constraint(Hoge::text, "text",
                c -> c.greaterThanOrEqual(5).message("{0} は5文字以上です")
                    .startsWith("H").message("{0} は H 始まりです"))
            .build();

        printResult(validator, new Hoge("hoge"));
        printResult(validator, new Hoge("Hoge"));
    }

    private static <T> void printResult(Validator<T> validator, T target) {
        final ConstraintViolations violations = validator.validate(target);
        System.out.println(
            target + " > " + (violations.isValid() ? "valid" : "invalid"));
        violations.stream()
                .map(e -> "  " + e.message())
                .forEach(System.out::println);
    }
}
```

```text:実行結果
Hoge[text=hoge] > invalid
  text は5文字以上です
  text は H 始まりです
Hoge[text=Hoge] > invalid
  text は5文字以上です
```

- 制約を定義した直後に `message` メソッドを続けることで、デフォルトのメッセージを上書きできる

# 違反メッセージの構築をカスタマイズする
```java:Hoge.java
package sandbox.yavi;

public record Hoge(String text) {
}
```

```java:MyMessageFormatter.java
package sandbox.yavi;

import am.ik.yavi.message.MessageFormatter;

import java.util.Locale;

public class MyMessageFormatter implements MessageFormatter {
    @Override
    public String format(
        String messageKey, String defaultMessageFormat, Object[] args, Locale locale) {
        return "めっせーじ";
    }
}
```

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;

public class Main {
    public static void main(String[] args) {
        final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
            .messageFormatter(new MyMessageFormatter())
            .constraint(Hoge::text, "text", c -> c.notEmpty())
            .build();

        printResult(validator, new Hoge(""));
    }

    private static <T> void printResult(Validator<T> validator, T target) {
        final ConstraintViolations violations = validator.validate(target);
        System.out.println(
            target + " > " + (violations.isValid() ? "valid" : "invalid"));
        violations.stream()
                .map(e -> "  " + e.message())
                .forEach(System.out::println);
    }
}
```

```text:実行結果
Hoge[text=] > invalid
  めっせーじ
```

**説明**

```java:MyMessageFormatter.java
public class MyMessageFormatter implements MessageFormatter {
```

- 違反メッセージの構築をカスタマイズするには、 `MessageFormatter` インタフェースを実装したクラスを作成する

```java:MyMessageFormatter.java
    @Override
    public String format(
        String messageKey, String defaultMessageFormat, Object[] args, Locale locale) {
        return "めっせーじ";
    }
```

- `format` メソッドをオーバーライドして、違反メッセージを返却するように実装する
- 引数として、以下の情報が渡される
    - `messageKey`: メッセージを識別するキー
    - `defaultMessageFormat`: デフォルトのメッセージ
    - `args`: メッセージに埋め込むパラメータ
        - 先頭は項目名、末尾は検査対象の実際の値
    - `locale`: ロケール

```java:Main.java
        final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
                .messageFormatter(new MyMessageFormatter())
                .constraint(Hoge::text, "text", c -> c.notEmpty())
                .build();
```

- 作成した自作の `MessageFormatter` は、 `Validator` を作成するときに `messageFormatter` で設定することで適用できる
- デフォルトの `MessageFormatter` は `SimpleMessageFormatter` というクラス
    - このクラスは、単に `java.text.MessageFormat` を使って `defaultMessageFormat` をフォーマットするだけになっている（`messageKey` は使わない）
- `ResourceBundle` を使ってメッセージの解決を実装する例が[公式ガイドに記載されている](https://yavi.ik.am/#message-formatter)

```java:ResourceBundleでメッセージを解決する例
import java.text.MessageFormat;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;

import am.ik.yavi.message.MessageFormatter;

public enum ResourceBundleMessageFormatter implements MessageFormatter {
    SINGLETON;

    @Override
    public String format(
        String messageKey, String defaultMessageFormat, Object[] args, Locale locale) {
        ResourceBundle resourceBundle = ResourceBundle.getBundle("messages", locale);
        String format;
        try {
            format = resourceBundle.getString(messageKey);
        }
        catch (MissingResourceException e) {
            format = defaultMessageFormat;
        }
        try {
            String target = resourceBundle.getString((String) args[0] /* field name */);
            args[0] = target;
        }
        catch (MissingResourceException e) {
        }
        return new MessageFormat(format, locale).format(args);
    }
}
```

# 違反情報と検査対象の値をひとまとめにして扱う
```java:Hoge.java
package sandbox.yavi;

public record Hoge(String text) {
}
```

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolation;
import am.ik.yavi.core.Validated;
import am.ik.yavi.core.Validator;

public class Main {
    public static void main(String[] args) {
        final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
                .constraint(Hoge::text, "text", c -> c.notEmpty())
                .build();

        final Hoge hoge = new Hoge("");

        final Validated<Hoge> hogeValidated = validator.applicative().validate(hoge);

        System.out.println("valid = " + hogeValidated.isValid());
        final Hoge result = hogeValidated
                .mapError(ConstraintViolation::message)
                .orElseThrow(messages -> new RuntimeException(messages.toString()));
        System.out.println("result = " + result);
    }
}
```

```text:実行結果
valid = false
Exception in thread "main" java.lang.RuntimeException: ["text" must not be empty]
	at sandbox.yavi.Main.lambda$main$1(Main.java:22)
	at am.ik.yavi.fn.Validation.orElseThrow(Validation.java:131)
	at sandbox.yavi.Main.main(Main.java:22)
```

**説明**

```java:Main.java
        final Validated<Hoge> hogeValidated = validator.applicative().validate(hoge);
```

- `Validator` の `applicative` メソッドを使用すると、 `ApplicativeValidator` というクラスが返される
- このクラスの `validate` メソッドを使って検証を行うと、違反情報と検査対象の値をワンセットにした `Validated` オブジェクトが返されるようになる

```java:Main.java
        final Hoge result = hogeValidated
                .mapError(ConstraintViolation::message)
                .orElseThrow(messages -> new RuntimeException(messages.toString()));
```

- `mapError` や `map` など、オブジェクト内の違反情報または検査対象の値に対して関数を適用させるためのメソッドが用意されている
- たぶん、関数型プログラミングっぽい実装をするのに役立つっぽい
- `value` や `errors` メソッドで検査対象の値や違反情報を取り出すことも可能
    - `value` で値が取れるのは、違反がない場合のみ
    - 違反がある状態で `value` を呼ぶと例外がスローされる

## 検査済みの値(Validated)を組み合わせる
```java:Text.java
package sandbox.yavi;

public record Text(String value) {
}
```

```java:Number.java
package sandbox.yavi;

public record Number(int value) {
}
```

```java:Hoge.java
package sandbox.yavi;

public record Hoge(Text text, Number number) {
}
```

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ApplicativeValidator;
import am.ik.yavi.core.ConstraintViolation;
import am.ik.yavi.core.Validated;
import am.ik.yavi.fn.Validation;

public class Main {
    public static void main(String[] args) {
        final ApplicativeValidator<Text> textValidator =
                ValidatorBuilder.<Text>of()
                .constraint(Text::value, "text", c -> c.notEmpty())
                .build()
                .applicative();

        final ApplicativeValidator<Number> numberValidator =
                ValidatorBuilder.<Number>of()
                .constraint(Number::value, "number", c -> c.lessThan(10))
                .build()
                .applicative();

        final Validated<Text> textValidated =
            textValidator.validate(new Text(""));
        final Validated<Number> numberValidated =
            numberValidator.validate(new Number(100));

        final Validation<ConstraintViolation, Hoge> hogeValidation =
            textValidated.combine(numberValidated)
                         .apply(Hoge::new);

        System.out.println("valid = " + hogeValidation.isValid());
        hogeValidation
                .mapError(ConstraintViolation::message)
                .errors()
                .forEach(System.out::println);
    }
}
```

```text:実行結果
valid = false
"text" must not be empty
"number" must be less than 10
```

**説明**

```java:Main.java
        final Validated<Text> textValidated =
            textValidator.validate(new Text(""));
        final Validated<Number> numberValidated =
            numberValidator.validate(new Number(100));

        final Validation<ConstraintViolation, Hoge> hogeValidation =
            textValidated.combine(numberValidated)
                         .apply(Hoge::new);
```

- 2つ以上の `Validated` を `combine` メソッドで組み合わせることができる
- 組み合わせた値(`Text` と `Number`) を元にして、新しい値(`Hoge`)を構築できる
    - `apply` メソッドで元の値から新しい値に変換する方法を指定する
- 組み合わせた後の値(`Validation`)には、それまでに組み合わせた全ての検証結果の情報が含まれている
    - 最終的に違反がゼロだったら、組み合わせ後の新しい値(`Hoge`)を取得できる
- 3つ以上の値を一度に組み合わせたい場合は、 `Validations` の `combine` メソッドが利用できる

```java:3つ以上を組み合わせる場合
        final Combining3<ConstraintViolation, Text, Number, Bool> combine
                = Validations.combine(textValidated, numberValidated, boolValidated);
```

- `Validations` には、最大 16 個の `Validation` を受け取るためのメソッドが用意されている

:::note info
`Validated` は `Validation` のサブクラスなので、 `Validation` を渡せる場所には `Validated` も渡すことができる。

```plantuml
abstract class Validation {}
class Validated extends Validation {}
```
:::

# 単一値のバリデーション
```java:Main.java
package sandbox.yavi;

import am.ik.yavi.arguments.StringValidator;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.core.Validated;
import am.ik.yavi.core.ValueValidator;

public class Main {
    public static void main(String[] args) {
        final StringValidator<String> textValidator
            = StringValidatorBuilder.of("text", c -> c.notEmpty()).build();

        printResult(textValidator, "");
        printResult(textValidator, "hoge");
    }

    private static <T, X> void printResult(ValueValidator<T, X> validator, T target) {
        final Validated<X> validated = validator.validate(target);
        System.out.println(target + " > " + (validated.isValid() ? "valid" : "invalid"));
        if (!validated.isValid()) {
            validated.errors()
                    .stream()
                    .map(e -> "  " + e.message())
                    .forEach(System.out::println);
        }
    }
}
```

```text:実行結果
 > invalid
  "text" must not be empty
hoge > valid
```

**説明**

```java:Main.java
        final StringValidator<String> textValidator
            = StringValidatorBuilder.of("text", c -> c.notEmpty()).build();
```

- 単一値用のバリデータを定義できる
- 検査対象の値の型ごとにバリデータとビルダーが用意されている
- 組み込みで以下のバリデータが用意されている
    - プリミティブ型のラッパー用
        - `BooleanValidator`
        - `DoubleValidator`
        - `FloatValidator`
        - `IntegerValidator`
        - `LongValidator`
        - `ShortValidator`
    - Date&Time API 用
        - `InstantValidator`
        - `LocalDateTimeValidator`
        - `LocalDateValidator`
        - `LocalTimeValidator`
        - `OffsetDateTimeValidator`
        - `YearMonthValidator`
        - `YearValidator`
        - `ZonedDateTimeValidator`
    - その他基本的な型用
        - `BigDecimalValidator`
        - `BigIntegerValidator`
        - `EnumValidator`
        - `ObjectValidator`
        - `StringValidator`

```java:Main.java
        final Validated<String> textValidated = textValidator.validate("");
```

- 検証結果が `Validated` として返される

## 検査 OK の場合に Value Object に変換する

```java:Hoge.java
package sandbox.yavi;

public record Hoge(String text) {
}
```

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.arguments.StringValidator;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.core.Validated;

public class Main {

    public static void main(String[] args) {
        final StringValidator<Hoge> hogeValidator = StringValidatorBuilder
            .of("text", c -> c.notEmpty())
            .build()
            .andThen(Hoge::new);

        final Validated<Hoge> hogeValidated = hogeValidator.validate("test");

        System.out.println("valid=" + hogeValidated.isValid());
        final Hoge hoge = hogeValidated.value();
        System.out.println("hoge=" + hoge);
    }
}
```

```text:実行結果
valid=true
hoge=Hoge[text=test]
```

**説明**

```java:Main.java
        final StringValidator<Hoge> hogeValidator = StringValidatorBuilder
            .of("text", c -> c.notEmpty())
            .build()
            .andThen(Hoge::new);
```

- 検証 OK だった場合に Value Object へ変換する処理を `andThen` で登録できる

## コレクションの検査

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.arguments.StringValidator;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.core.ConstraintViolation;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validated;

import java.util.List;

public class Main {

    public static void main(String[] args) {
        final StringValidator<String> stringValidator = StringValidatorBuilder
            .of("text", c -> c.notEmpty())
            .build();

        final Validated<List<String>> listValidated
            = stringValidator.liftList().validate(List.of("a", "", "c"));
        printViolations(listValidated.errors());
    }

    private static void printViolations(ConstraintViolations violations) {
        System.out.println("valid=" + violations.isValid());
        violations.stream()
                .map(ConstraintViolation::message)
                .forEach(System.out::println);
    }
}
```

```text:実行結果
valid=false
"text[1]" must not be empty
```

**説明**

```java:Main.java
        final Validated<List<String>> listValidated
            = stringValidator.liftList().validate(List.of("a", "", "c"));
```

- 単一値のバリデータの `liftList` を使用することで、 `List` のバリデータへ変換できる
- `List` 以外の他のコレクション用に `liftSet`, `liftCollection(Supplier)` が用意されている
    - `liftOptional` というのもある

# 引数の検証
```java:Hoge.java
package sandbox.yavi;

public record Hoge(String text, int number) {
}
```

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.arguments.Arguments1;
import am.ik.yavi.arguments.Arguments2;
import am.ik.yavi.arguments.Arguments2Validator;
import am.ik.yavi.builder.ArgumentsValidatorBuilder;
import am.ik.yavi.core.ConstraintViolation;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validated;

public class Main {
    public static void main(String[] args) {
        final Arguments2Validator<String, Integer, Hoge> validator =
            ArgumentsValidatorBuilder.of(Hoge::new)
                .builder(b -> b
                    ._string(Arguments1::arg1, "text", c -> c.notEmpty())
                    ._integer(Arguments2::arg2, "number", c -> c.lessThan(10))
                ).build();

        final Validated<Hoge> hogeValidated = validator.validate("", 100);

        printViolations(hogeValidated.errors());
    }

    private static void printViolations(ConstraintViolations violations) {
        System.out.println("valid=" + violations.isValid());
        violations.stream()
                .map(ConstraintViolation::message)
                .forEach(System.out::println);
    }
}
```

```text:実行結果
valid=false
"text" must not be empty
"number" must be less than 10
```

**説明**

- コンストラクタやファクトリメソッドのように、何らかのオブジェクトを生成するメソッドの引数を検証するための機能が用意されている
- この機能を利用すると、不正な状態のオブジェクトを作ることなく検査を実施できる
    - 既に作成されたオブジェクトにだけ検査が行えるという方式だと、不正な状態のオブジェクトが生成できるようにしないといけない
    - もしくは、検査のためだけの入れ物クラスが必要になる

```java:Main.java
        final Arguments2Validator<String, Integer, Hoge> validator =
            ArgumentsValidatorBuilder.of(Hoge::new)
                .builder(b -> b
                    ._string(Arguments1::arg1, "text", c -> c.notEmpty())
                    ._integer(Arguments2::arg2, "number", c -> c.lessThan(10))
                ).build();
                
        final Validated<Hoge> hogeValidated = validator.validate("", 100);
```

- `ArgumentsValidatorBuilder` でビルダーを定義する
    - `of` メソッドに検査対象のメソッド or コンストラクタのメソッド参照を渡す
    - `builder` メソッドに渡す関数の中で、制約を定義する
        - `_string` のように、各引数の型に合わせたメソッドで制約を定義する
        - 第一引数には `Arguments1::arg1` のように引数の順番に合った `ArgumentsN` 型のメソッド参照を利用する
- 作成したバリデータの `validate` メソッドを使用すると、生成されるオブジェクトと違反情報を保持した `Validated` オブジェクトが返される
    - 実際に目的のオブジェクト(`Hoge`)が生成されるのは違反がない場合に限られる

## 単一値のバリデータを用いて簡潔に定義する

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.arguments.Arguments2Validator;
import am.ik.yavi.arguments.ArgumentsValidators;
import am.ik.yavi.arguments.IntegerValidator;
import am.ik.yavi.arguments.StringValidator;
import am.ik.yavi.builder.IntegerValidatorBuilder;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.core.ConstraintViolation;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validated;

public class Main {

    public static void main(String[] args) {
        final StringValidator<String> textValidator = StringValidatorBuilder
                .of("text", c -> c.notEmpty()).build();
        final IntegerValidator<Integer> numberValidator = IntegerValidatorBuilder
                .of("number", c -> c.lessThan(10)).build();

        final Arguments2Validator<String, Integer, Hoge> hogeValidator
            = ArgumentsValidators.split(textValidator, numberValidator)
                                 .apply(Hoge::new);

        final Validated<Hoge> hogeValidated = hogeValidator.validate("", 100);

        printViolations(hogeValidated.errors());
    }

    private static void printViolations(ConstraintViolations violations) {
        System.out.println("valid=" + violations.isValid());
        violations.stream()
                .map(ConstraintViolation::message)
                .forEach(System.out::println);
    }
}
```

```text:実行結果
valid=false
"text" must not be empty
"number" must be less than 10
```

**説明**

```java:Main.java
        final Arguments2Validator<String, Integer, Hoge> hogeValidator
            = ArgumentsValidators.split(textValidator, numberValidator)
                                 .apply(Hoge::new);
```

- 単一値のバリデータを `ArgumentsValidators` の `split` に渡すことで、引数検査のバリデータをより簡潔に定義できるようになる

## Value Object が引数の場合

```java:Text.java
package sandbox.yavi;

public record Text(String value) {
}
```

```java:Number.java
package sandbox.yavi;

public record Number(int value) {
}
```

```java:Hoge.java
package sandbox.yavi;

public record Hoge(Text text, Number number) {
}
```

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.arguments.Arguments2Validator;
import am.ik.yavi.arguments.ArgumentsValidators;
import am.ik.yavi.arguments.IntegerValidator;
import am.ik.yavi.arguments.StringValidator;
import am.ik.yavi.builder.IntegerValidatorBuilder;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.core.ConstraintViolation;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validated;

public class Main {

    public static void main(String[] args) {
        final StringValidator<Text> textValidator = StringValidatorBuilder
                .of("text", c -> c.notEmpty())
                .build()
                .andThen(Text::new);
        final IntegerValidator<Number> numberValidator = IntegerValidatorBuilder
                .of("number", c -> c.lessThan(10))
                .build()
                .andThen(Number::new);

        final Arguments2Validator<String, Integer, Hoge> hogeValidator
            = ArgumentsValidators.split(textValidator, numberValidator)
                                 .apply(Hoge::new);

        final Validated<Hoge> hogeValidated = hogeValidator.validate("", 100);

        printViolations(hogeValidated.errors());
    }

    private static void printViolations(ConstraintViolations violations) {
        System.out.println("valid=" + violations.isValid());
        violations.stream()
                .map(ConstraintViolation::message)
                .forEach(System.out::println);
    }
}
```

```text:実行結果
valid=false
"text" must not be empty
"number" must be less than 10
```

**説明**

```java:Hoge.java
public record Hoge(Text text, Number number) {
```

- 検査対象の引数が Value Object の場合の実装方法

```java:Main.java
        final StringValidator<Text> textValidator = StringValidatorBuilder
                .of("text", c -> c.notEmpty())
                .build()
                .andThen(Text::new);
        final IntegerValidator<Number> numberValidator = IntegerValidatorBuilder
                .of("number", c -> c.lessThan(10))
                .build()
                .andThen(Number::new);

        final Arguments2Validator<String, Integer, Hoge> hogeValidator
            = ArgumentsValidators.split(textValidator, numberValidator)
                                 .apply(Hoge::new);
```

- 単一値のバリデータを `andThen` で Value Object へ変換できるようにする

### 通常の Validator を元にする

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.arguments.Arguments2Validator;
import am.ik.yavi.arguments.ArgumentsValidators;
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolation;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validated;
import am.ik.yavi.core.Validator;

public class Main {

    public static void main(String[] args) {
        final Validator<Text> textValidator = ValidatorBuilder.<Text>of()
                .constraint(Text::value, "text", c -> c.notEmpty())
                .build();

        final Validator<Number> numberValidator = ValidatorBuilder.<Number>of()
                .constraint(Number::value, "number", c -> c.lessThan(10))
                .build();

        final Arguments2Validator<Text, Number, Hoge> hogeValidator
                = ArgumentsValidators.split(
                        textValidator.applicative(), numberValidator.applicative())
                .apply(Hoge::new);

        final Validated<Hoge> hogeValidated =
            hogeValidator.validate(new Text(""), new Number(50));

        printViolations(hogeValidated.errors());
    }

    private static void printViolations(ConstraintViolations violations) {
        System.out.println("valid=" + violations.isValid());
        violations.stream()
                .map(ConstraintViolation::message)
                .forEach(System.out::println);
    }
}
```

```text:実行結果
valid=false
"text" must not be empty
"number" must be less than 10
```

**説明**

```java:Main.java
        final Validator<Text> textValidator = ValidatorBuilder.<Text>of()
                .constraint(Text::value, "text", c -> c.notEmpty())
                .build();

        final Validator<Number> numberValidator = ValidatorBuilder.<Number>of()
                .constraint(Number::value, "number", c -> c.lessThan(10))
                .build();

        final Arguments2Validator<Text, Number, Hoge> hogeValidator
                = ArgumentsValidators.split(
                        textValidator.applicative(), numberValidator.applicative())
                .apply(Hoge::new);
```

- 単一値のバリデータではなく、 `ValidatorBuilder` で作られた通常のバリデータをもとに引数検査のバリデータを生成する場合の方法
- `applicative` で `ApplicativeValidator` にすると `split` メソッドへ渡せるようになる

# 生成対象のオブジェクトの元となるオブジェクトを検査対象にする

```java:Text.java
package sandbox.yavi;

public record Text(String value) {
}
```

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.arguments.Arguments1Validator;
import am.ik.yavi.arguments.StringValidator;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.core.ConstraintViolation;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validated;

import java.util.Map;

public class Main {

    public static void main(String[] args) {
        final StringValidator<Text> textValidator = StringValidatorBuilder
            .of("text", c -> c.notEmpty())
            .build()
            .andThen(Text::new);

        final Arguments1Validator<Map<String, String>, Text> sourceTextValidator =
                textValidator.compose(src -> src.get("text"));

        final Map<String, String> source = Map.of("text", "");
        final Validated<Text> textValidated = sourceTextValidator.validate(source);

        printViolations(textValidated.errors());
    }

    private static void printViolations(ConstraintViolations violations) {
        System.out.println("valid=" + violations.isValid());
        violations.stream()
                .map(ConstraintViolation::message)
                .forEach(System.out::println);
    }
}
```

```text:実行結果
valid=false
"text" must not be empty
```

**説明**

```java:Main.java
        final Map<String, String> source = Map.of("text", "");
        final Validated<Text> textValidated = sourceTextValidator.validate(source);
```

- 最終的な生成対象のオブジェクトは `Text` だが、その元となるオブジェクト(ソース)として `Map` を使用している
- `validate` にはソースとなる `Map` を渡して検査を行い、 OK であれば `Text` で結果を取得できる形で `Validated` を受け取っている
- 例えば Web アプリなどでは `HttpServletRequest` からパラメータを取得してドメインオブジェクトを構築する、といった流れが考えられる
- この機能は、そういった場面で活用できる

```java:Main.java
        final Arguments1Validator<Map<String, String>, Text> sourceTextValidator =
                textValidator.compose(src -> src.get("text"));
```

- ソースオブジェクトから検査対象の値を取り出す関数を、元となる単一値バリデータの `compose` メソッドに渡す

## バリデータを組み合わせる
```java:Text.java
package sandbox.yavi;

public record Text(String value) {
}
```

```java:Number.java
package sandbox.yavi;

public record Number(int value) {
}
```

```java:Hoge.java
package sandbox.yavi;

public record Hoge(Text text, Number number) {
}
```

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.arguments.Arguments1Validator;
import am.ik.yavi.arguments.ArgumentsValidators;
import am.ik.yavi.builder.IntegerValidatorBuilder;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.core.ConstraintViolation;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validated;

import java.util.Map;

public class Main {

    public static void main(String[] args) {
        final Arguments1Validator<Map<String, String>, Text> sourceTextValidator =
            StringValidatorBuilder
                .of("text", c -> c.notEmpty())
                .build()
                .andThen(Text::new)
                .compose(src -> src.get("text"));

        final Arguments1Validator<Map<String, String>, Number> sourceNumberValidator =
            IntegerValidatorBuilder
                .of("number", c -> c.lessThan(10))
                .build()
                .andThen(Number::new)
                .compose(src -> Integer.valueOf(src.get("number")));


        final Map<String, String> source = Map.of(
            "text", "",
            "number", "50"
        );

        final Arguments1Validator<Map<String, String>, Hoge> sourceHogeValidator
                = ArgumentsValidators
                    .combine(sourceTextValidator, sourceNumberValidator)
                    .apply(Hoge::new);

        final Validated<Hoge> hogeValidated = sourceHogeValidator.validate(source);

        printViolations(hogeValidated.errors());
    }

    private static void printViolations(ConstraintViolations violations) {
        System.out.println("valid=" + violations.isValid());
        violations.stream()
                .map(ConstraintViolation::message)
                .forEach(System.out::println);
    }
}
```

```text:実行結果
valid=false
"text" must not be empty
"number" must be less than 10
```

**説明**

```java:Main.java
        final Arguments1Validator<Map<String, String>, Hoge> sourceHogeValidator
                = ArgumentsValidators
                    .combine(sourceTextValidator, sourceNumberValidator)
                    .apply(Hoge::new);
```

- 複数のソースを検査するバリデータは、 `ArgumentsValidators` の `combine` で1つにまとめることができる



# Validator と ValueValidator

途中から `Validated` とか単一値のバリデータが出てきて各クラスの関係性が分かりにくくなったので整理する。

```plantuml
scale 0.8

class Validator<T> {
    validate(target: T): ConstraintViolations
    applicative(): ApplicativeValidator<T>
}

class ConstraintViolations {
    violations(): List<ConstraintViolation>
}
class ConstraintViolation {}


interface ValueValidator<T, X> {
    validate(target: T): Validated<X>
}
interface ApplicativeValidator<T> extends ValueValidator {}
interface Arguments1Validator<T> extends ValueValidator {}
class StringValidator implements Arguments1Validator {}


abstract class Validation<E, T> {
    value(): T
    errors(): List<E>
}
class Validated<T> extends Validation {
    errors(): List<ConstraintViolation>
}

Validator .> ApplicativeValidator
Validator ..> ConstraintViolations
ConstraintViolations o-- "0..*" ConstraintViolation
ValueValidator ...> Validated
Validated o-- "0..*" ConstraintViolation
```

- 基本となる `Validator` は、単に検査対象の値を受け取って違反情報(`ConstraintViolations`)を返す
- 一方で、 `ValueValidator` は検査対象の値と違反情報をワンセットにした `Validated` を返す
- `applicative` で作成する `ApplicativeValidator` や単一値バリデータである `StringValidator` などは `ValueValidator` のサブタイプに該当する

# アノテーションプロセッサー
```gradle:build.gradle
dependencies {
    implementation "am.ik.yavi:yavi:0.14.0"
    annotationProcessor "am.ik.yavi:yavi:0.14.0" // ★追加
}
```

**IntelliJ の設定**

- [File] > [Settings] で設定画面を開く
- [Build, Execution, Deployment] > [Compiler] > [Annotation Processors] を選択
- [Enable annotation processing] のチェックを **オン** にする

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/9a7fde76-a7d8-17d6-dd46-7133ef7a51fc.png)



```java:Hoge.java
package sandbox.yavi;

import am.ik.yavi.meta.ConstraintTarget;

public class Hoge {
    private final String text;
    private final int number;

    public Hoge(String text, int number) {
        this.text = text;
        this.number = number;
    }

    @ConstraintTarget
    public String getText() {
        return text;
    }

    @ConstraintTarget
    public int getNumber() {
        return number;
    }
}
```

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolation;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;

public class Main {

    public static void main(String[] args) {
        final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
                .constraint(_HogeMeta.TEXT, c -> c.notEmpty())
                .constraint(_HogeMeta.NUMBER, c -> c.lessThan(10))
                .build();

        final ConstraintViolations violations = validator.validate(new Hoge("", 50));

        printViolations(violations);
    }

    private static void printViolations(ConstraintViolations violations) {
        System.out.println("valid=" + violations.isValid());
        violations.stream()
                .map(ConstraintViolation::message)
                .forEach(System.out::println);
    }
}
```

```text:実行結果
valid=false
"text" must not be empty
"number" must be less than 10
```

**説明**

```gradle:build.gradle
dependencies {
    implementation "am.ik.yavi:yavi:0.14.0"
    annotationProcessor "am.ik.yavi:yavi:0.14.0" // ★追加
}
```

- アノテーションプロセッサーを有効にするため、 `annotationProcessor` の指定を追加する
- IDE を使っている場合は、 IDE でもアノテーションプロセッサーが有功になるように設定を変更する

```java:Hoge.java
    @ConstraintTarget
    public String getText() {
        return text;
    }

    @ConstraintTarget
    public int getNumber() {
        return number;
    }
```

- 検査対象となるプロパティの Getter に `@ConstraintTarget` を設定する

```java:_HogeMeta.java
package sandbox.yavi;

// Generated at 2024-06-23T09:53:55.930424700+09:00
public class _HogeMeta {
  
	public static final am.ik.yavi.meta.StringConstraintMeta<sandbox.yavi.Hoge> TEXT
         = new am.ik.yavi.meta.StringConstraintMeta<sandbox.yavi.Hoge>() {

		@Override
		public String name() {
			return "text";
		}

		@Override
		public java.util.function.Function<sandbox.yavi.Hoge, java.lang.String> toValue() {
			return sandbox.yavi.Hoge::getText;
		}
	};
  
	public static final am.ik.yavi.meta.IntegerConstraintMeta<sandbox.yavi.Hoge> NUMBER
         = new am.ik.yavi.meta.IntegerConstraintMeta<sandbox.yavi.Hoge>() {

		@Override
		public String name() {
			return "number";
		}

		@Override
		public java.util.function.Function<sandbox.yavi.Hoge, java.lang.Integer> toValue() {
			return sandbox.yavi.Hoge::getNumber;
		}
	};
}
```

- プロジェクトをビルドしてアノテーションプロセッサーを実行すると、 `_HogeMeta` というクラスが自動生成される
    - クラス名は、対象クラス名の先頭にアンダーバー (`_`) が付き、末尾に `Meta` が追加されたものになる
- Gradle の場合は `build/generated/sources/annotationProcessor/java` の下に出力される

```java:Main.java
        final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
                .constraint(_HogeMeta.TEXT, c -> c.notEmpty())
                .constraint(_HogeMeta.NUMBER, c -> c.lessThan(10))
                .build();
```

- 生成されたメタクラスを利用すると、制約を定義するときに名前の指定を省略できるようになる

## コンストラクタ引数で指定する

```java:Hoge.java
package sandbox.yavi;

import am.ik.yavi.meta.ConstraintTarget;

public class Hoge {
    private final String text;
    private final int number;

    public Hoge(@ConstraintTarget String text, @ConstraintTarget int number) {
        this.text = text;
        this.number = number;
    }

    public String getText() {
        return text;
    }

    public int getNumber() {
        return number;
    }
}
```

- Getter の名前が Java Beans の仕様に沿っている場合は、コンストラクタ引数に `@ConstraintTarget` を設定することでもメタクラスを生成できる

```java:生成されたメタクラス
package sandbox.yavi;

// Generated at 2024-06-23T10:05:26.182380100+09:00
public class _HogeMeta {
  
	public static final am.ik.yavi.meta.StringConstraintMeta<sandbox.yavi.Hoge> TEXT
         = new am.ik.yavi.meta.StringConstraintMeta<sandbox.yavi.Hoge>() {

		@Override
		public String name() {
			return "text";
		}

		@Override
		public java.util.function.Function<sandbox.yavi.Hoge, java.lang.String> toValue() {
			return sandbox.yavi.Hoge::getText;
		}
	};
  
	public static final am.ik.yavi.meta.IntegerConstraintMeta<sandbox.yavi.Hoge> NUMBER
        = new am.ik.yavi.meta.IntegerConstraintMeta<sandbox.yavi.Hoge>() {

		@Override
		public String name() {
			return "number";
		}

		@Override
		public java.util.function.Function<sandbox.yavi.Hoge, java.lang.Integer> toValue() {
			return sandbox.yavi.Hoge::getNumber;
		}
	};
}
```

### Getter の名前が項目名と等しい場合

```java:Hoge.java
package sandbox.yavi;

import am.ik.yavi.meta.ConstraintTarget;

public class Hoge {
    private final String text;
    private final int number;

    public Hoge(
            @ConstraintTarget(getter = false) String text,
            @ConstraintTarget(getter = false) int number) {
        this.text = text;
        this.number = number;
    }

    public String text() {
        return text;
    }

    public int number() {
        return number;
    }
}
```

- Getter の名前を項目名と同じにしている場合は、 `@ConstraintTarget` の `getter` に `false` を設定する

```java:生成されたメタクラス
package sandbox.yavi;

// Generated at 2024-06-23T10:27:21.116552100+09:00
public class _HogeMeta {
  
	public static final am.ik.yavi.meta.StringConstraintMeta<sandbox.yavi.Hoge> TEXT
         = new am.ik.yavi.meta.StringConstraintMeta<sandbox.yavi.Hoge>() {

		@Override
		public String name() {
			return "text";
		}

		@Override
		public java.util.function.Function<sandbox.yavi.Hoge, java.lang.String> toValue() {
			return sandbox.yavi.Hoge::text;
		}
	};
  
	public static final am.ik.yavi.meta.IntegerConstraintMeta<sandbox.yavi.Hoge> NUMBER
         = new am.ik.yavi.meta.IntegerConstraintMeta<sandbox.yavi.Hoge>() {

		@Override
		public String name() {
			return "number";
		}

		@Override
		public java.util.function.Function<sandbox.yavi.Hoge, java.lang.Integer> toValue() {
			return sandbox.yavi.Hoge::number;
		}
	};
}
```

### フィールドアクセスにする

```java:Hoge.java
package sandbox.yavi;

import am.ik.yavi.meta.ConstraintTarget;

public class Hoge {
    final String text;
    final int number;

    public Hoge(
            @ConstraintTarget(field = true) String text,
            @ConstraintTarget(field = true) int number) {
        this.text = text;
        this.number = number;
    }
}
```

- Getter を用意したくない場合は、メタクラスからのアクセスをフィールド直接のアクセスにできる
- この場合、 `@ConstraintTarget` の `field` に `true` を設定する
- 自動生成されるメタクラスは対象クラスと同じパッケージに出力されるので、フィールドはパッケージプライベート以上の可視性にしておく必要がある

```java:生成されたメタクラス
package sandbox.yavi;

// Generated at 2024-06-23T10:27:52.651064400+09:00
public class _HogeMeta {
  
	public static final am.ik.yavi.meta.StringConstraintMeta<sandbox.yavi.Hoge> TEXT
         = new am.ik.yavi.meta.StringConstraintMeta<sandbox.yavi.Hoge>() {

		@Override
		public String name() {
			return "text";
		}

		@Override
		public java.util.function.Function<sandbox.yavi.Hoge, java.lang.String> toValue() {
			return x  -> x.text;
		}
	};
  
	public static final am.ik.yavi.meta.IntegerConstraintMeta<sandbox.yavi.Hoge> NUMBER
         = new am.ik.yavi.meta.IntegerConstraintMeta<sandbox.yavi.Hoge>() {

		@Override
		public String name() {
			return "number";
		}

		@Override
		public java.util.function.Function<sandbox.yavi.Hoge, java.lang.Integer> toValue() {
			return x  -> x.number;
		}
	};
}
```

## Record クラスの場合

```java:Hoge.java
package sandbox.yavi;

import am.ik.yavi.meta.ConstraintTarget;

public record Hoge(@ConstraintTarget String text, @ConstraintTarget int number) {
}
```

- Record クラスの場合は、レコードコンポーネントに `@ConstraintTarget` を設定する
- このとき `getter = false` は不要

```java:生成されたメタクラス
package sandbox.yavi;

// Generated at 2024-06-23T10:30:29.492874400+09:00
public class _HogeMeta {
  
	public static final am.ik.yavi.meta.StringConstraintMeta<sandbox.yavi.Hoge> TEXT
         = new am.ik.yavi.meta.StringConstraintMeta<sandbox.yavi.Hoge>() {

		@Override
		public String name() {
			return "text";
		}

		@Override
		public java.util.function.Function<sandbox.yavi.Hoge, java.lang.String> toValue() {
			return sandbox.yavi.Hoge::text;
		}
	};
  
	public static final am.ik.yavi.meta.IntegerConstraintMeta<sandbox.yavi.Hoge> NUMBER
         = new am.ik.yavi.meta.IntegerConstraintMeta<sandbox.yavi.Hoge>() {

		@Override
		public String name() {
			return "number";
		}

		@Override
		public java.util.function.Function<sandbox.yavi.Hoge, java.lang.Integer> toValue() {
			return sandbox.yavi.Hoge::number;
		}
	};
}
```

# ビルトインの制約
デフォルトメッセージが何なのかや、埋め込まれるパラメータなどの細かい情報は本家ガイドを参照。

- https://yavi.ik.am/#built-in-constraints

ここでは気になったポイントに絞ってメモする。

## String
### 文字数のカウント
```java:Main.java
package sandbox.yavi;

import am.ik.yavi.arguments.StringValidator;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.core.Validated;
import am.ik.yavi.core.ValueValidator;

public class Main {
    public static void main(String[] args) {
        final StringValidator<String> validator = StringValidatorBuilder
                .of("text", c -> c.lessThanOrEqual(3)).build();

        printResult(validator, "𠮷野家");
        printResult(validator, "𠮷野家！");
        printResult(validator, "abc");
        printResult(validator, "abcd");
    }

    private static <T, X> void printResult(ValueValidator<T, X> validator, T target) {
        final Validated<X> validated = validator.validate(target);
        System.out.println(target + " > " + (validated.isValid() ? "valid" : "invalid"));
        if (!validated.isValid()) {
            validated.errors()
                    .stream()
                    .map(e -> "  " + e.message())
                    .forEach(System.out::println);
        }
    }
}
```

```text:実行結果
𠮷野家 > valid
𠮷野家！ > invalid
  The size of "text" must be less than or equal to 3. The given size is 4
abc > valid
abcd > invalid
  The size of "text" must be less than or equal to 3. The given size is 4
```

- YAVI は、文字数のカウントをするときに以下のケースを考慮する
    - サロゲートペア
    - 結合文字
    - 異体字セレクタ
    - 絵文字
- サロゲートペアと結合文字についてはデフォルトで有効になっており、見た目と文字数が一致する形で検査される
- 異体字セレクタと絵文字については、パフォーマンスを優先して**デフォルトは無効となっている**
    - 絵文字については「できるだけ見た目と文字数が一致するようにしているが保証はできない」というスタンス
    - 詳しくは[公式ドキュメント](https://yavi.ik.am/#emoji)を参照

## email

- YAVI は、文字列が email アドレス形式であることを検証する制約をサポートしている
- email アドレスのフォーマットといえばバリデーションが難しいことで有名？
    - 参考：[間違ったメールバリデーションが多いので正解を探してみた｜りふ](https://note.com/reflex4qa/n/n27fb36daa699)
- YAVI の email アドレスのチェックが何に準拠しているのか、ドキュメントやソースコードには特に明記は無かった
- チェックで使用している正規表現は以下で定義されている
    - [yavi/src/main/java/am/ik/yavi/constraint/CharSequenceConstraint.java at 0.14.1 · making/yavi](https://github.com/making/yavi/blob/0.14.1/src/main/java/am/ik/yavi/constraint/CharSequenceConstraint.java#L63)
- 正規表現を抜き出すと、以下のようになっている

```text
^[^\x00-\x1F()<>@,;:\\".\[\]\s]+(\.[^\x00-\x1F()<>@,;:\\".\[\]\s]+)*@([^\x00-\x1F()<>@,;:\\".\[\]\s]+(\.[^\x00-\x1F()<>@,;:\\".\[\]\s]+)*|^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$)$
```

```plantuml
@startregex
^[^\x00-\x1F()<>@,;:\\".\[\]\s]+(\.[^\x00-\x1F()<>@,;:\\".\[\]\s]+)*@([^\x00-\x1F()<>@,;:\\".\[\]\s]+(\.[^\x00-\x1F()<>@,;:\\".\[\]\s]+)*|^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$)$
@endregex
```

## バイト数での判定

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.arguments.StringValidator;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.core.Validated;
import am.ik.yavi.core.ValueValidator;

import java.nio.charset.Charset;

public class Main {
    public static void main(String[] args) {
        final StringValidator<String> validator = StringValidatorBuilder
                .of("text",
                        c -> c.asByteArray(Charset.forName("MS932")).lessThan(4))
                .build();

        printResult(validator, "abc");
        printResult(validator, "あｲ");
        printResult(validator, "あい");
    }

    private static <T, X> void printResult(ValueValidator<T, X> validator, T target) {
        final Validated<X> validated = validator.validate(target);
        System.out.println(target + " > " + (validated.isValid() ? "valid" : "invalid"));
        if (!validated.isValid()) {
            validated.errors()
                    .stream()
                    .map(e -> "  " + e.message())
                    .forEach(System.out::println);
        }
    }
}
```

```text:実行結果
abc > valid
あｲ > valid
あい > invalid
  The byte size of "text" must be less than 4. The given size is 4
```

- `asByteArray` で文字列を byte 配列にした場合のサイズを検証できるようになる
- 引数の文字コードの指定を省略した場合は、デフォルトで `UTF-8` でエンコードされる


## password

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.arguments.StringValidator;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.core.Validated;

public class Main {

    public static void main(String[] args) {
        final StringValidator<String> validator = StringValidatorBuilder
            .of("password", c -> c.password(policy ->
                policy.uppercase().lowercase().numbers().build()
            ))
            .build();

        printResult(validator, "Aa11");
        printResult(validator, "Aa1#");
        printResult(validator, "aa11");
    }

    private static void printResult(StringValidator<String> validator, String password) {
        final Validated<String> validated = validator.validate(password);
        System.out.println(password + " > " + (validated.isValid() ? "valid" : "invalid"));
        if (!validated.isValid()) {
            validated.errors()
                    .stream()
                    .map(e -> "  " + e.message())
                    .forEach(System.out::println);
        }
    }
}
```

```text:実行結果
Aa11 > valid
Aa1# > valid
aa11 > invalid
  "password" must meet Uppercase policy
```

**説明**

```java:Main.java
        final StringValidator<String> validator = StringValidatorBuilder
            .of("password", c -> c.password(policy ->
                policy.uppercase().lowercase().numbers().build()
            ))
            .build();
```

- パスワードの文字種チェックを定義できる
- `password` の引数にパスワードポリシーを定義する関数を渡す
    - `uppercase` や `lowercase` など、含めなければならない文字種を定義するメソッドが用意されている
    - `upercase()`, `lowercase()` は、最低１文字、その文字種が含まれている必要があることを定義している
    - したがって、上記実装例は「大文字・小文字・数字が、それぞれ最低１文字含まれている必要がある」というポリシーを定義していることになる

### 指定していない文字種は任意扱い？

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.arguments.StringValidator;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.core.Validated;

public class Main {

    public static void main(String[] args) {
        final StringValidator<String> validator = StringValidatorBuilder
                .of("password", c -> c.password(policy ->
                        policy.uppercase().build()
                ))
                .build();

        printResult(validator, "AAAA");
        printResult(validator, "Aa1#");
        printResult(validator, "aa1#");
    }

    private static void printResult(StringValidator<String> validator, String password) {
        final Validated<String> validated = validator.validate(password);
        System.out.println(password + " > " + (validated.isValid() ? "valid" : "invalid"));
        if (!validated.isValid()) {
            validated.errors()
                    .stream()
                    .map(e -> "  " + e.message())
                    .forEach(System.out::println);
        }
    }
}
```

```text:実行結果
AAAA > valid
Aa1# > valid
aa1# > invalid
  "password" must meet Uppercase policy
```

- `uppercase` だけを定義したポリシーに対して `Aa1#` が valid と判定されている
- ポリシーで指定された文字種以外は任意扱いになるっぽい
- 「指定文字種以外は含めてはいけない」を実現しようと思うと、 `pattern` で否定の正規表現を指定するか、独自の制約を定義する必要がありそう？

### 最低文字数を指定する

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.arguments.StringValidator;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.core.Validated;

public class Main {

    public static void main(String[] args) {
        final StringValidator<String> validator = StringValidatorBuilder
                .of("password", c -> c.password(policy ->
                    policy.uppercase(2).lowercase(3).build()
                ))
                .build();

        printResult(validator, "ABcde");
        printResult(validator, "Abcd");
        printResult(validator, "Abc");
    }

    private static void printResult(StringValidator<String> validator, String password) {
        final Validated<String> validated = validator.validate(password);
        System.out.println(password + " > " + (validated.isValid() ? "valid" : "invalid"));
        if (!validated.isValid()) {
            validated.errors()
                    .stream()
                    .map(e -> "  " + e.message())
                    .forEach(System.out::println);
        }
    }
}
```

```text:実行結果
ABcde > valid
Abcd > invalid
  "password" must meet Uppercase policy
Abc > invalid
  "password" must meet Uppercase policy
  "password" must meet Lowercase policy
```

- `uppercase` などの文字種を指定するメソッドの引数で最低文字数（1以上）を指定できる


### 任意のポリシーの組み合わせを選択できるようにする

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.arguments.StringValidator;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.constraint.password.PasswordPolicy;
import am.ik.yavi.core.Validated;

public class Main {

    public static void main(String[] args) {
        final StringValidator<String> validator = StringValidatorBuilder
                .of("password", c -> c.password(policy ->
                        policy.optional(
                            2,
                            PasswordPolicy.LOWERCASE,
                            PasswordPolicy.UPPERCASE,
                            PasswordPolicy.NUMBERS)
                        .build()
                ))
                .build();

        printResult(validator, "aA");
        printResult(validator, "a1");
        printResult(validator, "A1");
        printResult(validator, "a#");
    }

    private static void printResult(StringValidator<String> validator, String password) {
        final Validated<String> validated = validator.validate(password);
        System.out.println(password + " > " + (validated.isValid() ? "valid" : "invalid"));
        if (!validated.isValid()) {
            validated.errors()
                    .stream()
                    .map(e -> "  " + e.message())
                    .forEach(System.out::println);
        }
    }
}
```

```text:実行結果
aA > valid
a1 > valid
A1 > valid
a# > invalid
  "password" must meet at least 2 policies from [Lowercase, Uppercase, Numbers]
```

**説明**

```java
        final StringValidator<String> validator = StringValidatorBuilder
                .of("password", c -> c.password(policy ->
                        policy.optional(
                            2,
                            PasswordPolicy.LOWERCASE,
                            PasswordPolicy.UPPERCASE,
                            PasswordPolicy.NUMBERS)
                        .build()
                ))
                .build();
```

- `optional` を使用すると、いくつかのポリシーの中から任意の n 個のポリシーを選択する、という制約を定義できる
- この場合、 `LOWERCASE` (小文字), `UPPERCASE` (大文字), `NMBERS` (数字) の３つのポリシーのうち任意の２つを選択できる、という定義になる

# Getter で公開していない項目を検査する

```java:Hoge.java
package sandbox.yavi;

import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.Validator;

public class Hoge {
    private final String text;

    public static final Validator<Hoge> VALIDATOR = ValidatorBuilder.<Hoge>of()
            ._string(h -> h.text, "text", c -> c.notEmpty())
            .build();

    public Hoge(String text) {
        this.text = text;
    }

    @Override
    public String toString() {
        return "Hoge{" + "text='" + text + '\'' + '}';
    }
}
```

```java:Main.java
package sandbox.yavi;

import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;

public class Main {

    public static void main(String[] args) {
        printResult(Hoge.VALIDATOR, new Hoge(""));
        printResult(Hoge.VALIDATOR, new Hoge("a"));
    }

    private static <T> void printResult(Validator<T> validator, T target) {
        final ConstraintViolations violations = validator.validate(target);
        System.out.println(
            target + " > " + (violations.isValid() ? "valid" : "invalid"));
        if (!violations.isValid()) {
            violations.stream()
                    .map(e -> "  " + e.message())
                    .forEach(System.out::println);
        }
    }
}
```

```text:実行結果
Hoge{text=''} > invalid
  "text" must not be empty
Hoge{text='a'} > valid
```

- クラス内にバリデータを定義することで、フィールドを非公開のまま検査できるように実装できる

# Spring で使用する

```gradle:build.gradle
plugins {
	id 'java'
	id 'org.springframework.boot' version '3.3.1'
	id 'io.spring.dependency-management' version '1.1.0'
}

group = 'sandbox.yavi'
version = '1.0.0'

compileJava.options.encoding = "UTF-8"

java {
	sourceCompatibility = '21'
	targetCompatibility = '21'
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
	implementation "am.ik.yavi:yavi:0.14.0"
}
```

```java:YaviApplication
package sandbox.yavi;

import am.ik.yavi.factory.ValidatorFactory;
import am.ik.yavi.message.MessageSourceMessageFormatter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class YaviApplication {

    public static void main(String[] args) {
        try (final ConfigurableApplicationContext context
             = SpringApplication.run(YaviApplication.class, args)) {

            final HogeService service = context.getBean(HogeService.class);
            service.execute(new Hoge(""));
            service.execute(new Hoge("a"));
        }
    }

    @Bean
    public ValidatorFactory yaviValidatorFactory(MessageSource messageSource) {
        final MessageSourceMessageFormatter formatter
            = new MessageSourceMessageFormatter(messageSource::getMessage);
        return new ValidatorFactory(".", formatter);
    }
}
```

```java:Hoge.java
package sandbox.yavi;

public record Hoge(String text) {
}
```

```java:HogeService.java
package sandbox.yavi;

import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;
import am.ik.yavi.factory.ValidatorFactory;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class HogeService {
    @Autowired
    private ValidatorFactory validatorFactory;
    private Validator<Hoge> hogeValidator;

    @PostConstruct
    public void init() {
        hogeValidator = validatorFactory.validator(
                b -> b.constraint(Hoge::text, "text", c -> c.notEmpty()));
    }

    public void execute(Hoge hoge) {
        final ConstraintViolations violations = hogeValidator.validate(hoge);
        System.out.println(hoge + " > " + (violations.isValid() ? "valid" : "invalid"));
        violations.stream()
                .map(v -> "  " + v.message())
                .forEach(System.out::println);
    }
}
```

```properties:messages.properties
container.notEmpty="{0}" must not be empty!!!!
```

```text:実行結果
Hoge[text=] > invalid
  "text" must not be empty!!!!
Hoge[text=a] > valid
```

**説明**

```java:YaviApplication.java
    @Bean
    public ValidatorFactory yaviValidatorFactory(MessageSource messageSource) {
        final MessageSourceMessageFormatter formatter
            = new MessageSourceMessageFormatter(messageSource::getMessage);
```

- Spring が管理する `MessageSource` と YAVI の `MessageFormatter` を繋げるために、 `MessageSourceMessageFormatter` というクラスが用意されている
    - `MessageSourceMessageFormatter` のコンストラクタに Spring の `MessageSource` の `getMessage` をメソッド参照で渡す

```java:YaviApplicaiton.java
        return new ValidatorFactory(".", formatter);
```

- 共通の設定をした `Validator` の生成を補助するクラスとして `ValidatorFactory` というクラスが用意されている
- コンストラクタの第一引数には、メッセージキーの区切り文字を渡す
- 第二引数には `MessageFormatter` を渡す

```java:HogeService
    @Autowired
    private ValidatorFactory validatorFactory;
    private Validator<Hoge> hogeValidator;

    @PostConstruct
    public void init() {
        hogeValidator = validatorFactory.validator(
                b -> b.constraint(Hoge::text, "text", c -> c.notEmpty()));
    }
```

- `ValidatorFactory` の `validator` メソッドで新しい `Validator` を生成できる



# 参考
- [YAVI (Yet Another ValIdation) Reference Documentation](https://yavi.ik.am/)
- [making/yavi: Yet Another Validation for Java (A lambda based type safe validation framework)](https://github.com/making/yavi)
- [Java用Validatorライブラリ"YAVI"(ヤヴァイ)の紹介 - IK.AM](https://ik.am/entries/461)