OpenAPI + Spring Boot(Kotlin)でファイルダウンロードAPIを作成する

# はじめに
OpenAPIを使うとサーバーとクライアント両方のコードが生成出来て便利です。
本記事では`/api/csv`などのURLにアクセスするとCSVがダウンロード出来たり、`/api/image`にアクセスすると画像が表示出来るようなAPIを作成する方法をご紹介します。

# 作ってみる
大まかな流れとしては以下のようになります。
1. OpenAPI Spec Fileを作成
2. openapi-generatorを使って、Spring Boot(Kotlin)プロジェクトを生成
3. Serviceクラスを実装
4. `./gradlew bootRun`で実行

## CSVダウンロード
#### OpenAPI Spec Fileの作成
まずはSpec Fileを作成します。
今回はYAML形式で作成します。

`http://localhost:8080/api/csv`にGETするとCSVがダウンロードされるAPIは以下のように定義します。

- `content`は `text/csv`にする
- `scheme`の`type`は`string`にする

```yaml:api.yml
openapi: "3.0.2"
info:
  title: API Title
  version: "1.0"
servers:
  - url: http://localhost:8080/api
tags:
  - name: Download
    description: ファイルダウンロード
paths:
  /csv:
    get:
      summary: CSVダウンロード
      tags:
        - Download
      responses:
        "200":
          description: OK
          content:
            text/csv:
              schema:
                type: string
```

#### Spring Boot(Kotlin)プロジェクトを作成
openapi-generatorがインストールされていない場合は、以下のコマンドでインストールします。

```bash
brew install openapi-generator
```

`openapi-generator generate`コマンドを使用して、Spec FileからSpring Bootプロジェクトを生成します。

```bash
openapi-generator generate -i api.yml -g kotlin-spring --additional-properties=serviceInterface=true
```

簡単なオプションの説明は以下の通りです。

- `-i (---input-spec)`
    - インプットとなるSpec Fileのパスを指定します
