[GraphQL] @oneOf ディレクティブで input 型の union を表現する

# `@oneOf` ディレクティブとは

`@oneOf` とは、 input 型が持つ field の中で、いずれか 1 つを必ず指定しなければならないことを表現するディレクティブです。

そりゃあカスタムディレクティブを頑張って実装すれば実現できるけど・・・と思ったそこのアナタ！
実はこれ、 GraphQL の公式仕様としてもうすぐマージされる予定の directive なんです。

https://github.com/graphql/graphql-spec/pull/825

もともと GraphQL には `union` があり、出力については「いずれか 1 種類だけ返す」ことを API 上で表現できていましたが、入力については表現する方法がありませんでした。
`@oneOf` ディレクティブによって、これが可能になります！


# 使用例
以下はユーザ検索 API に `@oneOf` を適用した例です。
検索条件である `FindUserCondition` を input で指定するとき、 `id` フィールドか `nameContaining` のどちらかに non-null な値が指定されている必要があることを表しています。


```graphql:example-schema.graphql
directive @oneOf on INPUT_OBJECT

input FindUserCondition @oneOf { # <--- ここで "oneOf" 指定
  # ユーザを ID 指定で検索する場合に指定する
  id: ID

  # ユーザを、名前の部分一致で検索する場合に指定する
  nameContaining: String
}

type Query {
  # ユーザを検索する
  findUser(by: FindUserCondition!): [User!]!
}
```

有効なクエリの例
```graphql:example-query.graphql
query q1 {
  findUser(by: {
    id: "u1"
  }) {
    id
    name
  }
}
```

以下のような指定はエラーになります。
どういう指定が無効なのかも前述の PR の中にパターン表が載っています。
```graphql:error.graphql
query error1 {
  findUser(by: {
    # NG: 2 つ以上のフィールドを同時に指定している
    id: "1"
    nameContaining: "foo"
  }) {
    id
    name
  }
}

query error2 {
  findUser(by: {
    # NG: 指定したフィールドは 1 つだが値が null
    id: null
  }) {
    id
    name
  }
}

query error3 {
  findUser(by: {
    # NG: 一見 nameContaining が有効そうに見えるが、この指定方法はダメ
    id: null
    nameContaining: "foo"
  }) {
    id
    name
  }
}
```

# もう使えるの？
今回私は spring-graphql のバージョンアップをしていて気づいたのですが、観測範囲では
少なくとも以下の実装に (モノによっては結構前から) `@oneOf` が含まれていました。

- [graphql-java (v21.2)](https://github.com/graphql-java/graphql-java/releases/tag/v21.2)
- [graphql-ruby (v2.0.14)](https://github.com/rmosolgo/graphql-ruby/pull/4184)
- [graphql-js (v17.0.0-alpha.3)](https://github.com/graphql/graphql-js/releases/tag/v17.0.0-alpha.3)


# spring-graphql で `@oneOf` を使ってみる
せっかくなので spring-graphql で `@oneOf` ディレクティブを使って API を実装してみます。
完全なコードは [GitHub](https://github.com/takeyoda/spring-graphql-oneof-exam) にあります。

まずは SDL から。
```graphql:schema.graphql
directive @oneOf on INPUT_OBJECT

type User {
  id: ID!
  name: String!
}
input FindUserCondition @oneOf {
  id: ID
  nameContaining: String
}
type Query {
  findUser(by: FindUserCondition!): [User!]!
}
```

続いて Controller 側。
```java:UserController.java
@Controller
public class UserController {

  private final List<User> users;

  public UserController() {
    List<User> users = new ArrayList<>();
    users.add(new User("u1", "taro"));
    users.add(new User("u2", "jiro"));
    users.add(new User("u3", "saburo"));
    this.users = users;
  }

  @QueryMapping
  public Flux<User> findUser(@Argument("by") FindUserCondition condition) {
    Predicate<User> filter;
    if (condition.id() != null) {
      filter = u -> u.id().equals(condition.id());
    } else if (condition.nameContaining() != null) {
      filter = u -> u.name().contains(condition.nameContaining());
    } else {
      throw new IllegalStateException();
    }
    return Flux.fromStream(users.stream().filter(filter));
  }
}
```

# 動かしてみる
## 正常系
- リクエスト
  ```graphql
  query findUser {
    findUser(by: {
      id: "u1"
    }) {
      id
      name
    }
  }
  ```
- レスポンス
  ```json
  {
    "data": {
      "findUser": [
        {
          "id": "u1",
          "name": "taro"
        }
      ]
    }
  }
  ```

## 異常系
- リクエスト
  ```graphql
  query findUser {
    findUser(by: {
      id: "u1"
      nameContaining: "x"
    }) {
      id
      name
    }
  }
  ```
- レスポンス
  ```json
  {
    "errors": [
      {
        "message": "graphql.execution.OneOfTooManyKeysException: Exactly one  key must be specified for OneOf type 'FindUserCondition'.",
        "locations": [
          {
            "line": 2,
            "column": 3
          }
        ],
        "path": [
          "findUser"
        ],
        "extensions": {
          "classification": "INTERNAL_ERROR"
        }
      }
    ],
    "data": null
  }
  ```
  入力チェックは何も実装していませんが、ちゃんとエラーになってくれました！

# あとがき
GraphQL の表現力が予約語や構文の追加ではなく directive で拡張されていくのは面白いですね。
こうなってくるとドキュメントにも directive の情報を出力したくなります。
私が GraphQL API のドキュメント生成で使っている [SpectaQL](https://github.com/anvilco/spectaql) では [カスタムディレクティブの出力をサポートしていない](https://github.com/anvilco/spectaql/issues/544) のですが、こういう流れが加速していくのであれば是非対応してもらいところです。
(`@oneOf` に限らず、認証のために `@auth` みたいなカスタムディレクティブを作って設定してもドキュメントに出力されないのがイマイチなんですよねぇ)