From b9c4e5cd120881df808a6fed308c9a385046d916 Mon Sep 17 00:00:00 2001 From: Willem Veelenturf Date: Sun, 5 Nov 2023 01:06:45 +0100 Subject: [PATCH] Java sealed classes (#117) --- .../open_api_app/java/JavaPetstoreClient.kt | 16 ++-- .../java/JavaPetstoreController.kt | 4 +- .../kotlin/KotlinPetstoreClient.kt | 14 ++-- .../compiler/core/emit/JavaEmitter.kt | 75 ++++++++++++------- .../compiler/core/emit/KotlinEmitter.kt | 22 +++--- .../wirespec/openapi/v3/OpenApiParser.kt | 5 +- .../maven/src/main/kotlin/GenerateMojo.kt | 9 ++- 7 files changed, 87 insertions(+), 58 deletions(-) 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 58e94ed0..c9b12103 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 @@ -10,6 +10,7 @@ import org.springframework.http.HttpMethod import org.springframework.web.client.RestTemplate import java.lang.reflect.Type import java.net.URI +import java.util.concurrent.CompletableFuture interface JavaPetstoreClient : AddPet, FindPetsByStatus @@ -38,8 +39,8 @@ class JavaPetClientConfiguration { object : JavaPetstoreClient { fun , Res : Wirespec.Response<*>> handle( request: Req, - responseMapper: (Wirespec.ContentMapper, Int, Map>, Wirespec.Content) -> Res - ):Res = restTemplate.execute( + responseMapper: (Wirespec.ContentMapper, Wirespec.Response) -> Res + ):CompletableFuture = restTemplate.execute( URI("https://6467e16be99f0ba0a819fd68.mockapi.io${request.path}"), HttpMethod.valueOf(request.method.name), { req -> @@ -50,16 +51,21 @@ class JavaPetClientConfiguration { { res -> val contentType = res.headers.contentType?.toString() ?: error("No content type") val content = Wirespec.Content(contentType, res.body.readBytes()) - responseMapper(javaContentMapper, res.statusCode.value(), res.headers, content) + val response = object :Wirespec.Response{ + override val status = res.statusCode.value() + override val headers = res.headers + override val content = content + } + CompletableFuture.completedFuture(responseMapper(javaContentMapper, response)) } ) ?: error("No response") - override fun addPet(request: AddPet.Request<*>): AddPet.Response<*> { + override fun addPet(request: AddPet.Request<*>): CompletableFuture> { return handle(request, AddPet::RESPONSE_MAPPER) } - override fun findPetsByStatus(request: FindPetsByStatus.Request<*>): FindPetsByStatus.Response<*> { + override fun findPetsByStatus(request: FindPetsByStatus.Request<*>): CompletableFuture> { return handle(request, FindPetsByStatus::RESPONSE_MAPPER) } 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 ca7ecc32..44a09685 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 @@ -18,7 +18,7 @@ class JavaPetstoreController( suspend fun addPet(): Optional? { 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)) { + return when (val res = javaPetstoreClient.addPet(req).get()) { is AddPet.Response200ApplicationJson -> res.content?.body?.id else -> error("No response") } @@ -27,7 +27,7 @@ class JavaPetstoreController( @PostMapping suspend fun create(@RequestBody pet: Pet): List { val req = FindPetsByStatus.RequestVoid(Optional.of(FindPetsByStatusParameterStatus.available)) - return when (val res = javaPetstoreClient.findPetsByStatus(req)) { + return when (val res = javaPetstoreClient.findPetsByStatus(req).get()) { 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 edd947ea..d2ceec8f 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 @@ -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, Wirespec.Response) -> Res ) = restTemplate.execute( URI("https://6467e16be99f0ba0a819fd68.mockapi.io${request.path}"), HttpMethod.valueOf(request.method.name), @@ -54,12 +54,12 @@ class KotlinPetClientConfiguration { { res -> val contentType = res.headers.contentType?.toString() ?: error("No content type") val content = Wirespec.Content(contentType, res.body.readBytes()) - responseMapper( - kotlinContentMapper, - res.statusCode.value(), - res.headers, - content - ) + val response = object :Wirespec.Response{ + override val status = res.statusCode.value() + override val headers = res.headers + override val content = content + } + responseMapper(kotlinContentMapper, response) } ) ?: error("No response") 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 f13f3346..6fc69708 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 @@ -44,7 +44,8 @@ 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.Wirespec;\n\n" + private fun import(ast: AST) = + if (!ast.hasEndpoints()) "" else "import community.flock.wirespec.Wirespec;\nimport java.util.concurrent.CompletableFuture;\n\n" override fun emit(ast: AST): List> = super.emit(ast) .map { (name, result) -> name to "$pkg\n\n${import(ast)}$result" } @@ -64,7 +65,14 @@ class JavaEmitter( "$SPACER${if (isNullable) "java.util.Optional<${reference.emit()}>" else reference.emit()} ${identifier.emit()}" } - override fun Type.Shape.Field.Identifier.emit() = withLogging(logger) { value } + override fun Type.Shape.Field.Identifier.emit() = withLogging(logger) { + value + .split("-") + .mapIndexed { index, s -> if (index > 0) s.firstToUpper() else s } + .joinToString("") + .sanitizeKeywords() + .sanitizeSymbols() + } private fun Reference.emitPrimaryType() = withLogging(logger) { when (this) { @@ -84,7 +92,7 @@ class JavaEmitter( } override fun Enum.emit() = withLogging(logger) { - fun String.sanitize() = replace("-", "_").let { if(it.first().isDigit()) "_$it" else it } + fun String.sanitize() = replace("-", "_").let { if (it.first().isDigit()) "_$it" else it } val body = """ |${SPACER}public final String label; |${SPACER}$name(String label) { @@ -97,7 +105,7 @@ class JavaEmitter( |${SPACER}${SPACER}return label; |${SPACER}} """.trimMargin() - "public enum $name {\n${SPACER}${entries.joinToString(",\n${SPACER}"){ enum -> "${enum.sanitize()}(\"${enum}\")"}};\n${body}\n${toString}\n}\n" + "public enum $name {\n${SPACER}${entries.joinToString(",\n${SPACER}") { enum -> "${enum.sanitize()}(\"${enum}\")" }};\n${body}\n${toString}\n}\n" } override fun Refined.emit() = withLogging(logger) { @@ -117,25 +125,25 @@ class JavaEmitter( """public interface $name { |${SPACER}static String PATH = "${path.emitSegment()}"; |${responses.emitResponseMapper()} - |${SPACER}interface Request extends Wirespec.Request {} + |${SPACER}sealed interface Request extends Wirespec.Request {} |${requests.joinToString("\n") { it.emit(this) }} - |${SPACER}interface Response extends Wirespec.Response {} + |${SPACER}sealed interface Response extends Wirespec.Response {} |${ responses.map { it.status.groupStatus() }.toSet() - .joinToString("\n") { "${SPACER}interface Response${it} extends Response{};" } + .joinToString("\n") { "${SPACER}sealed interface Response${it} extends Response{};" } } |${ responses.filter { it.status.isInt() }.map { it.status }.toSet() - .joinToString("\n") { "${SPACER}interface Response${it} extends Response${it.groupStatus()}{};" } + .joinToString("\n") { "${SPACER}sealed interface Response${it} extends Response${it.groupStatus()}{};" } } |${responses.distinctBy { it.status to it.content?.type }.joinToString("\n") { it.emit() }} - |${SPACER}public Response ${name.firstToLower()}(Request request); + |${SPACER}public CompletableFuture ${name.firstToLower()}(Request request); |} |""".trimMargin() } private fun Endpoint.Request.emit(endpoint: Endpoint) = """ - |${SPACER}class Request${content.emitContentType()} implements Request<${content?.reference?.emit() ?: "Void"}> { + |${SPACER}final class Request${content.emitContentType()} implements Request<${content?.reference?.emit() ?: "Void"}> { |${SPACER}${SPACER}private final String path; |${SPACER}${SPACER}private final Wirespec.Method method; |${SPACER}${SPACER}private final java.util.Map> query; @@ -157,8 +165,8 @@ class JavaEmitter( """.trimMargin() private fun Endpoint.Response.emit() = """ - |${SPACER}class Response${status.firstToUpper()}${content.emitContentType()} implements Response${ - status.takeIf { it.isInt() }?.groupStatus().orEmptyString() + |${SPACER}final class Response${status.firstToUpper()}${content.emitContentType()} implements Response${ + status.firstToUpper().orEmptyString() }<${content?.reference?.emit() ?: "Void"}> { |${SPACER}${SPACER}private final int status; |${SPACER}${SPACER}private final java.util.Map> headers; @@ -177,7 +185,7 @@ class JavaEmitter( """.trimMargin() private fun List.emitResponseMapper() = """ - |${SPACER}static Response RESPONSE_MAPPER(Wirespec.ContentMapper contentMapper, int status, java.util.Map> headers, Wirespec.Content content) { + |${SPACER}static Response RESPONSE_MAPPER(Wirespec.ContentMapper contentMapper, Wirespec.Response response) { |${distinctBy { it.status to it.content?.type }.joinToString("") { it.emitResponseMapperCondition() }} |${SPACER}${SPACER}throw new IllegalStateException("Unknown response type"); |${SPACER}} @@ -187,21 +195,21 @@ class JavaEmitter( when (content) { null -> """ |${SPACER}${SPACER}${SPACER}if(${ - status.takeIf { it.isInt() }?.let { "status == $status && " }.orEmptyString() - }content == null) { return new Response${status.firstToUpper()}Void(${ - status.takeIf { !it.isInt() }?.let { "status, " }.orEmptyString() - }headers); } + status.takeIf { it.isInt() }?.let { "response.getStatus() == $status && " }.orEmptyString() + }response.getContent() == null) { return new Response${status.firstToUpper()}Void(${ + status.takeIf { !it.isInt() }?.let { "response.getStatus(), " }.orEmptyString() + }response.getHeaders()); } | """.trimMargin() else -> """ |${SPACER}${SPACER}${SPACER}if(${ - status.takeIf { it.isInt() }?.let { "status == $status && " }.orEmptyString() - }content.type().equals("${content.type}")) { - |${SPACER}${SPACER}${SPACER}${SPACER}Wirespec.Content<${content.reference.emit()}> c = contentMapper.read(content, Wirespec.getType(${content.reference.emitPrimaryType()}.class, ${content.reference.isIterable})); + status.takeIf { it.isInt() }?.let { "response.getStatus() == $status && " }.orEmptyString() + }response.getContent().type().equals("${content.type}")) { + |${SPACER}${SPACER}${SPACER}${SPACER}Wirespec.Content<${content.reference.emit()}> content = contentMapper.read(response.getContent(), Wirespec.getType(${content.reference.emitPrimaryType()}.class, ${content.reference.isIterable})); |${SPACER}${SPACER}${SPACER}${SPACER}return new Response${status.firstToUpper()}${content.emitContentType()}(${ - status.takeIf { !it.isInt() }?.let { "status, " }.orEmptyString() - }headers, c.body()); + status.takeIf { !it.isInt() }?.let { "response.getStatus(), " }.orEmptyString() + }response.getHeaders(), content.body()); |${SPACER}${SPACER}${SPACER}} | """.trimMargin() @@ -231,11 +239,7 @@ class JavaEmitter( .joinToString(", ") { it.emit() } } - private fun List.emitMap() = joinToString( - ", ", - "java.util.Map.of(", - ")" - ) { "\"${it.identifier.emit()}\", java.util.List.of(${it.identifier.emit()})" } + private fun List.emitMap() = joinToString(", ", "java.util.Map.ofEntries(", ")") { "java.util.Map.entry(\"${it.identifier.value}\", java.util.List.of(${it.identifier.emit()}))" } private fun List.emitSegment() = "/" + joinToString("/") { when (it) { @@ -254,4 +258,21 @@ class JavaEmitter( if (isInt()) substring(0, 1) + "XX" else firstToUpper() + fun String.sanitizeKeywords() = if (reservedKeywords.contains(this)) "_$this" else this + + fun String.sanitizeSymbols() = replace(".", "") + companion object { + private val reservedKeywords = listOf( + "abstract", "continue", "for", "new", "switch", + "assert", "default", "goto", "package", "synchronized", + "boolean", "do", "if", "private", "this", + "break", "double", "implements", "protected", "throw", + "byte", "else", "import", "public", "throws", + "case", "enum", "instanceof", "return", "transient", + "catch", "extends", "int", "short", "try", + "char", "final", "interface", "static", "void", + "class", "finally", "long", "strictfp", "volatile", + "const", "float", "native", "super", "while" + ) + } } 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 a206cdf0..9d630c2c 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 @@ -174,7 +174,7 @@ class KotlinEmitter( } private fun List.emitRequestMapper() = """ - |${SPACER}${SPACER}fun REQUEST_MAPPER(contentMapper: Wirespec.ContentMapper, path:String, method: Wirespec.Method, query: Map>, headers:Map>, content: Wirespec.Content?) = + |${SPACER}${SPACER}fun REQUEST_MAPPER(contentMapper: Wirespec.ContentMapper, request: Wirespec.Request) = |${SPACER}${SPACER}${SPACER}when { |${joinToString("\n") { it.emitRequestMapperCondition() }} |${SPACER}${SPACER}${SPACER}${SPACER}else -> error("Cannot map request") @@ -184,35 +184,35 @@ class KotlinEmitter( private fun Endpoint.Request.emitRequestMapperCondition() = when (content) { null -> """ - |${SPACER}${SPACER}${SPACER}${SPACER}content == null -> RequestUnit(path, method, query, headers, null) + |${SPACER}${SPACER}${SPACER}${SPACER}request.content == null -> RequestUnit(request.path, request.method, request.query, request.headers, null) """.trimMargin() else -> """ - |${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) } + |${SPACER}${SPACER}${SPACER}${SPACER}request.content?.type == "${content.type}" -> contentMapper + |${SPACER}${SPACER}${SPACER}${SPACER}${SPACER}.read<${content.reference.emit()}>(request.content!!, Wirespec.getType(${content.reference.emitPrimaryType()}::class.java, ${content.reference.isIterable})) + |${SPACER}${SPACER}${SPACER}${SPACER}${SPACER}.let{ Request${content.emitContentType()}(request.path, request.method, request.query, request.headers, it) } """.trimMargin() } private fun List.emitResponseMapper() = """ - |${SPACER}${SPACER}fun RESPONSE_MAPPER(contentMapper: Wirespec.ContentMapper, status: Int, headers:Map>, content: Wirespec.Content?) = + |${SPACER}${SPACER}fun RESPONSE_MAPPER(contentMapper: Wirespec.ContentMapper, response: Wirespec.Response) = |${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}${SPACER}else -> error("Cannot map response with status ${"$"}status") + |${SPACER}${SPACER}${SPACER}${SPACER}else -> error("Cannot map response with status ${"$"}response.status") |${SPACER}${SPACER}${SPACER}} """.trimMargin() private fun Endpoint.Response.emitResponseMapperCondition() = when (content) { null -> """ - |${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) + |${SPACER}${SPACER}${SPACER}${SPACER}${status.takeIf { it.isInt() }?.let { "response.status == $status && " }.orEmptyString()}response.content == null -> Response${status.firstToUpper()}Unit(${status.takeIf { !it.isInt() }?.let { "response.status, " }.orEmptyString()}response.headers) """.trimMargin() else -> """ - |${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) } + |${SPACER}${SPACER}${SPACER}${SPACER}${status.takeIf { it.isInt() }?.let { "response.status == $status && " }.orEmptyString()}response.content?.type == "${content.type}" -> contentMapper + |${SPACER}${SPACER}${SPACER}${SPACER}${SPACER}.read<${content.reference.emit()}>(response.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 { "response.status, " }.orEmptyString()}response.headers, it.body) } """.trimMargin() } diff --git a/src/openapi/src/commonMain/kotlin/community/flock/wirespec/openapi/v3/OpenApiParser.kt b/src/openapi/src/commonMain/kotlin/community/flock/wirespec/openapi/v3/OpenApiParser.kt index a3723575..452efbc8 100644 --- a/src/openapi/src/commonMain/kotlin/community/flock/wirespec/openapi/v3/OpenApiParser.kt +++ b/src/openapi/src/commonMain/kotlin/community/flock/wirespec/openapi/v3/OpenApiParser.kt @@ -470,8 +470,9 @@ class OpenApiParser(private val openApi: OpenAPIObject) { is SchemaObject -> { Field( identifier = Field.Identifier(key), - reference = when(value.type){ - OpenapiType.ARRAY -> value.toReference(className(name, key, "Array")) + reference = when{ + value.enum != null -> value.toReference(className(name, key)) + value.type == OpenapiType.ARRAY -> value.toReference(className(name, key, "Array")) else -> value.toReference(className(name, key)) }, isNullable = !(this.required?.contains(key) ?: false) diff --git a/src/plugin/maven/src/main/kotlin/GenerateMojo.kt b/src/plugin/maven/src/main/kotlin/GenerateMojo.kt index fec82148..d55d7b8b 100644 --- a/src/plugin/maven/src/main/kotlin/GenerateMojo.kt +++ b/src/plugin/maven/src/main/kotlin/GenerateMojo.kt @@ -35,7 +35,7 @@ class GenerateMojo : BaseMojo() { private var languages: List? = null @Parameter - private var shared: Boolean? = null + private var shared: Boolean = true @Parameter(defaultValue = "\${project}", readonly = true, required = true) private lateinit var project: MavenProject @@ -50,11 +50,12 @@ class GenerateMojo : BaseMojo() { Language.Wirespec -> TODO() } } + project.addCompileSourceRoot(output); } fun executeKotlin() { val emitter = KotlinEmitter(packageName, logger) - if(shared == true) { + if(shared) { JvmUtil.emitJvm("community.flock.wirespec", output, "Wirespec", "kt").writeText(emitter.shared) } if (openapi != null) { @@ -80,7 +81,7 @@ class GenerateMojo : BaseMojo() { fun executeJava() { val emitter = JavaEmitter(packageName, logger) - if(shared == true) { + if(shared) { JvmUtil.emitJvm("community.flock.wirespec", output, "Wirespec", "java").writeText(emitter.shared) } if (openapi != null) { @@ -103,7 +104,7 @@ class GenerateMojo : BaseMojo() { fun executeScala() { val emitter = ScalaEmitter(packageName, logger) - if(shared == true) { + if(shared) { JvmUtil.emitJvm("community.flock.wirespec", output, "Wirespec", "scala").writeText(emitter.shared) } compile(input, logger, emitter)