Skip to content

Commit 8f3c0aa

Browse files
feat(codegen): dedicated consumer generator
1 parent 4b994e5 commit 8f3c0aa

3 files changed

Lines changed: 199 additions & 22 deletions

File tree

codegen/src/main/kotlin/tools/samt/codegen/Codegen.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package tools.samt.codegen
22

33
import tools.samt.codegen.http.HttpTransportConfigurationParser
44
import tools.samt.codegen.kotlin.KotlinTypesGenerator
5+
import tools.samt.codegen.kotlin.ktor.KotlinKtorConsumerGenerator
56
import tools.samt.codegen.kotlin.ktor.KotlinKtorProviderGenerator
67
import tools.samt.common.DiagnosticController
78
import tools.samt.common.SamtGeneratorConfiguration
@@ -13,6 +14,7 @@ object Codegen {
1314
private val generators: List<Generator> = listOf(
1415
KotlinTypesGenerator,
1516
KotlinKtorProviderGenerator,
17+
KotlinKtorConsumerGenerator,
1618
)
1719

1820
private val transports: List<TransportConfigurationParser> = listOf(
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package tools.samt.codegen.kotlin.ktor
2+
3+
import tools.samt.codegen.*
4+
import tools.samt.codegen.http.HttpTransportConfiguration
5+
import tools.samt.codegen.kotlin.GeneratedFilePreamble
6+
import tools.samt.codegen.kotlin.KotlinTypesGenerator
7+
import tools.samt.codegen.kotlin.getQualifiedName
8+
9+
object KotlinKtorConsumerGenerator : Generator {
10+
override val name: String = "kotlin-ktor-consumer"
11+
12+
override fun generate(generatorParams: GeneratorParams): List<CodegenFile> {
13+
generatorParams.packages.forEach {
14+
generateMappings(it, generatorParams.options)
15+
generatePackage(it, generatorParams.options)
16+
}
17+
val result = KotlinTypesGenerator.generate(generatorParams) + emittedFiles
18+
emittedFiles.clear()
19+
return result
20+
}
21+
22+
private val emittedFiles = mutableListOf<CodegenFile>()
23+
24+
private fun generateMappings(pack: SamtPackage, options: Map<String, String>) {
25+
val packageSource = mappingFileContent(pack, options)
26+
if (packageSource.isNotEmpty()) {
27+
val filePath = "${pack.getQualifiedName(options).replace('.', '/')}/KtorMappings.kt"
28+
val file = CodegenFile(filePath, packageSource)
29+
emittedFiles.add(file)
30+
}
31+
}
32+
33+
private fun generatePackage(pack: SamtPackage, options: Map<String, String>) {
34+
val relevantConsumers = pack.consumers.filter { it.provider.type is ProviderType && (it.provider.type as ProviderType).transport is HttpTransportConfiguration }
35+
if (relevantConsumers.isNotEmpty()) {
36+
// generate ktor consumers
37+
relevantConsumers.forEach { consumer ->
38+
val provider = consumer.provider.type as ProviderType
39+
val transportConfiguration = provider.transport as HttpTransportConfiguration
40+
41+
val packageSource = buildString {
42+
appendLine(GeneratedFilePreamble)
43+
appendLine()
44+
appendLine("package ${pack.getQualifiedName(options)}")
45+
appendLine()
46+
47+
appendConsumer(consumer, transportConfiguration, options)
48+
}
49+
50+
val filePath = "${pack.getQualifiedName(options).replace('.', '/')}/Consumer.kt"
51+
val file = CodegenFile(filePath, packageSource)
52+
emittedFiles.add(file)
53+
}
54+
}
55+
}
56+
57+
data class ConsumerInfo(val uses: ConsumerUses) {
58+
val reference = uses.service
59+
val service = reference.type as ServiceType
60+
val serviceArgumentName = service.name.replaceFirstChar { it.lowercase() }
61+
}
62+
63+
private fun StringBuilder.appendConsumer(consumer: ConsumerType, transportConfiguration: HttpTransportConfiguration, options: Map<String, String>) {
64+
appendLine("import io.ktor.client.*")
65+
appendLine("import io.ktor.client.engine.cio.*")
66+
appendLine("import io.ktor.client.plugins.contentnegotiation.*")
67+
appendLine("import io.ktor.client.request.*")
68+
appendLine("import io.ktor.client.statement.*")
69+
appendLine("import io.ktor.http.*")
70+
appendLine("import io.ktor.serialization.kotlinx.json.*")
71+
appendLine("import io.ktor.util.*")
72+
appendLine("import kotlinx.coroutines.runBlocking")
73+
appendLine("import kotlinx.serialization.json.*")
74+
75+
val implementedServices = consumer.uses.map { ConsumerInfo(it) }
76+
appendLine("// ${transportConfiguration.exceptionMap}")
77+
appendLine("class ${consumer.name} : ${implementedServices.joinToString { it.service.getQualifiedName(options) }} {")
78+
implementedServices.forEach { info ->
79+
appendConsumerOperations(info, transportConfiguration, options)
80+
}
81+
appendLine("}")
82+
}
83+
84+
private fun StringBuilder.appendConsumerOperations(info: ConsumerInfo, transportConfiguration: HttpTransportConfiguration, options: Map<String, String>) {
85+
appendLine(" private val client = HttpClient(CIO) {")
86+
appendLine(" install(ContentNegotiation) {")
87+
appendLine(" json()")
88+
appendLine(" }")
89+
appendLine(" }")
90+
appendLine()
91+
92+
val service = info.service
93+
info.uses.operations.forEach { operation ->
94+
val operationParameters = operation.parameters.joinToString { "${it.name}: ${it.type.getQualifiedName(options)}" }
95+
96+
when (operation) {
97+
is RequestResponseOperation -> {
98+
if (operation.returnType != null) {
99+
appendLine(" override fun ${operation.name}($operationParameters): ${operation.returnType!!.getQualifiedName(options)} {")
100+
} else {
101+
appendLine(" override fun ${operation.name}($operationParameters): Unit {")
102+
}
103+
104+
// TODO Config: HTTP status code
105+
// TODO serialize response correctly
106+
// TODO validate response
107+
appendLine("return runBlocking {")
108+
109+
appendConsumerServiceCall(info, operation, transportConfiguration)
110+
appendConsumerResponseParsing(operation, transportConfiguration)
111+
112+
appendLine("}")
113+
}
114+
115+
is OnewayOperation -> {
116+
// TODO
117+
}
118+
}
119+
}
120+
}
121+
122+
private fun StringBuilder.appendConsumerServiceCall(info: ConsumerInfo, operation: ServiceOperation, transport: HttpTransportConfiguration) {
123+
/*
124+
val response = client.request("$baseUrl/todos/$title") {
125+
method = HttpMethod.Post
126+
headers["title"] = title
127+
cookie("description", description)
128+
setBody(
129+
buildJsonObject {
130+
put("title", title)
131+
put("description", description)
132+
}
133+
)
134+
contentType(ContentType.Application.Json)
135+
}
136+
*/
137+
138+
// collect parameters for each transport type
139+
val headerParameters = mutableListOf<String>()
140+
val cookieParameters = mutableListOf<String>()
141+
val bodyParameters = mutableListOf<String>()
142+
val pathParameters = mutableListOf<String>()
143+
val queryParameters = mutableListOf<String>()
144+
operation.parameters.forEach {
145+
val name = it.name
146+
val transportMode = transport.getTransportMode(info.service.name, operation.name, name)
147+
when (transportMode) {
148+
HttpTransportConfiguration.TransportMode.Header -> {
149+
headerParameters.add(name)
150+
}
151+
HttpTransportConfiguration.TransportMode.Cookie -> {
152+
cookieParameters.add(name)
153+
}
154+
HttpTransportConfiguration.TransportMode.Body -> {
155+
bodyParameters.add(name)
156+
}
157+
HttpTransportConfiguration.TransportMode.Path -> {
158+
pathParameters.add(name)
159+
}
160+
HttpTransportConfiguration.TransportMode.Query -> {
161+
queryParameters.add(name)
162+
}
163+
}
164+
}
165+
166+
// build request path
167+
// need to split transport path into path segments and query parameter slots
168+
val pathSegments = mutableListOf<String>()
169+
val queryParameterSlots = mutableListOf<String>()
170+
val transportPath = transport.getPath(info.service.name, operation.name)
171+
val pathParts = transportPath.split("/")
172+
173+
// build request headers and body
174+
175+
// oneway vs request-response
176+
}
177+
178+
private fun StringBuilder.appendConsumerResponseParsing(operation: ServiceOperation, transport: HttpTransportConfiguration) {
179+
/*
180+
val bodyAsText = response.bodyAsText()
181+
val body = Json.parseToJsonElement(bodyAsText)
182+
183+
val respTitle = body.jsonObject["title"]!!.jsonPrimitive.content
184+
val respDescription = response.headers["description"]!!
185+
check(respTitle.length in 1..100)
186+
187+
Todo(
188+
title = respTitle,
189+
description = respDescription,
190+
)
191+
*/
192+
193+
194+
}
195+
}

codegen/src/main/kotlin/tools/samt/codegen/kotlin/ktor/KotlinKtorProviderGenerator.kt

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ object KotlinKtorProviderGenerator : Generator {
4242
// generate ktor providers
4343
relevantProviders.forEach { provider ->
4444
val transportConfiguration = provider.transport
45-
check (transportConfiguration is HttpTransportConfiguration)
45+
check(transportConfiguration is HttpTransportConfiguration)
4646

4747
val packageSource = buildString {
4848
appendLine(GeneratedFilePreamble)
@@ -57,27 +57,6 @@ object KotlinKtorProviderGenerator : Generator {
5757
val file = CodegenFile(filePath, packageSource)
5858
emittedFiles.add(file)
5959
}
60-
61-
// generate ktor consumers
62-
pack.consumers.forEach { consumer ->
63-
val provider = consumer.provider.type as ProviderType
64-
val transportConfiguration = provider.transport
65-
if (transportConfiguration !is HttpTransportConfiguration) {
66-
// Skip consumers that are not HTTP
67-
return@forEach
68-
}
69-
70-
val packageSource = buildString {
71-
appendLine("package ${pack.qualifiedName}")
72-
appendLine()
73-
74-
appendConsumer(consumer, transportConfiguration, options)
75-
}
76-
77-
val filePath = pack.qualifiedName.replace('.', '/') + "Consumer.kt"
78-
val file = CodegenFile(filePath, packageSource)
79-
emittedFiles.add(file)
80-
}
8160
}
8261
}
8362

@@ -271,6 +250,7 @@ object KotlinKtorProviderGenerator : Generator {
271250
appendLine("import io.ktor.client.statement.*")
272251
appendLine("import io.ktor.http.*")
273252
appendLine("import io.ktor.serialization.kotlinx.json.*")
253+
appendLine("import io.ktor.util.*")
274254
appendLine("import kotlinx.coroutines.runBlocking")
275255
appendLine("import kotlinx.serialization.json.*")
276256

0 commit comments

Comments
 (0)