Skip to content

Commit

Permalink
Harmonize Kotlin and Java emitter (#108)
Browse files Browse the repository at this point in the history
  • Loading branch information
wilmveel authored Nov 1, 2023
1 parent 6d484e5 commit dde53a6
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 49 deletions.
30 changes: 30 additions & 0 deletions examples/spring-boot-openapi-maven-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
<languages>
<language>Kotlin</language>
</languages>
<shared>true</shared>
</configuration>
</execution>
<execution>
Expand All @@ -104,6 +105,7 @@
<languages>
<language>Kotlin</language>
</languages>
<shared>false</shared>
</configuration>
</execution>
<execution>
Expand All @@ -119,6 +121,7 @@
<languages>
<language>Java</language>
</languages>
<shared>false</shared>
</configuration>
</execution>
<execution>
Expand All @@ -134,6 +137,7 @@
<languages>
<language>Java</language>
</languages>
<shared>false</shared>
</configuration>
</execution>
</executions>
Expand All @@ -157,6 +161,32 @@
</execution>
</executions>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<id>default-compile</id>
<phase>none</phase>
</execution>
<execution>
<id>default-testCompile</id>
<phase>none</phase>
</execution>
<execution>
<id>java-compile</id>
<phase>compile</phase>
<goals> <goal>compile</goal> </goals>
</execution>
<execution>
<id>java-test-compile</id>
<phase>test-compile</phase>
<goals> <goal>testCompile</goal> </goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
package community.flock.wirespec.examples.open_api_app.java

import com.fasterxml.jackson.databind.ObjectMapper
import community.flock.wirespec.java.Wirespec.Request
import community.flock.wirespec.java.Wirespec.Response
import community.flock.wirespec.java.Wirespec.Content
import community.flock.wirespec.java.Wirespec.ContentMapper
import community.flock.wirespec.Wirespec
import community.flock.wirespec.generated.java.v3.AddPet
import community.flock.wirespec.generated.java.v3.FindPetsByStatus
import org.springframework.context.annotation.Bean
Expand All @@ -21,27 +18,27 @@ class JavaPetClientConfiguration {

@Bean
fun javaContentMapper(objectMapper: ObjectMapper) =
object : ContentMapper<ByteArray> {
override fun <T> read(content: Content<ByteArray>, valueType: Type): Content<T> = content.let {
object : Wirespec.ContentMapper<ByteArray> {
override fun <T> read(content: Wirespec.Content<ByteArray>, valueType: Type): Wirespec.Content<T> = content.let {
val type = objectMapper.constructType(valueType)
val obj: T = objectMapper.readValue(content.body, type)
Content(it.type, obj)
Wirespec.Content(it.type, obj)
}

override fun <T> write(content: Content<T>): Content<ByteArray> = content.let {
override fun <T> write(content: Wirespec.Content<T>): Wirespec.Content<ByteArray> = content.let {
val bytes = objectMapper.writeValueAsBytes(content.body)
Content(it.type, bytes)
Wirespec.Content(it.type, bytes)
}
}


@Bean
fun javaPetstoreClient(restTemplate: RestTemplate, javaContentMapper: ContentMapper<ByteArray>): JavaPetstoreClient =
fun javaPetstoreClient(restTemplate: RestTemplate, javaContentMapper: Wirespec.ContentMapper<ByteArray>): JavaPetstoreClient =

object : JavaPetstoreClient {
fun <Req : Request<*>, Res : Response<*>> handle(
fun <Req : Wirespec.Request<*>, Res : Wirespec.Response<*>> handle(
request: Req,
responseMapper: (ContentMapper<ByteArray>, Int, Map<String, List<String>>, Content<ByteArray>) -> Res
responseMapper: (Wirespec.ContentMapper<ByteArray>, Int, Map<String, List<String>>, Wirespec.Content<ByteArray>) -> Res
):Res = restTemplate.execute(
URI("https://6467e16be99f0ba0a819fd68.mockapi.io${request.path}"),
HttpMethod.valueOf(request.method.name),
Expand All @@ -52,7 +49,7 @@ class JavaPetClientConfiguration {
},
{ res ->
val contentType = res.headers.contentType?.toString() ?: error("No content type")
val content = Content(contentType, res.body.readBytes())
val content = Wirespec.Content(contentType, res.body.readBytes())
responseMapper(javaContentMapper, res.statusCode.value(), res.headers, content)
}
) ?: error("No response")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class JavaPetstoreController(
val pet = Pet(Optional.empty(), "Petje", Optional.empty(), emptyList(), Optional.empty(), Optional.empty())
val req = AddPet.RequestApplicationJson(pet)
return when (val res = javaPetstoreClient.addPet(req)) {
is AddPet.Response200ApplicationJson -> res.content.body.id
is AddPet.Response200ApplicationJson -> res.content?.body?.id
else -> error("No response")
}
}
Expand All @@ -28,7 +28,7 @@ class JavaPetstoreController(
suspend fun create(@RequestBody pet: Pet): List<Int> {
val req = FindPetsByStatus.RequestVoid(Optional.of(FindPetsByStatusParameterStatus.available))
return when (val res = javaPetstoreClient.findPetsByStatus(req)) {
is FindPetsByStatus.Response200ApplicationJson -> res.content.body.mapNotNull { it.id.getOrNull() }
is FindPetsByStatus.Response200ApplicationJson -> res.content?.body?.mapNotNull { it.id.getOrNull() } ?: emptyList()
else -> error("No response")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package community.flock.wirespec.examples.open_api_app.kotlin
import com.fasterxml.jackson.databind.ObjectMapper
import community.flock.wirespec.generated.kotlin.v3.AddPet
import community.flock.wirespec.generated.kotlin.v3.FindPetsByStatus
import community.flock.wirespec.kotlin.Wirespec
import community.flock.wirespec.Wirespec
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod
Expand Down Expand Up @@ -42,7 +42,7 @@ class KotlinPetClientConfiguration {
object : KotlinPetstoreClient {
fun <Req : Wirespec.Request<*>, Res : Wirespec.Response<*>> handle(
request: Req,
responseMapper: (Wirespec.ContentMapper<ByteArray>) -> (Int, Map<String, List<String>>, Wirespec.Content<ByteArray>) -> Res
responseMapper: (Wirespec.ContentMapper<ByteArray>, Int, Map<String, List<String>>, Wirespec.Content<ByteArray>) -> Res
) = restTemplate.execute(
URI("https://6467e16be99f0ba0a819fd68.mockapi.io${request.path}"),
HttpMethod.valueOf(request.method.name),
Expand All @@ -54,7 +54,8 @@ class KotlinPetClientConfiguration {
{ res ->
val contentType = res.headers.contentType?.toString() ?: error("No content type")
val content = Wirespec.Content(contentType, res.body.readBytes())
responseMapper(kotlinContentMapper)(
responseMapper(
kotlinContentMapper,
res.statusCode.value(),
res.headers,
content
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class JavaEmitter(
) : Emitter(logger, true) {

override val shared = """
|package community.flock.wirespec.java;
|package community.flock.wirespec;
|
|import java.lang.reflect.Type;
|import java.lang.reflect.ParameterizedType;
Expand All @@ -44,7 +44,7 @@ class JavaEmitter(
""".trimMargin()

private val pkg = if (packageName.isBlank()) "" else "package $packageName;"
private fun import(ast:AST) = if (!ast.hasEndpoints()) "" else "import community.flock.wirespec.java.Wirespec;\n\n"
private fun import(ast:AST) = if (!ast.hasEndpoints()) "" else "import community.flock.wirespec.Wirespec;\n\n"

override fun emit(ast: AST): List<Pair<String, String>> = super.emit(ast)
.map { (name, result) -> name to "$pkg\n\n${import(ast)}$result" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,36 @@ class KotlinEmitter(
) : Emitter(logger) {

override val shared = """
|package community.flock.wirespec.kotlin
|package community.flock.wirespec
|
|import java.lang.reflect.Type
|import java.lang.reflect.ParameterizedType
|
|interface Wirespec {
|${SPACER}enum class Method { GET, PUT, POST, DELETE, OPTIONS, HEAD, PATCH, TRACE }
|${SPACER}data class Content<T> (val type:String, val body:T )
|${SPACER}@JvmRecord data class Content<T> (val type:String, val body:T )
|${SPACER}interface Request<T> { val path:String; val method: Method; val query: Map<String, List<Any?>>; val headers: Map<String, List<Any?>>; val content:Content<T>? }
|${SPACER}interface Response<T> { val status:Int; val headers: Map<String, List<Any?>>; val content:Content<T>? }
|${SPACER}interface ContentMapper<B> { fun <T> read(content: Content<B>, valueType: Type): Content<T> fun <T> write(content: Content<T>): Content<B> }
|${SPACER}companion object {
|${SPACER}${SPACER}@JvmStatic fun getType(type: Class<*>, isIterable: Boolean): Type {
|${SPACER}${SPACER}${SPACER}return if (isIterable) {
|${SPACER}${SPACER}${SPACER}${SPACER}object : ParameterizedType {
|${SPACER}${SPACER}${SPACER}${SPACER}${SPACER}override fun getRawType() = MutableList::class.java
|${SPACER}${SPACER}${SPACER}${SPACER}${SPACER}override fun getActualTypeArguments() = arrayOf(type)
|${SPACER}${SPACER}${SPACER}${SPACER}${SPACER}override fun getOwnerType() = null
|${SPACER}${SPACER}${SPACER}${SPACER}}
|${SPACER}${SPACER}${SPACER}} else {
|${SPACER}${SPACER}${SPACER}${SPACER}type
|${SPACER}${SPACER}${SPACER}}
|${SPACER}${SPACER}}
|${SPACER}}
|}
""".trimMargin()

val import = """
|import kotlin.reflect.typeOf
|import kotlin.reflect.jvm.javaType
|import community.flock.wirespec.kotlin.Wirespec
|
|import community.flock.wirespec.Wirespec
|
""".trimMargin()

Expand Down Expand Up @@ -72,7 +85,7 @@ class KotlinEmitter(
.sanitizeSymbols()
}

override fun Reference.emit() = withLogging(logger) {
private fun Reference.emitPrimaryType() = withLogging(logger) {
when (this) {
is Reference.Any -> "Any"
is Reference.Custom -> value
Expand All @@ -82,6 +95,10 @@ class KotlinEmitter(
Reference.Primitive.Type.Boolean -> "Boolean"
}
}
}

override fun Reference.emit() = withLogging(logger) {
emitPrimaryType()
.let { if (isIterable) "List<$it>" else it }
.let { if (isMap) "Map<String, $it>" else it }
}
Expand All @@ -108,11 +125,11 @@ class KotlinEmitter(
|${responses.filter { it.status.isInt() }.map { it.status }.toSet().joinToString("\n") { "${SPACER}sealed interface Response${it}<T>: Response${it.groupStatus()}<T>" }}
|${responses.filter { it.status.isInt() }.distinctBy { it.status to it.content?.type }.joinToString("\n") { "${SPACER}class Response${it.status}${it.content?.emitContentType() ?: "Unit"} (override val headers: Map<String, List<Any?>>${it.content?.let { ", body: ${it.reference.emit()}" } ?: ""} ): Response${it.status}<${it.content?.reference?.emit() ?: "Unit"}> { override val status = ${it.status}; override val content = ${it.content?.let { "Wirespec.Content(\"${it.type}\", body)" } ?: "null"}}" }}
|${responses.filter { !it.status.isInt() }.distinctBy { it.status to it.content?.type }.joinToString("\n") { "${SPACER}class Response${it.status.firstToUpper()}${it.content?.emitContentType() ?: "Unit"} (override val status: Int, override val headers: Map<String, List<Any?>>${it.content?.let { ", body: ${it.reference.emit()}" } ?: ""} ): Response${it.status.firstToUpper()}<${it.content?.reference?.emit() ?: "Unit"}> { override val content = ${it.content?.let { "Wirespec.Content(\"${it.type}\", body)" } ?: "null"}}" }}
|suspend fun ${name.firstToLower()}(request: Request<*>): Response<*>
|${SPACER}suspend fun ${name.firstToLower()}(request: Request<*>): Response<*>
|${SPACER}companion object{
|${SPACER}${SPACER}const val PATH = "${path.emitSegment()}"
|${SPACER}${SPACER}${requests.emitRequestMapper()}
|${SPACER}${SPACER}${responses.emitResponseMapper()}
|${requests.emitRequestMapper()}
|${responses.emitResponseMapper()}
|${SPACER}}
|}
|""".trimMargin()
Expand Down Expand Up @@ -157,47 +174,45 @@ class KotlinEmitter(
}

private fun List<Endpoint.Request>.emitRequestMapper() = """
|fun <B> REQUEST_MAPPER(contentMapper: Wirespec.ContentMapper<B>) =
|${SPACER}fun(path:String, method: Wirespec.Method, query: Map<String, List<Any?>>, headers:Map<String, List<Any?>>, content: Wirespec.Content<B>?) =
|${SPACER}${SPACER}when {
|${SPACER}${SPACER}fun <B> REQUEST_MAPPER(contentMapper: Wirespec.ContentMapper<B>, path:String, method: Wirespec.Method, query: Map<String, List<Any?>>, headers:Map<String, List<Any?>>, content: Wirespec.Content<B>?) =
|${SPACER}${SPACER}${SPACER}when {
|${joinToString("\n") { it.emitRequestMapperCondition() }}
|${SPACER}${SPACER}${SPACER}else -> error("Cannot map request")
|${SPACER}${SPACER}}
|${SPACER}${SPACER}${SPACER}${SPACER}else -> error("Cannot map request")
|${SPACER}${SPACER}${SPACER}}
""".trimMargin()

private fun Endpoint.Request.emitRequestMapperCondition() =
when (content) {
null -> """
|${SPACER}${SPACER}${SPACER}content == null -> RequestUnit(path, method, query, headers, null)
|${SPACER}${SPACER}${SPACER}${SPACER}content == null -> RequestUnit(path, method, query, headers, null)
""".trimMargin()

else -> """
|${SPACER}${SPACER}${SPACER}content?.type == "${content.type}" -> contentMapper
|${SPACER}${SPACER}${SPACER}${SPACER}.read<${content.reference.emit()}>(content, typeOf<${content.reference.emit()}>().javaType)
|${SPACER}${SPACER}${SPACER}${SPACER}.let{ Request${content.emitContentType()}(path, method, query, headers, it) }
|${SPACER}${SPACER}${SPACER}${SPACER}content?.type == "${content.type}" -> contentMapper
|${SPACER}${SPACER}${SPACER}${SPACER}${SPACER}.read<${content.reference.emit()}>(content, Wirespec.getType(${content.reference.emitPrimaryType()}::class.java, ${content.reference.isIterable}))
|${SPACER}${SPACER}${SPACER}${SPACER}${SPACER}.let{ Request${content.emitContentType()}(path, method, query, headers, it) }
""".trimMargin()
}

private fun List<Endpoint.Response>.emitResponseMapper() = """
|fun <B> RESPONSE_MAPPER(contentMapper: Wirespec.ContentMapper<B>) =
|${SPACER}fun(status: Int, headers:Map<String, List<Any?>>, content: Wirespec.Content<B>?) =
|${SPACER}${SPACER}when {
|${SPACER}${SPACER}fun <B> RESPONSE_MAPPER(contentMapper: Wirespec.ContentMapper<B>, status: Int, headers:Map<String, List<Any?>>, content: Wirespec.Content<B>?) =
|${SPACER}${SPACER}${SPACER}when {
|${filter { it.status.isInt() }.distinctBy { it.status to it.content?.type }.joinToString("\n") { it.emitResponseMapperCondition() }}
|${filter { !it.status.isInt() }.distinctBy { it.status to it.content?.type }.joinToString("\n") { it.emitResponseMapperCondition() }}
|${SPACER}${SPACER}${SPACER}else -> error("Cannot map response with status ${"$"}status")
|${SPACER}${SPACER}}
|${SPACER}${SPACER}${SPACER}${SPACER}else -> error("Cannot map response with status ${"$"}status")
|${SPACER}${SPACER}${SPACER}}
""".trimMargin()

private fun Endpoint.Response.emitResponseMapperCondition() =
when (content) {
null -> """
|${SPACER}${SPACER}${SPACER}${status.takeIf { it.isInt() }?.let { "status == $status && " }.orEmptyString()}content == null -> Response${status.firstToUpper()}Unit(${status.takeIf { !it.isInt() }?.let { "status, " }.orEmptyString()}headers)
|${SPACER}${SPACER}${SPACER}${SPACER}${status.takeIf { it.isInt() }?.let { "status == $status && " }.orEmptyString()}content == null -> Response${status.firstToUpper()}Unit(${status.takeIf { !it.isInt() }?.let { "status, " }.orEmptyString()}headers)
""".trimMargin()

else -> """
|${SPACER}${SPACER}${SPACER}${status.takeIf { it.isInt() }?.let { "status == $status && " }.orEmptyString()}content?.type == "${content.type}" -> contentMapper
|${SPACER}${SPACER}${SPACER}${SPACER}.read<${content.reference.emit()}>(content, typeOf<${content.reference.emit()}>().javaType)
|${SPACER}${SPACER}${SPACER}${SPACER}.let{ Response${status.firstToUpper()}${content.emitContentType()}(${status.takeIf { !it.isInt() }?.let { "status, " }.orEmptyString()}headers, it.body) }
|${SPACER}${SPACER}${SPACER}${SPACER}${status.takeIf { it.isInt() }?.let { "status == $status && " }.orEmptyString()}content?.type == "${content.type}" -> contentMapper
|${SPACER}${SPACER}${SPACER}${SPACER}${SPACER}.read<${content.reference.emit()}>(content, Wirespec.getType(${content.reference.emitPrimaryType()}::class.java, ${content.reference.isIterable}))
|${SPACER}${SPACER}${SPACER}${SPACER}${SPACER}.let{ Response${status.firstToUpper()}${content.emitContentType()}(${status.takeIf { !it.isInt() }?.let { "status, " }.orEmptyString()}headers, it.body) }
""".trimMargin()
}

Expand Down
Loading

0 comments on commit dde53a6

Please sign in to comment.