- `-g (--generator-name)`
    - 何を生成するか出力します
    - 他に使用できるものはこちらを参照してください  
      ([Generators List · OpenAPI Generator](https://openapi-generator.tech/docs/generators))
- `--additional-properties`
    - generatorごとの設定を記述します
    - **serviceInterface=true**はService interfaceを作成して、Service classにロジックが書けるようにします
    - kotlin-springで使用できるものはこちらを参照してください  
      ([openapi-generator/kotlin-spring.md at master · OpenAPITools/openapi-generator](https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/kotlin-spring.md))

#### Serviceクラスを実装
`DownloadApiService`というInterfaceが自動生成されているので、それを継承した`DownloadApiServiceImpl`を実装します。
戻り値がStringとなっているため、CSVとして出力したいStringを設定しておきます。

```kotlin:src/main/kotlin/org/openapitools/api/DownloadApiService.kt
package org.openapitools.api

import org.openapitools.model.ImageType
interface DownloadApiService {

	fun csvGet(): kotlin.String
}
```

```kotlin:src/main/kotlin/org/openapitools/api/DownloadApiServiceImpl.kt
package org.openapitools.api

import org.openapitools.model.ImageType
import org.springframework.core.io.ClassPathResource
import org.springframework.core.io.Resource
import org.springframework.stereotype.Service

@Service
class DownloadApiServiceImpl : DownloadApiService {
    override fun csvGet(): String {
        return "商品名,価格\nりんご,100\nオレンジ,200"
    }
}
```

#### Spring Bootアプリケーションを実行
bootRunコマンドを実行して、APIにアクセスしてみます。

```bash
./gradlew bootRun
```

http://localhost:8080/api/image にアクセスするとCSVがダウンロードされました。

```text:csv.csv
商品名,価格
りんご,100
オレンジ,200
```

### Shift_JISで出力する
上記の方法だとUTF-8として出力されます。
Excelで簡単に取り込みたいとの要望から、Shift_JISで出力する場合を考えてみます。

`http://localhost:8080/api/csv/sjis`というAPIを新設します。pathに以下を追加してください。
`text/csv; charset=Shift_JIS`のように指定するのがポイントです。

```yaml:api.yml
path:
  # 省略
  /csv/sjis:
    get:
      summary: CSVダウンロード(sjis)
      tags:
        - Download
      responses:
        "200":
          description: OK
          content:
            text/csv; charset=Shift_JIS:
              schema:
                type: string
```

あとは違いはないので、openapi-generatorで再度生成して上書きし、同様にServiceを実装します。

```kotlin:src/main/kotlin/org/openapitools/api/DownloadApiServiceImpl.kt
package org.openapitools.api

import org.openapitools.model.ImageType
import org.springframework.core.io.ClassPathResource
import org.springframework.core.io.Resource
import org.springframework.stereotype.Service

@Service
class DownloadApiServiceImpl : DownloadApiService {
    override fun csvGet(): String {
        return "商品名,価格\nりんご,100\nオレンジ,200"
    }

    override fun csvSjisGet(): String {
        return "商品名,価格\nりんご,100\nオレンジ,200"
    }
}
```

http://localhost:8080/api/csv/sjis にアクセスするとShift_JISエンコードのCSVが出力されます。

## 画像の表示
次は画像を表示するようなAPIを作成します。
Spec Fileに画像表示用のAPIを追加します。

- `content`は `image/jpeg`にする
- `scheme`の`type`は`string`に、`format`は`binary`にする

```yaml:api.yml
path:
  # 省略
  /image:
    get:
      summary: 画像ダウンロード
      tags:
        - Download
      responses:
        "200":
          description: OK
          content:
            image/jpeg:
              schema:
                type: string
                format: binary
```

openapi-generatorで再度生成すると、`Resource`を返すようなメソッドが生成されています。
今回は`src/main/resources/image.jpg`に置いた画像を表示するようにするため、`ClassPathResource("image.jpg")`として指定します。

```kotlin:src/main/kotlin/org/openapitools/api/DownloadApiService.kt
package org.openapitools.api

import org.openapitools.model.ImageType
interface DownloadApiService {

	// 省略

	fun imageGet(): org.springframework.core.io.Resource
}
```

```kotlin:src/main/kotlin/org/openapitools/api/DownloadApiServiceImpl.kt
package org.openapitools.api

import org.openapitools.model.ImageType
import org.springframework.core.io.ClassPathResource
import org.springframework.core.io.Resource
import org.springframework.stereotype.Service

@Service
class DownloadApiServiceImpl : DownloadApiService {

    // 省略

    override fun imageGet(): Resource {
        return ClassPathResource("image.jpg")
    }
}
```

http://localhost:8080/api/image にアクセスすると画像が表示されます。

### MIME typeを切り替える
上記のAPIではMIME typeを`image/jpeg`として指定していました。
pngの画像も同じAPIで返したいとなったらどうしたら良いでしょうか？
そのままだとpngの画像も`image/jpeg`として返却されてしまいます。

`content`には複数のMIME typeが設定できるので、複数設定するとSpring Boot側でいい感じに設定してもらえます。
query parameterでjpegかpngの画像を選択肢出来るようにしています。

```yaml:api.yml
path:
  # 省略
  /image/jpeg_or_png:
    get:
      summary: 画像ダウンロード(jpeg or png)
      tags:
        - Download
      parameters:
        - in: query
          name: type
          required: true
          schema:
            $ref: "#/components/schemas/ImageType"
      responses:
        "200":
          description: OK
          content:
            image/jpeg:
              schema:
                type: string
                format: binary
            image/png:
              schema:
                type: string
                format: binary
components:
  schemas:
    ImageType:
      type: string
      enum:
        - jpeg
        - png
```

あとは同様にServiceクラスを実装するだけです。
`src/main/resources/image.jpg`にpngの画像を配置しておきます。

```kotlin:src/main/kotlin/org/openapitools/api/DownloadApiServiceImpl.kt
package org.openapitools.api

import org.openapitools.model.ImageType
import org.springframework.core.io.ClassPathResource
import org.springframework.core.io.Resource
import org.springframework.stereotype.Service

@Service
class DownloadApiServiceImpl : DownloadApiService {

    // 省略

    override fun imageGet(): Resource {
        return ClassPathResource("image.jpg")
    }

    override fun imageJpegOrPngGet(type: ImageType): Resource {
        return when (type) {
            ImageType.jpeg -> ClassPathResource("image.jpg")
            ImageType.png -> ClassPathResource("image.png")
        }
    }
}
```
http://localhost:8080/api/image/jpeg_or_png?type=jpeg にアクセスすると`image/jpeg`のjpeg画像が、
http://localhost:8080/api/image/jpeg_or_png?type=png にアクセスすると`image/png`のpng画像が表示されます。

# おわりに
今回作成したサンプルは以下で確認できます。

#### GitHub
[boronngo/openapi-spring-boot-file-download: File download API with openapi and spring boot](https://github.com/boronngo/openapi-spring-boot-file-download)

#### デモ
https://openapi-spring-boot-file-dow.herokuapp.com/api/csv
https://openapi-spring-boot-file-dow.herokuapp.com/api/csv/sjis
https://openapi-spring-boot-file-dow.herokuapp.com/api/image
https://openapi-spring-boot-file-dow.herokuapp.com/api/image/jpeg_or_png?type=jpeg
https://openapi-spring-boot-file-dow.herokuapp.com/api/image/jpeg_or_png?type=png