APIGatewayを作成するときにOpenAPIから型情報を読み込み補完がきくようにしたメモ

## 概要
API Gatewayの設定にOpenAPIを連携できないか検討した。
OpenAPIのJSONから型情報を読み込む形で実装してみたメモを残す。

## 検討
[cdk - openapi-definition](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway-readme.html#openapi-definition)を読むと、定義はすべてOpenAPIのほうに書く必要があるもよう。また、OpenAPIは3.1は使えず3.0か2.0を使う必要がある。

> すべてのリソースとメソッドは OpenAPI 仕様ファイルの一部として定義する必要があり、CDK を介して追加することはできません。これは、これらのプロパティの重複や潜在的な混乱を防ぐためです。

[こちらの記事](https://zenn.dev/taroman_zenn/articles/91879cec40627c)で「OpenAPI定義にAWS用の設定値を記載するのが嫌だ」と言っているように、OpenAPIとAWSの設定は分けて管理したい。
記事中では[openapix](https://github.com/alma-cdk/openapix)を使用しているが、1年更新がないのが不安。
型定義をOpenAPIから取り込む方法を考える。

## 構造
OpenAPIのフォルダとcdkのフォルダを分けて作成する。
以下はイメージ。

```yaml
- openAPI
  - openapi.yaml # ... OpenAPIの定義ファイル
- cdk
  - data
    - openapi.json # ... OpenAPIの定義ファイルをJSONとしてエクスポートした生成ファイル
  - lib
    - stack.ts
```

## StackファイルにOpenAPIの定義を適用する

[yaml](https://github.com/hibohiboo/kartagraph-monorepo/blob/c2768e635a38d31b2f3d1bb9349ce01006b04f46/docs/openAPI/openapi/api.yaml)

OpenAPIから、Stackで使いたい情報をピックアップして記載する。

```yaml
openapi: 3.1.0
paths:
  /tags:
    put:
      summary: タグ情報永続化
  /scenario/{scenarioId}/tags:
    get:
      summary: シナリオタグサマリー取得
  /scenario:
    get:
      summary: シナリオ一覧取得
```

これをもとに、下記のように補完が効くようにする。
yamlからjsonへの変換は、[package.json](https://github.com/hibohiboo/kartagraph-monorepo/blob/c2768e635a38d31b2f3d1bb9349ce01006b04f46/infrastructure/package.json)でスクリプトにしている。(openapi-init)。
Stackを書く前に実行し、OpenAPIの情報を反映する必要がある。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/8c22ef5a-27d4-7b6d-adad-a6f89b8aede4.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/84666cd7-6ae0-75e7-4ac8-51d90d557309.png)

[rest-stack.ts](https://github.com/hibohiboo/kartagraph-monorepo/blob/c2768e635a38d31b2f3d1bb9349ce01006b04f46/infrastructure/lib/rest-stack.ts)
```ts:rest-stack.ts
// 省略
import openApi from '../data/openapi.json';

type OpenAPI = typeof openApi;
type Paths = OpenAPI['paths'];

export class KartaGraphRESTAPIStack extends core.Stack {
  private apiRoot: apigateway.Resource;
  // 省略

  private createAPILambdaResource<T extends keyof Paths>(path: T, method: keyof Paths[T], props: Partial<NodejsFunctionProps>) {
    const methodStr = method as string; // 型が合わない(string | number | Symbol とみなされる)ので本来の型であるstringキャスト
    const lambda = this.createLambda(
      {
        ...props,
        // summary は 必ず入力するものとして、 any で逃げる
        description: (openApi as any).paths[path][method].summary,
      },
      `${methodStr}-${path}`,
    );
    const methodOptions = this.createMethodOptions({
      'method.request.header.Content-Type': true,
    });

    this.getResource(path).addMethod(methodStr.toUpperCase(), new LambdaIntegration(lambda), methodOptions);
  }

  // 省略
```

上記のようにジェネリックを使って、補完をできるようにした。
ただ、メソッドの中では`as string`や`as any`を使っているのが分かるように、うまく型予測することはできなかった。

パスからリソースを作る部分については、[パスを分割し](https://github.com/hibohiboo/kartagraph-monorepo/blob/c2768e635a38d31b2f3d1bb9349ce01006b04f46/infrastructure/test/convertPathList.test.ts)、[マップでキャッシュするヘルパー関数](https://github.com/hibohiboo/kartagraph-monorepo/blob/c2768e635a38d31b2f3d1bb9349ce01006b04f46/infrastructure/lib/rest-stack.ts#L106)を作っている。（同じリソースは2回作ろうとするとエラーとなるため、すでにあるリソースは再利用する必要がある）

これでデプロイした結果が下記。
OpenAPIの情報をStackに反映することができた。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/7e9af4a5-c141-3d4b-e864-0f9f012f219b.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/4c7761a2-c70a-3870-37bf-332c5f7447ce.png)



## 参考
[cdk - openapi-definition](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway-readme.html#openapi-definition)
[cdk - SpecRestApi](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.SpecRestApi.html)
[openapi-down-convert](https://github.com/apiture/openapi-down-convert)
[OpenAPI を使用した REST API の設定](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-import-api.html)
[OpenAPI × AWS CDK × APIGateway でRest APIを管理する](https://zenn.dev/taroman_zenn/articles/91879cec40627c)
[AWS CDKで作ったAPI Gateway + LambdaからOpenAPIドキュメントを生成してみた](https://zenn.dev/ncdc/articles/42c7d2b8dfec3e)
[AWS CDKを使ってAPI Gateway(HTTPAPI)+LambdaをOpenAPIで定義してデプロイする](https://serverless.co.jp/blog/347/) 
[APIGatewayをCDKで構築するコツ](https://qiita.com/yuukiiiii/items/2605b58bcde883a9ea65)
[【TypeScript】Utility Typesをまとめて理解する](https://qiita.com/k-penguin-sato/items/e2791d7a57e96f6144e5)
[【TypeScript】Yaml を読み込んで（無理やり）型付けする](https://kakehashi-dev.hatenablog.com/entry/2021/12/20/080000)