Skip to content

Commit 514e764

Browse files
author
luigi
committed
separate files
1 parent 140366e commit 514e764

File tree

13 files changed

+1399
-1334
lines changed

13 files changed

+1399
-1334
lines changed

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/service/KtorStubGenerator.kt

Lines changed: 14 additions & 1318 deletions
Large diffs are not rendered by default.

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/service/ServiceStubGenerator.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,17 @@ internal interface ServiceStubGenerator {
1515
}
1616

1717
internal abstract class AbstractStubGenerator(
18-
protected val ctx: GenerationContext,
19-
protected val delegator: KotlinDelegator,
20-
protected val fileManifest: FileManifest,
18+
val ctx: GenerationContext,
19+
val delegator: KotlinDelegator,
20+
val fileManifest: FileManifest,
2121
) : ServiceStubGenerator {
2222

23-
protected val serviceShape = ctx.settings.getService(ctx.model)
24-
protected val operations = TopDownIndex.of(ctx.model)
23+
val serviceShape = ctx.settings.getService(ctx.model)
24+
val operations = TopDownIndex.of(ctx.model)
2525
.getContainedOperations(serviceShape)
2626
.sortedBy { it.defaultName() }
2727

28-
protected val pkgName = ctx.settings.pkg.name
28+
val pkgName = ctx.settings.pkg.name
2929

3030
final override fun render() {
3131
renderServiceFrameworkConfig()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package software.amazon.smithy.kotlin.codegen.service.ktor
2+
3+
import software.amazon.smithy.aws.traits.auth.SigV4ATrait
4+
import software.amazon.smithy.aws.traits.auth.SigV4Trait
5+
import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes
6+
import software.amazon.smithy.kotlin.codegen.core.withBlock
7+
import software.amazon.smithy.kotlin.codegen.model.getTrait
8+
import software.amazon.smithy.kotlin.codegen.service.KtorStubGenerator
9+
import software.amazon.smithy.kotlin.codegen.service.ServiceTypes
10+
11+
internal fun KtorStubGenerator.writeAuthentication() {
12+
delegator.useFileWriter("UserPrincipal.kt", "$pkgName.auth") { writer ->
13+
writer.withBlock("public data class UserPrincipal(", ")") {
14+
write("val user: String")
15+
}
16+
}
17+
18+
delegator.useFileWriter("Validation.kt", "$pkgName.auth") { writer ->
19+
20+
writer.withBlock("internal object BearerValidation {", "}") {
21+
withBlock("public fun bearerValidation(token: String): UserPrincipal? {", "}") {
22+
write("// TODO: implement me")
23+
write("if (true) return UserPrincipal(#S) else return null", "Authenticated User")
24+
}
25+
}
26+
}
27+
28+
delegator.useFileWriter("Authentication.kt", "$pkgName.auth") { writer ->
29+
writer.withBlock("internal fun #T.configureAuthentication() {", "}", RuntimeTypes.KtorServerCore.Application) {
30+
write("")
31+
withBlock(
32+
"#T(#T) {",
33+
"}",
34+
RuntimeTypes.KtorServerCore.install,
35+
RuntimeTypes.KtorServerAuth.Authentication,
36+
) {
37+
withBlock("#T(#S) {", "}", RuntimeTypes.KtorServerAuth.bearer, "auth-bearer") {
38+
write("realm = #S", "Access to API")
39+
write("authenticate { cred -> BearerValidation.bearerValidation(cred.token) }")
40+
}
41+
withBlock("sigV4(name = #S) {", "}", "aws-sigv4") {
42+
write("region = #T.region", ServiceTypes(pkgName).serviceFrameworkConfig)
43+
val serviceSigV4AuthTrait = serviceShape.getTrait<SigV4Trait>()
44+
if (serviceSigV4AuthTrait != null) {
45+
write("service = #S", serviceSigV4AuthTrait.name)
46+
}
47+
}
48+
withBlock("sigV4A(name = #S) {", "}", "aws-sigv4a") {
49+
write("region = #T.region", ServiceTypes(pkgName).serviceFrameworkConfig)
50+
val serviceSigV4AAuthTrait = serviceShape.getTrait<SigV4ATrait>()
51+
if (serviceSigV4AAuthTrait != null) {
52+
write("service = #S", serviceSigV4AAuthTrait.name)
53+
}
54+
}
55+
write("provider(#S) { authenticate { ctx -> ctx.principal(Unit) } }", "no-auth")
56+
}
57+
}
58+
}
59+
}

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/service/ktor/AuthenticationAWS.kt

Lines changed: 442 additions & 0 deletions
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package software.amazon.smithy.kotlin.codegen.service.ktor
2+
3+
import software.amazon.smithy.kotlin.codegen.core.withBlock
4+
import software.amazon.smithy.kotlin.codegen.service.KtorStubGenerator
5+
6+
internal fun KtorStubGenerator.writePerOperationHandlers() {
7+
operations.forEach { shape ->
8+
val name = shape.id.name
9+
delegator.useFileWriter("${name}Operation.kt", "$pkgName.operations") { writer ->
10+
writer.addImport("$pkgName.model", "${shape.id.name}Request")
11+
writer.addImport("$pkgName.model", "${shape.id.name}Response")
12+
13+
writer.withBlock("public fun handle${name}Request(req: ${name}Request): ${name}Response {", "}") {
14+
write("// TODO: implement me")
15+
write("// To build a ${name}Response object:")
16+
write("// 1. Use`${name}Response.Builder()`")
17+
write("// 2. Set fields like `${name}Response.variable = ...`")
18+
write("// 3. Return the built object using `return ${name}Response.build()`")
19+
write("return ${name}Response.Builder().build()")
20+
}
21+
}
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
package software.amazon.smithy.kotlin.codegen.service.ktor
2+
3+
import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes
4+
import software.amazon.smithy.kotlin.codegen.core.withBlock
5+
import software.amazon.smithy.kotlin.codegen.core.withInlineBlock
6+
import software.amazon.smithy.kotlin.codegen.service.KtorStubGenerator
7+
import software.amazon.smithy.kotlin.codegen.service.ServiceTypes
8+
9+
internal fun KtorStubGenerator.writePlugins() {
10+
renderErrorHandler()
11+
renderContentTypeGuard()
12+
renderAcceptTypeGuard()
13+
}
14+
15+
private fun KtorStubGenerator.renderErrorHandler() {
16+
delegator.useFileWriter("ErrorHandler.kt", "$pkgName.plugins") { writer ->
17+
writer.write("internal val ResponseHandledKey = #T<Boolean>(#S)", RuntimeTypes.KtorServerUtils.AttributeKey, "ResponseHandled")
18+
.write("")
19+
writer.write("@#T", RuntimeTypes.KotlinxCborSerde.Serializable)
20+
.write("private data class ErrorPayload(val code: Int, val message: String)")
21+
.write("")
22+
.withInlineBlock("internal class ErrorEnvelope(", ")") {
23+
write("val code: Int,")
24+
write("val msg: String,")
25+
write("cause: Throwable? = null,")
26+
}
27+
.withBlock(" : RuntimeException(msg, cause) {", "}") {
28+
withBlock("fun toJson(json: #T = #T): String {", "}", RuntimeTypes.KotlinxJsonSerde.Json, RuntimeTypes.KotlinxJsonSerde.Json) {
29+
withInlineBlock("return json.encodeToString(", ")") {
30+
write("ErrorPayload(code, message ?: #S)", "Unknown error")
31+
}
32+
}
33+
withBlock("fun toCbor(cbor: #T = #T { }): ByteArray {", "}", RuntimeTypes.KotlinxCborSerde.Cbor, RuntimeTypes.KotlinxCborSerde.Cbor) {
34+
withInlineBlock("return cbor.#T(", ")", RuntimeTypes.KotlinxCborSerde.encodeToByteArray) {
35+
write("ErrorPayload(code, message ?: #S)", "Unknown error")
36+
}
37+
}
38+
}
39+
.write("")
40+
.withInlineBlock("private suspend fun #T.respondEnvelope(", ")", RuntimeTypes.KtorServerCore.ApplicationCallClass) {
41+
write("envelope: ErrorEnvelope,")
42+
write("status: #T,", RuntimeTypes.KtorServerHttp.HttpStatusCode)
43+
}
44+
.withBlock("{", "}") {
45+
write("val acceptsCbor = request.#T().any { it.value == #S }", RuntimeTypes.KtorServerRouting.requestAcceptItems, "application/cbor")
46+
write("val acceptsJson = request.#T().any { it.value == #S }", RuntimeTypes.KtorServerRouting.requestAcceptItems, "application/json")
47+
write("")
48+
write("val log = #T.getLogger(#S)", RuntimeTypes.KtorLoggingSlf4j.LoggerFactory, pkgName)
49+
write("log.info(#S)", "Route Error Message: \${envelope.msg}")
50+
write("")
51+
withBlock("when {", "}") {
52+
withBlock("acceptsCbor -> {", "}") {
53+
withBlock("#T(", ")", RuntimeTypes.KtorServerRouting.responseRespondBytes) {
54+
write("bytes = envelope.toCbor(),")
55+
write("status = status,")
56+
write("contentType = #T", RuntimeTypes.KtorServerHttp.Cbor)
57+
}
58+
}
59+
withBlock("acceptsJson -> {", "}") {
60+
withBlock("#T(", ")", RuntimeTypes.KtorServerRouting.responseResponseText) {
61+
write("envelope.toJson(),")
62+
write("status = status,")
63+
write("contentType = #T", RuntimeTypes.KtorServerHttp.Json)
64+
}
65+
}
66+
withBlock("else -> {", "}") {
67+
withBlock("#T(", ")", RuntimeTypes.KtorServerRouting.responseResponseText) {
68+
write("envelope.msg,")
69+
write("status = status")
70+
}
71+
}
72+
}
73+
}
74+
.write("")
75+
.withBlock("internal fun #T.configureErrorHandling() {", "}", RuntimeTypes.KtorServerCore.Application) {
76+
write("")
77+
withBlock(
78+
"#T(#T) {",
79+
"}",
80+
RuntimeTypes.KtorServerCore.install,
81+
RuntimeTypes.KtorServerStatusPage.StatusPages,
82+
) {
83+
withBlock("status(#T.Unauthorized) { call, status ->", "}", RuntimeTypes.KtorServerHttp.HttpStatusCode) {
84+
write("if (call.attributes.getOrNull(#T) == true) { return@status }", ServiceTypes(pkgName).responseHandledKey)
85+
write("call.attributes.put(#T, true)", ServiceTypes(pkgName).responseHandledKey)
86+
write("val missing = call.request.headers[#S].isNullOrBlank()", "Authorization")
87+
write("val message = if (missing) #S else #S", "Missing bearer token", "Invalid or expired bearer token")
88+
write("call.respondEnvelope( ErrorEnvelope(status.value, message), status )")
89+
}
90+
write("")
91+
withBlock("status(#T.NotFound) { call, status ->", "}", RuntimeTypes.KtorServerHttp.HttpStatusCode) {
92+
write("if (call.attributes.getOrNull(#T) == true) { return@status }", ServiceTypes(pkgName).responseHandledKey)
93+
write("call.attributes.put(#T, true)", ServiceTypes(pkgName).responseHandledKey)
94+
write("val message = #S", "Resource not found")
95+
write("call.respondEnvelope( ErrorEnvelope(status.value, message), status )")
96+
}
97+
write("")
98+
withBlock("status(#T.MethodNotAllowed) { call, status ->", "}", RuntimeTypes.KtorServerHttp.HttpStatusCode) {
99+
write("if (call.attributes.getOrNull(#T) == true) { return@status }", ServiceTypes(pkgName).responseHandledKey)
100+
write("call.attributes.put(#T, true)", ServiceTypes(pkgName).responseHandledKey)
101+
write("val message = #S", "Method not allowed for this resource")
102+
write("call.respondEnvelope( ErrorEnvelope(status.value, message), status )")
103+
}
104+
write("")
105+
withBlock("#T<Throwable> { call, cause ->", "}", RuntimeTypes.KtorServerStatusPage.exception) {
106+
withBlock("val status = when (cause) {", "}") {
107+
write(
108+
"is ErrorEnvelope -> #T.fromValue(cause.code)",
109+
RuntimeTypes.KtorServerHttp.HttpStatusCode,
110+
)
111+
write(
112+
"is #T -> #T.BadRequest",
113+
RuntimeTypes.KtorServerCore.BadRequestException,
114+
RuntimeTypes.KtorServerHttp.HttpStatusCode,
115+
)
116+
write(
117+
"is #T -> #T.PayloadTooLarge",
118+
RuntimeTypes.KtorServerBodyLimit.PayloadTooLargeException,
119+
RuntimeTypes.KtorServerHttp.HttpStatusCode,
120+
)
121+
write("else -> #T.InternalServerError", RuntimeTypes.KtorServerHttp.HttpStatusCode)
122+
}
123+
write("")
124+
125+
write("val envelope = if (cause is ErrorEnvelope) cause else ErrorEnvelope(status.value, cause.message ?: #S)", "Unexpected error")
126+
write("call.attributes.put(#T, true)", ServiceTypes(pkgName).responseHandledKey)
127+
write("call.respondEnvelope( envelope, status )")
128+
}
129+
}
130+
}
131+
}
132+
}
133+
134+
private fun KtorStubGenerator.renderContentTypeGuard() {
135+
delegator.useFileWriter("ContentTypeGuard.kt", "$pkgName.plugins") { writer ->
136+
137+
writer.withBlock("private fun #T.hasBody(): Boolean {", "}", RuntimeTypes.KtorServerRouting.requestApplicationRequest) {
138+
write(
139+
"return (#T()?.let { it > 0 } == true) || headers.contains(#T.TransferEncoding)",
140+
RuntimeTypes.KtorServerRouting.requestContentLength,
141+
RuntimeTypes.KtorServerHttp.HttpHeaders,
142+
)
143+
}
144+
writer.withBlock("public class ContentTypeGuardConfig {", "}") {
145+
write("public var allow: List<#T> = emptyList()", RuntimeTypes.KtorServerHttp.ContentType)
146+
write("")
147+
withBlock("public fun any(): Unit {", "}") {
148+
write("allow = listOf(#T)", RuntimeTypes.KtorServerHttp.Any)
149+
}
150+
write("")
151+
withBlock("public fun json(): Unit {", "}") {
152+
write("allow = listOf(#T)", RuntimeTypes.KtorServerHttp.Json)
153+
}
154+
write("")
155+
withBlock("public fun cbor(): Unit {", "}") {
156+
write("allow = listOf(#T)", RuntimeTypes.KtorServerHttp.Cbor)
157+
}
158+
write("")
159+
withBlock("public fun text(): Unit {", "}") {
160+
write("allow = listOf(#T)", RuntimeTypes.KtorServerHttp.PlainText)
161+
}
162+
write("")
163+
withBlock("public fun binary(): Unit {", "}") {
164+
write("allow = listOf(#T)", RuntimeTypes.KtorServerHttp.OctetStream)
165+
}
166+
}
167+
.write("")
168+
169+
writer.withInlineBlock(
170+
"public val ContentTypeGuard: #T<ContentTypeGuardConfig> = #T(",
171+
")",
172+
RuntimeTypes.KtorServerCore.ApplicationRouteScopedPlugin,
173+
RuntimeTypes.KtorServerCore.ApplicationCreateRouteScopedPlugin,
174+
) {
175+
write("name = #S,", "ContentTypeGuard")
176+
write("createConfiguration = ::ContentTypeGuardConfig,")
177+
}
178+
.withBlock("{", "}") {
179+
write("val allowed: List<#T> = pluginConfig.allow", RuntimeTypes.KtorServerHttp.ContentType)
180+
write("require(allowed.isNotEmpty()) { #S }", "ContentTypeGuard installed with empty allow-list.")
181+
write("")
182+
withBlock("onCall { call ->", "}") {
183+
write("if (!call.request.hasBody()) return@onCall")
184+
write("val incoming = call.request.#T()", RuntimeTypes.KtorServerRouting.requestContentType)
185+
withBlock("if (incoming == #T.Any || allowed.none { incoming.match(it) }) {", "}", RuntimeTypes.KtorServerHttp.ContentType) {
186+
withBlock("throw #T(", ")", ServiceTypes(pkgName).errorEnvelope) {
187+
write("#T.UnsupportedMediaType.value, ", RuntimeTypes.KtorServerHttp.HttpStatusCode)
188+
write("#S", "Not acceptable Content‑Type found: '\${incoming}'. Accepted content types: \${allowed.joinToString()}")
189+
}
190+
}
191+
}
192+
}
193+
}
194+
}
195+
196+
private fun KtorStubGenerator.renderAcceptTypeGuard() {
197+
delegator.useFileWriter("AcceptTypeGuard.kt", "${ctx.settings.pkg.name}.plugins") { writer ->
198+
199+
writer.withBlock(
200+
"private fun #T.acceptedContentTypes(): List<#T> {",
201+
"}",
202+
RuntimeTypes.KtorServerRouting.requestApplicationRequest,
203+
RuntimeTypes.KtorServerHttp.ContentType,
204+
) {
205+
write("val raw = headers[#T.Accept] ?: return emptyList()", RuntimeTypes.KtorServerHttp.HttpHeaders)
206+
write(
207+
"return #T(raw).mapNotNull { it.value?.let(#T::parse) }",
208+
RuntimeTypes.KtorServerHttp.parseAndSortHeader,
209+
RuntimeTypes.KtorServerHttp.ContentType,
210+
)
211+
}
212+
213+
writer.withBlock("public class AcceptTypeGuardConfig {", "}") {
214+
write("public var allow: List<#T> = emptyList()", RuntimeTypes.KtorServerHttp.ContentType)
215+
write("")
216+
withBlock("public fun any(): Unit {", "}") {
217+
write("allow = listOf(#T)", RuntimeTypes.KtorServerHttp.Any)
218+
}
219+
write("")
220+
withBlock("public fun json(): Unit {", "}") {
221+
write("allow = listOf(#T)", RuntimeTypes.KtorServerHttp.Json)
222+
}
223+
write("")
224+
withBlock("public fun cbor(): Unit {", "}") {
225+
write("allow = listOf(#T)", RuntimeTypes.KtorServerHttp.Cbor)
226+
}
227+
write("")
228+
withBlock("public fun text(): Unit {", "}") {
229+
write("allow = listOf(#T)", RuntimeTypes.KtorServerHttp.PlainText)
230+
}
231+
write("")
232+
withBlock("public fun binary(): Unit {", "}") {
233+
write("allow = listOf(#T)", RuntimeTypes.KtorServerHttp.OctetStream)
234+
}
235+
}
236+
.write("")
237+
238+
writer.withInlineBlock(
239+
"public val AcceptTypeGuard: #T<AcceptTypeGuardConfig> = #T(",
240+
")",
241+
RuntimeTypes.KtorServerCore.ApplicationRouteScopedPlugin,
242+
RuntimeTypes.KtorServerCore.ApplicationCreateRouteScopedPlugin,
243+
) {
244+
write("name = #S,", "AcceptTypeGuard")
245+
write("createConfiguration = ::AcceptTypeGuardConfig,")
246+
}
247+
.withBlock("{", "}") {
248+
write("val allowed: List<#T> = pluginConfig.allow", RuntimeTypes.KtorServerHttp.ContentType)
249+
write("require(allowed.isNotEmpty()) { #S }", "AcceptTypeGuard installed with empty allow-list.")
250+
write("")
251+
withBlock("onCall { call ->", "}") {
252+
write("val accepted = call.request.acceptedContentTypes()")
253+
write("if (accepted.isEmpty()) return@onCall")
254+
write("")
255+
write("val isOk = accepted.any { candidate -> allowed.any { candidate.match(it) } }")
256+
257+
withBlock("if (!isOk) {", "}") {
258+
withBlock("throw #T(", ")", ServiceTypes(pkgName).errorEnvelope) {
259+
write("#T.NotAcceptable.value, ", RuntimeTypes.KtorServerHttp.HttpStatusCode)
260+
write("#S", "Not acceptable Accept type found: '\${accepted}'. Accepted types: \${allowed.joinToString()}")
261+
}
262+
}
263+
}
264+
}
265+
}
266+
}

0 commit comments

Comments
 (0)