[Ktor]REST Assuredでmultipart送信(複数ファイル+パラメータ)


## やりたかったこと
kotlin、Ktorでテストコードを書く際に、multipart/form-data形式のデータを
REST Assuredで送信したかった。

検証のために、formdataには複数ファイル、およびパラメータの設定をやってみました。
また、せっかくなのでREST Assuredはkotlin-extensionsを使用しています。

## 参考
ディレクトリ構成等は以下を参考にさせていただきました。
kotlin-ktor-exposed-starter
https://github.com/raharrison/kotlin-ktor-exposed-starter

REST Assuredについてはこちらの記事が参考になります。
https://qiita.com/shimashima35/items/b85228d5bf5ec2708c5a

## 使用ライブラリ（抜粋）
REST Assured:4.1.2
kotlin-extensions :4.1.2

## ソース
朝、昼、晩の食費が記述された明細ファイル3つとユーザーの名前、所持金をパラメータに
multiPart形式で送信し、multiPartからデータを取得してDBに情報を格納する感じです。
重要じゃないクラスは省略しています。

``` MealResourceTest.kt
class MealResourceTest: ServerTest() {
    @Test
    fun addMealTest() {
       // ファイル生成
       val morningDetail = createTempFile("morning", ".json") 
       val lunchDetail = createTempFile("lunch", ".json")
       val dinnerDetail = createTempFile("dinner", ".json")

       // jsonファイルに書き込む
       morningDetail.appendText("""{"price": 300}""")
       lunchDetail.appendText("""{"price": 600}""")
       dinnerDetail.appendText("""{"price": 1000}""")

       val mealData = Given {
           // multiPart形式で３ファイル設定。１つ目の引数は、ファイル名ではなくファイル識別名
           multiPart("morning", morningDetail)
           multiPart("lunch", lunchDetail)
           multiPart("dinner", dinnerDetail)
           // パラメーターはformParamsでMap形式で設定する
           formParams(mapOf("name" to "田中太郎", "money" to 3000))
       } When {
           post("/meal")
       } Then {
           statusCode(200)
       } Extract {
           // 戻り値の型を指定（ここでは、MealDataというデータクラス)
           to<MealData>()
       }
       // 結果検証
       assertEquals("田中太郎", mealData.name)
       assertEquals(3000, mealData.money)
       assertEquals(1900, mealData.totalExpenses)
       assertEquals(1100, mealData.balance)
    }
}
```

``` MealResource.kt
fun Route.meal(mealService: MealService) {
    route("/meal") {
        post("/") {
            val multiPartData = call.receiveMultipart()
            val mealData = mealService.addMeal(multiPartData)
            call.respond(HttpStatusCode.OK, mealData)
        }
    }
}
```

``` MealService.kt

class MealService {
    suspend fun addMeal() {
        // ファイル生成
        val morningDetail = createTempFile("morning", ".json") 
        val lunchDetail = createTempFile("lunch", ".json")
        val dinnerDetail = createTempFile("dinner", ".json")

        val name = ""
        val money = 0

        // ここの内容は、私の過去の記事をご確認ください
        multiPartData.forEachPart {part ->
            when (part) {
                is PartData.FileItem -> {
                    when(part.name) {
                        "morning" -> makeFile(part, morningDetail)
                        "lunch" -> makeFile(part, lunchDetail)
                        "dinner" -> makeFile(part, dinnerDetail)
                    }
                }
                is PartData.FormItem -> {
                    when(part.name) {
                        "name" -> name = part.value
                        "money" -> money = part.value.toInt()
                    }
                }
            }
        }
        
        // Jsonからデータクラスにマッピング
        val morningData = toData(morningDetail)
        val lunchData = toData(lunchDetail)
        val dinnerData = toData(dinnerDetail)

        // 朝、昼、晩の食費を合計する
        val totalExpenses = morningData.price + lunchData.price + dinnerData.price

        lateinit var mealData : MealData
        dbQuery { // kotlin-ktor-exposed-starter参照
            // DBに登録し、登録内容を取得
            mealData = Meal.new {
                this.name = name
                this.money = money
                this.totalExpenses = totalExpenses
                this.balance = money - totalExpenses
            }.toData() // Entity -> data class するメソッド
        }
        return mealData
    }

    /** PartData.FileItemからFileに情報を書き込む */
    private fun makeFile(fileItem: PartData.FileItem, file: File) {
        fileItem.streamProvider().use { inputStream ->
            file.outputStream().buffered().use {
                inputStream.copyTo(it)
            }
        }
    }

    /** Fileからデータクラスに変換 */
    private fun mappingData(file: File) : DetailData {
        val mapper = ObjectMapper().registerModule(KotlinModule())
        val text = file.readText(charset("UTF-8"))
        return mapper.readValue(text, DetailData::class.java)
    }

```

## まとめ
複数ファイルをmultiPartで送信しないといけない、なんてことは滅多にないとは思いますが、
もしそのようなことがあれば何かの参考になれば幸いです。