Skip to content

Commit

Permalink
Java sealed classes (#117)
Browse files Browse the repository at this point in the history
  • Loading branch information
wilmveel authored Nov 5, 2023
1 parent 91276db commit b9c4e5c
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -38,8 +39,8 @@ class JavaPetClientConfiguration {
object : JavaPetstoreClient {
fun <Req : Wirespec.Request<*>, Res : Wirespec.Response<*>> handle(
request: Req,
responseMapper: (Wirespec.ContentMapper<ByteArray>, Int, Map<String, List<String>>, Wirespec.Content<ByteArray>) -> Res
):Res = restTemplate.execute(
responseMapper: (Wirespec.ContentMapper<ByteArray>, Wirespec.Response<ByteArray>) -> Res
):CompletableFuture<Res> = restTemplate.execute(
URI("https://6467e16be99f0ba0a819fd68.mockapi.io${request.path}"),
HttpMethod.valueOf(request.method.name),
{ req ->
Expand All @@ -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<ByteArray>{
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<AddPet.Response<*>> {
return handle(request, AddPet::RESPONSE_MAPPER)
}

override fun findPetsByStatus(request: FindPetsByStatus.Request<*>): FindPetsByStatus.Response<*> {
override fun findPetsByStatus(request: FindPetsByStatus.Request<*>): CompletableFuture<FindPetsByStatus.Response<*>> {
return handle(request, FindPetsByStatus::RESPONSE_MAPPER)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class JavaPetstoreController(
suspend fun addPet(): Optional<Int>? {
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")
}
Expand All @@ -27,7 +27,7 @@ class JavaPetstoreController(
@PostMapping
suspend fun create(@RequestBody pet: Pet): List<Int> {
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")
}
Expand Down
Original file line number Diff line number Diff line change
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>, Wirespec.Response<ByteArray>) -> Res
) = restTemplate.execute(
URI("https://6467e16be99f0ba0a819fd68.mockapi.io${request.path}"),
HttpMethod.valueOf(request.method.name),
Expand All @@ -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<ByteArray>{
override val status = res.statusCode.value()
override val headers = res.headers
override val content = content
}
responseMapper(kotlinContentMapper, response)
}
) ?: error("No response")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Pair<String, String>> = super.emit(ast)
.map { (name, result) -> name to "$pkg\n\n${import(ast)}$result" }
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -117,25 +125,25 @@ class JavaEmitter(
"""public interface $name {
|${SPACER}static String PATH = "${path.emitSegment()}";
|${responses.emitResponseMapper()}
|${SPACER}interface Request<T> extends Wirespec.Request<T> {}
|${SPACER}sealed interface Request<T> extends Wirespec.Request<T> {}
|${requests.joinToString("\n") { it.emit(this) }}
|${SPACER}interface Response<T> extends Wirespec.Response<T> {}
|${SPACER}sealed interface Response<T> extends Wirespec.Response<T> {}
|${
responses.map { it.status.groupStatus() }.toSet()
.joinToString("\n") { "${SPACER}interface Response${it}<T> extends Response<T>{};" }
.joinToString("\n") { "${SPACER}sealed interface Response${it}<T> extends Response<T>{};" }
}
|${
responses.filter { it.status.isInt() }.map { it.status }.toSet()
.joinToString("\n") { "${SPACER}interface Response${it}<T> extends Response${it.groupStatus()}<T>{};" }
.joinToString("\n") { "${SPACER}sealed interface Response${it}<T> extends Response${it.groupStatus()}<T>{};" }
}
|${responses.distinctBy { it.status to it.content?.type }.joinToString("\n") { it.emit() }}
|${SPACER}public Response ${name.firstToLower()}(Request request);
|${SPACER}public CompletableFuture<Response> ${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<String, java.util.List<Object>> query;
Expand All @@ -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<String, java.util.List<Object>> headers;
Expand All @@ -177,7 +185,7 @@ class JavaEmitter(
""".trimMargin()

private fun List<Endpoint.Response>.emitResponseMapper() = """
|${SPACER}static <B> Response RESPONSE_MAPPER(Wirespec.ContentMapper<B> contentMapper, int status, java.util.Map<String, java.util.List<Object>> headers, Wirespec.Content<B> content) {
|${SPACER}static <B> Response RESPONSE_MAPPER(Wirespec.ContentMapper<B> contentMapper, Wirespec.Response<B> response) {
|${distinctBy { it.status to it.content?.type }.joinToString("") { it.emitResponseMapperCondition() }}
|${SPACER}${SPACER}throw new IllegalStateException("Unknown response type");
|${SPACER}}
Expand All @@ -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()
Expand Down Expand Up @@ -231,11 +239,7 @@ class JavaEmitter(
.joinToString(", ") { it.emit() }
}

private fun List<Type.Shape.Field>.emitMap() = joinToString(
", ",
"java.util.Map.of(",
")"
) { "\"${it.identifier.emit()}\", java.util.List.of(${it.identifier.emit()})" }
private fun List<Type.Shape.Field>.emitMap() = joinToString(", ", "java.util.Map.ofEntries(", ")") { "java.util.Map.entry(\"${it.identifier.value}\", java.util.List.of(${it.identifier.emit()}))" }

private fun List<Endpoint.Segment>.emitSegment() = "/" + joinToString("/") {
when (it) {
Expand All @@ -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"
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ class KotlinEmitter(
}

private fun List<Endpoint.Request>.emitRequestMapper() = """
|${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}fun <B> REQUEST_MAPPER(contentMapper: Wirespec.ContentMapper<B>, request: Wirespec.Request<B>) =
|${SPACER}${SPACER}${SPACER}when {
|${joinToString("\n") { it.emitRequestMapperCondition() }}
|${SPACER}${SPACER}${SPACER}${SPACER}else -> error("Cannot map request")
Expand All @@ -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<Endpoint.Response>.emitResponseMapper() = """
|${SPACER}${SPACER}fun <B> RESPONSE_MAPPER(contentMapper: Wirespec.ContentMapper<B>, status: Int, headers:Map<String, List<Any?>>, content: Wirespec.Content<B>?) =
|${SPACER}${SPACER}fun <B> RESPONSE_MAPPER(contentMapper: Wirespec.ContentMapper<B>, response: Wirespec.Response<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}${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()
}

Expand Down
Loading

0 comments on commit b9c4e5c

Please sign in to comment.