Skip to content

Commit

Permalink
Use KotlinPoet %T for call.receive (#288)
Browse files Browse the repository at this point in the history
Fixes an issue where invalid code generated when request body is an list of models.
  • Loading branch information
ulrikandersen authored May 23, 2024
1 parent 259d4de commit 6b73349
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -266,9 +266,10 @@ class KtorControllerInterfaceGenerator(

bodyParams.forEach { param ->
builder.addStatement(
"val ${param.name} = %M.%M<${param.type.simpleName()}>()",
"val ${param.name} = %M.%M<%T>()",
MemberName("io.ktor.server.application", "call"),
MemberName("io.ktor.server.request", "receive"),
param.type,
)
}

Expand Down Expand Up @@ -527,7 +528,4 @@ private data class IncomingParametersByType(
val bodyParams: List<BodyParameter>,
)

private fun TypeName.simpleName(): String = this.toString().split(".").last()

private fun TypeName.isUnit(): Boolean = this == Unit::class.asTypeName()
private fun TypeName.isNotUnit(): Boolean = !isUnit()
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class KtorControllerInterfaceGeneratorTest {
"singleAllOf",
"pathLevelParameters",
"parameterNameClash",
"requestBodyAsArray",
)

private fun setupGithubApiTestEnv() {
Expand Down
59 changes: 59 additions & 0 deletions src/test/resources/examples/requestBodyAsArray/api.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
openapi: 3.0.1
info:
title: Library Catalog API
description: An API to manage books in the library catalog.
version: 1.0.0
paths:
/books/batch:
post:
summary: Add a batch of books to the catalog
description: Submit a batch of books to be added to the library catalog.
operationId: addBooksBatch
requestBody:
description: A list of books to be added to the catalog.
required: true
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Book'
responses:
'201':
description: Books successfully added to the catalog
content:
application/json:
schema:
$ref: '#/components/schemas/BooksResponse'
components:
schemas:
Book:
type: object
properties:
title:
type: string
description: The title of the book.
example: "The Great Gatsby"
author:
type: string
description: The author of the book.
example: "F. Scott Fitzgerald"
isbn:
type: string
description: The ISBN number of the book.
example: "978-0743273565"
required:
- title
- author
- isbn
BooksResponse:
type: object
properties:
message:
type: string
example: "Books added successfully!"
bookIds:
type: array
items:
type: string
example: ["book_123456", "book_789012"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package examples.requestBodyAsArray.controllers

import examples.requestBodyAsArray.models.Book
import examples.requestBodyAsArray.models.BooksResponse
import io.ktor.http.Headers
import io.ktor.http.HttpStatusCode
import io.ktor.http.Parameters
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.call
import io.ktor.server.plugins.BadRequestException
import io.ktor.server.plugins.ParameterConversionException
import io.ktor.server.request.receive
import io.ktor.server.response.respond
import io.ktor.server.routing.Route
import io.ktor.server.routing.post
import io.ktor.util.converters.DefaultConversionService
import io.ktor.util.reflect.typeInfo
import kotlin.Any
import kotlin.String
import kotlin.Suppress
import kotlin.collections.List

public interface BooksBatchController {
/**
* Add a batch of books to the catalog
* Submit a batch of books to be added to the library catalog.
*
* Route is expected to respond with [examples.requestBodyAsArray.models.BooksResponse].
* Use [examples.requestBodyAsArray.controllers.TypedApplicationCall.respondTyped] to send the
* response.
*
* @param requestBody A list of books to be added to the catalog.
* @param call Decorated ApplicationCall with additional typed respond methods
*/
public suspend fun addBooksBatch(
requestBody: List<Book>,
call: TypedApplicationCall<BooksResponse>,
)

public companion object {
/**
* Mounts all routes for the BooksBatch resource
*
* - POST /books/batch Add a batch of books to the catalog
*/
public fun Route.booksBatchRoutes(controller: BooksBatchController) {
post("/books/batch") {
val requestBody = call.receive<List<Book>>()
controller.addBooksBatch(requestBody, TypedApplicationCall(call))
}
}

/**
* Gets parameter value associated with this name or null if the name is not present.
* Converting to type R using DefaultConversionService.
*
* Throws:
* ParameterConversionException - when conversion from String to R fails
*/
private inline fun <reified R : Any> Parameters.getTyped(name: String): R? {
val values = getAll(name) ?: return null
val typeInfo = typeInfo<R>()
return try {
@Suppress("UNCHECKED_CAST")
DefaultConversionService.fromValues(values, typeInfo) as R
} catch (cause: Exception) {
throw ParameterConversionException(
name,
typeInfo.type.simpleName
?: typeInfo.type.toString(),
cause,
)
}
}

/**
* Gets first value from the list of values associated with a name.
*
* Throws:
* BadRequestException - when the name is not present
*/
private fun Headers.getOrFail(name: String): String = this[name] ?: throw
BadRequestException("Header " + name + " is required")
}
}

/**
* Decorator for Ktor's ApplicationCall that provides type safe variants of the [respond] functions.
*
* It can be used as a drop-in replacement for [io.ktor.server.application.ApplicationCall].
*
* @param R The type of the response body
*/
public class TypedApplicationCall<R : Any>(
private val applicationCall: ApplicationCall,
) : ApplicationCall by applicationCall {
@Suppress("unused")
public suspend inline fun <reified T : R> respondTyped(message: T) {
respond(message)
}

@Suppress("unused")
public suspend inline fun <reified T : R> respondTyped(status: HttpStatusCode, message: T) {
respond(status, message)
}
}

0 comments on commit 6b73349

Please sign in to comment.