diff --git a/examples/spring-boot-openapi-maven-plugin/pom.xml b/examples/spring-boot-openapi-maven-plugin/pom.xml index f1bcda33..cee7a247 100644 --- a/examples/spring-boot-openapi-maven-plugin/pom.xml +++ b/examples/spring-boot-openapi-maven-plugin/pom.xml @@ -89,6 +89,7 @@ Kotlin + true @@ -104,6 +105,7 @@ Kotlin + false @@ -119,6 +121,7 @@ Java + false @@ -134,6 +137,7 @@ Java + false @@ -157,6 +161,32 @@ + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + + default-compile + none + + + default-testCompile + none + + + java-compile + compile + compile + + + java-test-compile + test-compile + testCompile + + + diff --git a/examples/spring-boot-openapi-maven-plugin/src/main/kotlin/community/flock/wirespec/examples/open_api_app/java/JavaPetstoreClient.kt b/examples/spring-boot-openapi-maven-plugin/src/main/kotlin/community/flock/wirespec/examples/open_api_app/java/JavaPetstoreClient.kt index ab30abfc..58e94ed0 100644 --- a/examples/spring-boot-openapi-maven-plugin/src/main/kotlin/community/flock/wirespec/examples/open_api_app/java/JavaPetstoreClient.kt +++ b/examples/spring-boot-openapi-maven-plugin/src/main/kotlin/community/flock/wirespec/examples/open_api_app/java/JavaPetstoreClient.kt @@ -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 @@ -21,27 +18,27 @@ class JavaPetClientConfiguration { @Bean fun javaContentMapper(objectMapper: ObjectMapper) = - object : ContentMapper { - override fun read(content: Content, valueType: Type): Content = content.let { + object : Wirespec.ContentMapper { + override fun read(content: Wirespec.Content, valueType: Type): Wirespec.Content = 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 write(content: Content): Content = content.let { + override fun write(content: Wirespec.Content): Wirespec.Content = content.let { val bytes = objectMapper.writeValueAsBytes(content.body) - Content(it.type, bytes) + Wirespec.Content(it.type, bytes) } } @Bean - fun javaPetstoreClient(restTemplate: RestTemplate, javaContentMapper: ContentMapper): JavaPetstoreClient = + fun javaPetstoreClient(restTemplate: RestTemplate, javaContentMapper: Wirespec.ContentMapper): JavaPetstoreClient = object : JavaPetstoreClient { - fun , Res : Response<*>> handle( + fun , Res : Wirespec.Response<*>> handle( request: Req, - responseMapper: (ContentMapper, Int, Map>, Content) -> Res + responseMapper: (Wirespec.ContentMapper, Int, Map>, Wirespec.Content) -> Res ):Res = restTemplate.execute( URI("https://6467e16be99f0ba0a819fd68.mockapi.io${request.path}"), HttpMethod.valueOf(request.method.name), @@ -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") diff --git a/examples/spring-boot-openapi-maven-plugin/src/main/kotlin/community/flock/wirespec/examples/open_api_app/java/JavaPetstoreController.kt b/examples/spring-boot-openapi-maven-plugin/src/main/kotlin/community/flock/wirespec/examples/open_api_app/java/JavaPetstoreController.kt index 468c539a..ca7ecc32 100644 --- a/examples/spring-boot-openapi-maven-plugin/src/main/kotlin/community/flock/wirespec/examples/open_api_app/java/JavaPetstoreController.kt +++ b/examples/spring-boot-openapi-maven-plugin/src/main/kotlin/community/flock/wirespec/examples/open_api_app/java/JavaPetstoreController.kt @@ -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") } } @@ -28,7 +28,7 @@ class JavaPetstoreController( suspend fun create(@RequestBody pet: Pet): List { 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") } } diff --git a/examples/spring-boot-openapi-maven-plugin/src/main/kotlin/community/flock/wirespec/examples/open_api_app/kotlin/KotlinPetstoreClient.kt b/examples/spring-boot-openapi-maven-plugin/src/main/kotlin/community/flock/wirespec/examples/open_api_app/kotlin/KotlinPetstoreClient.kt index 97d5b5d7..edd947ea 100644 --- a/examples/spring-boot-openapi-maven-plugin/src/main/kotlin/community/flock/wirespec/examples/open_api_app/kotlin/KotlinPetstoreClient.kt +++ b/examples/spring-boot-openapi-maven-plugin/src/main/kotlin/community/flock/wirespec/examples/open_api_app/kotlin/KotlinPetstoreClient.kt @@ -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 @@ -42,7 +42,7 @@ class KotlinPetClientConfiguration { object : KotlinPetstoreClient { fun , Res : Wirespec.Response<*>> handle( request: Req, - responseMapper: (Wirespec.ContentMapper) -> (Int, Map>, Wirespec.Content) -> Res + responseMapper: (Wirespec.ContentMapper, Int, Map>, Wirespec.Content) -> Res ) = restTemplate.execute( URI("https://6467e16be99f0ba0a819fd68.mockapi.io${request.path}"), HttpMethod.valueOf(request.method.name), @@ -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 diff --git a/src/compiler/core/src/commonMain/kotlin/community/flock/wirespec/compiler/core/emit/JavaEmitter.kt b/src/compiler/core/src/commonMain/kotlin/community/flock/wirespec/compiler/core/emit/JavaEmitter.kt index a311853f..f13f3346 100644 --- a/src/compiler/core/src/commonMain/kotlin/community/flock/wirespec/compiler/core/emit/JavaEmitter.kt +++ b/src/compiler/core/src/commonMain/kotlin/community/flock/wirespec/compiler/core/emit/JavaEmitter.kt @@ -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; @@ -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> = super.emit(ast) .map { (name, result) -> name to "$pkg\n\n${import(ast)}$result" } diff --git a/src/compiler/core/src/commonMain/kotlin/community/flock/wirespec/compiler/core/emit/KotlinEmitter.kt b/src/compiler/core/src/commonMain/kotlin/community/flock/wirespec/compiler/core/emit/KotlinEmitter.kt index 07bbeaaa..a206cdf0 100644 --- a/src/compiler/core/src/commonMain/kotlin/community/flock/wirespec/compiler/core/emit/KotlinEmitter.kt +++ b/src/compiler/core/src/commonMain/kotlin/community/flock/wirespec/compiler/core/emit/KotlinEmitter.kt @@ -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 (val type:String, val body:T ) + |${SPACER}@JvmRecord data class Content (val type:String, val body:T ) |${SPACER}interface Request { val path:String; val method: Method; val query: Map>; val headers: Map>; val content:Content? } |${SPACER}interface Response { val status:Int; val headers: Map>; val content:Content? } |${SPACER}interface ContentMapper { fun read(content: Content, valueType: Type): Content fun write(content: Content): Content } + |${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() @@ -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 @@ -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" else it } } @@ -108,11 +125,11 @@ class KotlinEmitter( |${responses.filter { it.status.isInt() }.map { it.status }.toSet().joinToString("\n") { "${SPACER}sealed interface Response${it}: Response${it.groupStatus()}" }} |${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>${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>${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() @@ -157,47 +174,45 @@ class KotlinEmitter( } private fun List.emitRequestMapper() = """ - |fun REQUEST_MAPPER(contentMapper: Wirespec.ContentMapper) = - |${SPACER}fun(path:String, method: Wirespec.Method, query: Map>, headers:Map>, content: Wirespec.Content?) = - |${SPACER}${SPACER}when { + |${SPACER}${SPACER}fun REQUEST_MAPPER(contentMapper: Wirespec.ContentMapper, path:String, method: Wirespec.Method, query: Map>, headers:Map>, content: Wirespec.Content?) = + |${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.emitResponseMapper() = """ - |fun RESPONSE_MAPPER(contentMapper: Wirespec.ContentMapper) = - |${SPACER}fun(status: Int, headers:Map>, content: Wirespec.Content?) = - |${SPACER}${SPACER}when { + |${SPACER}${SPACER}fun RESPONSE_MAPPER(contentMapper: Wirespec.ContentMapper, status: Int, headers:Map>, content: Wirespec.Content?) = + |${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() } diff --git a/src/plugin/maven/src/main/kotlin/GenerateMojo.kt b/src/plugin/maven/src/main/kotlin/GenerateMojo.kt index e17d94bb..fec82148 100644 --- a/src/plugin/maven/src/main/kotlin/GenerateMojo.kt +++ b/src/plugin/maven/src/main/kotlin/GenerateMojo.kt @@ -34,6 +34,9 @@ class GenerateMojo : BaseMojo() { @Parameter private var languages: List? = null + @Parameter + private var shared: Boolean? = null + @Parameter(defaultValue = "\${project}", readonly = true, required = true) private lateinit var project: MavenProject @@ -51,7 +54,9 @@ class GenerateMojo : BaseMojo() { fun executeKotlin() { val emitter = KotlinEmitter(packageName, logger) - JvmUtil.emitJvm("community.flock.wirespec.kotlin", output, "Wirespec", "kt").writeText(emitter.shared) + if(shared == true) { + JvmUtil.emitJvm("community.flock.wirespec", output, "Wirespec", "kt").writeText(emitter.shared) + } if (openapi != null) { val fileName = input.split("/") .last() @@ -75,7 +80,9 @@ class GenerateMojo : BaseMojo() { fun executeJava() { val emitter = JavaEmitter(packageName, logger) - JvmUtil.emitJvm("community.flock.wirespec.java", output, "Wirespec", "java").writeText(emitter.shared) + if(shared == true) { + JvmUtil.emitJvm("community.flock.wirespec", output, "Wirespec", "java").writeText(emitter.shared) + } if (openapi != null) { val json = File(input).readText() val ast = when (openapi) { @@ -96,6 +103,9 @@ class GenerateMojo : BaseMojo() { fun executeScala() { val emitter = ScalaEmitter(packageName, logger) + if(shared == true) { + JvmUtil.emitJvm("community.flock.wirespec", output, "Wirespec", "scala").writeText(emitter.shared) + } compile(input, logger, emitter) .forEach { (name, result) -> JvmUtil.emitJvm(packageName, output, name, "scala").writeText(result) } }