Skip to content

Commit 4fd9c2f

Browse files
author
luigi
committed
merge
2 parents f8fef99 + 8c48223 commit 4fd9c2f

20 files changed

+1400
-210
lines changed

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

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import software.amazon.smithy.kotlin.codegen.core.withBlock
1010
import software.amazon.smithy.kotlin.codegen.core.withInlineBlock
1111
import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes
1212
import software.amazon.smithy.kotlin.codegen.model.getTrait
13+
import software.amazon.smithy.kotlin.codegen.service.contraints.ConstraintGenerator
14+
import software.amazon.smithy.kotlin.codegen.service.contraints.ConstraintUtilsGenerator
1315
import software.amazon.smithy.kotlin.codegen.service.MediaType.ANY
1416
import software.amazon.smithy.kotlin.codegen.service.MediaType.JSON
1517
import software.amazon.smithy.kotlin.codegen.service.MediaType.OCTET_STREAM
@@ -104,7 +106,7 @@ internal class KtorStubGenerator(
104106
}
105107

106108
private fun renderLogging() {
107-
delegator.useFileWriter("Logging.kt", "${ctx.settings.pkg.name}.utils") { writer ->
109+
delegator.useFileWriter("Logging.kt", "$pkgName.utils") { writer ->
108110

109111
writer.withBlock("internal fun #T.configureLogging() {", "}", RuntimeTypes.KtorServerCore.Application) {
110112
withBlock(
@@ -178,20 +180,20 @@ internal class KtorStubGenerator(
178180

179181
// Generates `Authentication.kt` with Authenticator interface + configureSecurity().
180182
override fun renderAuthModule() {
181-
delegator.useFileWriter("UserPrincipal.kt", "${ctx.settings.pkg.name}.auth") { writer ->
183+
delegator.useFileWriter("UserPrincipal.kt", "$pkgName.auth") { writer ->
182184
writer.withBlock("public data class UserPrincipal(", ")") {
183185
write("val user: String")
184186
}
185187
}
186188

187-
delegator.useFileWriter("Validation.kt", "${ctx.settings.pkg.name}.auth") { writer ->
189+
delegator.useFileWriter("Validation.kt", "$pkgName.auth") { writer ->
188190
writer.withBlock("public fun bearerValidation(token: String): UserPrincipal? {", "}") {
189191
write("// TODO: implement me")
190192
write("if (true) return UserPrincipal(#S) else return null", "Authenticated User")
191193
}
192194
}
193195

194-
delegator.useFileWriter("Authentication.kt", "${ctx.settings.pkg.name}.auth") { writer ->
196+
delegator.useFileWriter("Authentication.kt", "$pkgName.auth") { writer ->
195197
writer.withBlock("internal fun #T.configureAuthentication() {", "}", RuntimeTypes.KtorServerCore.Application) {
196198
write("")
197199
withBlock(
@@ -212,18 +214,21 @@ internal class KtorStubGenerator(
212214

213215
// For every operation request structure, create a `validate()` function file.
214216
override fun renderConstraintValidators() {
217+
ConstraintUtilsGenerator(ctx, delegator).render()
218+
operations.forEach { operation -> ConstraintGenerator(ctx, operation, delegator).render() }
215219
}
216220

217221
// Writes `Routing.kt` that maps Smithy operations → Ktor routes.
218222
override fun renderRouting() {
219223
delegator.useFileWriter("Routing.kt", ctx.settings.pkg.name) { writer ->
220224

221225
operations.forEach { shape ->
222-
writer.addImport("${ctx.settings.pkg.name}.serde", "${shape.id.name}OperationDeserializer")
223-
writer.addImport("${ctx.settings.pkg.name}.serde", "${shape.id.name}OperationSerializer")
224-
writer.addImport("${ctx.settings.pkg.name}.model", "${shape.id.name}Request")
225-
writer.addImport("${ctx.settings.pkg.name}.model", "${shape.id.name}Response")
226-
writer.addImport("${ctx.settings.pkg.name}.operations", "handle${shape.id.name}Request")
226+
writer.addImport("$pkgName.serde", "${shape.id.name}OperationDeserializer")
227+
writer.addImport("$pkgName.serde", "${shape.id.name}OperationSerializer")
228+
writer.addImport("$pkgName.constraints", "check${shape.id.name}RequestConstraint")
229+
writer.addImport("$pkgName.model", "${shape.id.name}Request")
230+
writer.addImport("$pkgName.model", "${shape.id.name}Response")
231+
writer.addImport("$pkgName.operations", "handle${shape.id.name}Request")
227232
}
228233

229234
writer.withBlock("internal fun #T.configureRouting(): Unit {", "}", RuntimeTypes.KtorServerCore.Application) {
@@ -296,6 +301,11 @@ internal class KtorStubGenerator(
296301
call { readHttpLabel(shape, writer) }
297302
call { readHttpQuery(shape, writer) }
298303
}
304+
write(
305+
"try { check${shape.id.name}RequestConstraint(requestObj) } catch (ex: Exception) { throw #T(ex?.message ?: #S, ex) }",
306+
RuntimeTypes.KtorServerCore.BadRequestException,
307+
"Error while validating constraints",
308+
)
299309
write("val responseObj = handle${shape.id.name}Request(requestObj)")
300310
write("val serializer = ${shape.id.name}OperationSerializer()")
301311
withBlock(
@@ -518,7 +528,7 @@ internal class KtorStubGenerator(
518528
}
519529

520530
private fun renderErrorHandler() {
521-
delegator.useFileWriter("ErrorHandler.kt", "${ctx.settings.pkg.name}.plugins") { writer ->
531+
delegator.useFileWriter("ErrorHandler.kt", "$pkgName.plugins") { writer ->
522532
writer.write("@#T", RuntimeTypes.KotlinxCborSerde.Serializable)
523533
.write("private data class ErrorPayload(val code: Int, val message: String)")
524534
.write("")
@@ -548,7 +558,7 @@ internal class KtorStubGenerator(
548558
write("val acceptsCbor = request.#T().any { it.value == #S }", RuntimeTypes.KtorServerRouting.requestAcceptItems, "application/cbor")
549559
write("val acceptsJson = request.#T().any { it.value == #S }", RuntimeTypes.KtorServerRouting.requestAcceptItems, "application/json")
550560
write("")
551-
write("val log = #T.getLogger(#S)", RuntimeTypes.KtorLoggingSlf4j.LoggerFactory, ctx.settings.pkg.name)
561+
write("val log = #T.getLogger(#S)", RuntimeTypes.KtorLoggingSlf4j.LoggerFactory, pkgName)
552562
write("log.info(#S)", "Route Error Message: \${envelope.msg}")
553563
write("")
554564
withBlock("when {", "}") {
@@ -589,6 +599,16 @@ internal class KtorStubGenerator(
589599
write("call.respondEnvelope( ErrorEnvelope(status.value, message), status )")
590600
}
591601
write("")
602+
withBlock("status(#T.NotFound) { call, status ->", "}", RuntimeTypes.KtorServerHttp.HttpStatusCode) {
603+
write("val message = #S", "Resource not found")
604+
write("call.respondEnvelope( ErrorEnvelope(status.value, message), status )")
605+
}
606+
write("")
607+
withBlock("status(#T.MethodNotAllowed) { call, status ->", "}", RuntimeTypes.KtorServerHttp.HttpStatusCode) {
608+
write("val message = #S", "Method not allowed for this resource")
609+
write("call.respondEnvelope( ErrorEnvelope(status.value, message), status )")
610+
}
611+
write("")
592612
withBlock("#T<Throwable> { call, cause ->", "}", RuntimeTypes.KtorServerStatusPage.exception) {
593613
withBlock("val status = when (cause) {", "}") {
594614
write(
@@ -618,7 +638,7 @@ internal class KtorStubGenerator(
618638
}
619639

620640
private fun renderContentTypeGuard() {
621-
delegator.useFileWriter("ContentTypeGuard.kt", "${ctx.settings.pkg.name}.plugins") { writer ->
641+
delegator.useFileWriter("ContentTypeGuard.kt", "$pkgName.plugins") { writer ->
622642

623643
writer.withBlock("private fun #T.hasBody(): Boolean {", "}", RuntimeTypes.KtorServerRouting.requestApplicationRequest) {
624644
write(
@@ -755,9 +775,9 @@ internal class KtorStubGenerator(
755775
override fun renderPerOperationHandlers() {
756776
operations.forEach { shape ->
757777
val name = shape.id.name
758-
delegator.useFileWriter("${name}Operation.kt", "${ctx.settings.pkg.name}.operations") { writer ->
759-
writer.addImport("${ctx.settings.pkg.name}.model", "${shape.id.name}Request")
760-
writer.addImport("${ctx.settings.pkg.name}.model", "${shape.id.name}Response")
778+
delegator.useFileWriter("${name}Operation.kt", "$pkgName.operations") { writer ->
779+
writer.addImport("$pkgName.model", "${shape.id.name}Request")
780+
writer.addImport("$pkgName.model", "${shape.id.name}Response")
761781

762782
writer.withBlock("public fun handle${name}Request(req: ${name}Request): ${name}Response {", "}") {
763783
write("// TODO: implement me")

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,14 @@ class ServiceTypes(val pkgName: String) {
6767
name = "AcceptTypeGuard"
6868
namespace = "$pkgName.plugins"
6969
}
70+
71+
val sizeOf = buildSymbol {
72+
name = "sizeOf"
73+
namespace = "$pkgName.constraints"
74+
}
75+
76+
val hasAllUniqueElements = buildSymbol {
77+
name = "hasAllUniqueElements"
78+
namespace = "$pkgName.constraints"
79+
}
7080
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package software.amazon.smithy.kotlin.codegen.service.contraints
2+
3+
internal abstract class AbstractConstraintTraitGenerator {
4+
abstract fun render()
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package software.amazon.smithy.kotlin.codegen.service.contraints
2+
3+
import software.amazon.smithy.kotlin.codegen.core.GenerationContext
4+
import software.amazon.smithy.kotlin.codegen.core.KotlinDelegator
5+
import software.amazon.smithy.kotlin.codegen.core.KotlinWriter
6+
import software.amazon.smithy.kotlin.codegen.core.withBlock
7+
import software.amazon.smithy.model.shapes.MemberShape
8+
import software.amazon.smithy.model.shapes.OperationShape
9+
import software.amazon.smithy.model.shapes.StructureShape
10+
import software.amazon.smithy.model.traits.RequiredTrait
11+
import kotlin.collections.iterator
12+
13+
internal class ConstraintGenerator(
14+
val ctx: GenerationContext,
15+
val operation: OperationShape,
16+
val delegator: KotlinDelegator,
17+
) {
18+
val inputShape = ctx.model.expectShape(operation.input.get()) as StructureShape
19+
val inputMembers = inputShape.allMembers
20+
21+
val opName = operation.id.name
22+
val pkgName = ctx.settings.pkg.name
23+
24+
fun render() {
25+
renderRequestConstraintsValidation()
26+
}
27+
private fun generateConstraintValidations(prefix: String, memberShape: MemberShape, writer: KotlinWriter) {
28+
val targetShape = ctx.model.expectShape(memberShape.target)
29+
30+
val memberName = memberShape.memberName
31+
val memberAndTargetTraits = memberShape.allTraits + targetShape.allTraits
32+
33+
for (memberTrait in memberAndTargetTraits.values) {
34+
val traitGenerator = getTraitGeneratorFromTrait(prefix, memberName, memberTrait, pkgName, writer)
35+
if (memberTrait !is RequiredTrait) {
36+
writer.write("if ($prefix$memberName == null) { return }")
37+
}
38+
traitGenerator?.render()
39+
}
40+
41+
for (member in targetShape.allMembers) {
42+
val newMemberPrefix = "${targetShape.id.name}".replaceFirstChar { it.lowercase() }
43+
writer.withBlock("if ($prefix$memberName != null) {", "}") {
44+
withBlock("for ($newMemberPrefix${member.key} in $prefix$memberName) {", "}") {
45+
call { generateConstraintValidations(newMemberPrefix, member.value, writer) }
46+
}
47+
}
48+
}
49+
}
50+
51+
private fun renderRequestConstraintsValidation() {
52+
delegator.useFileWriter("${opName}RequestConstraints.kt", "$pkgName.constraints") { writer ->
53+
writer.addImport("$pkgName.model", "${operation.id.name}Request")
54+
55+
writer.withBlock("public fun check${opName}RequestConstraint(data: ${opName}Request) {", "}") {
56+
for (memberShape in inputMembers.values) {
57+
generateConstraintValidations("data.", memberShape, writer)
58+
}
59+
}
60+
}
61+
}
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package software.amazon.smithy.kotlin.codegen.service.contraints
2+
3+
import software.amazon.smithy.kotlin.codegen.core.GenerationContext
4+
import software.amazon.smithy.kotlin.codegen.core.KotlinDelegator
5+
import software.amazon.smithy.kotlin.codegen.core.KotlinWriter
6+
import software.amazon.smithy.kotlin.codegen.core.withBlock
7+
8+
internal class ConstraintUtilsGenerator(
9+
val ctx: GenerationContext,
10+
val delegator: KotlinDelegator,
11+
) {
12+
val pkgName = ctx.settings.pkg.name
13+
14+
fun render() {
15+
delegator.useFileWriter("utils.kt", "$pkgName.constraints") { writer ->
16+
renderLengthTraitUtils(writer)
17+
18+
writer.write("")
19+
renderUniqueItemsTraitUtils(writer)
20+
}
21+
}
22+
23+
private fun renderLengthTraitUtils(writer: KotlinWriter) {
24+
writer.withBlock("internal fun sizeOf(value: Any?): Long = when (value) {", "}") {
25+
write("is Collection<*> -> value.size.toLong()")
26+
write("is Array<*> -> value.size.toLong()")
27+
write("is Map<*, *> -> value.size.toLong()")
28+
write("is String -> value.codePointCount(0, value.length).toLong()")
29+
write("is ByteArray -> value.size.toLong()")
30+
withBlock("else -> {", "}") {
31+
write("val typeName = value?.javaClass?.simpleName ?: #S", "null")
32+
write("throw IllegalArgumentException( #S )", "sizeOf does not support \${typeName} type")
33+
}
34+
}
35+
}
36+
37+
private fun renderUniqueItemsTraitUtils(writer: KotlinWriter) {
38+
writer.withBlock("internal fun hasAllUniqueElements(elements: List<Any?>): Boolean {", "}") {
39+
withBlock("class Wrapped(private val v: Any?) {", "}") {
40+
withBlock("override fun equals(other: Any?): Boolean {", "}") {
41+
write("if (other !is Wrapped) return false")
42+
write("if (v?.javaClass != other.v?.javaClass) return false")
43+
withBlock("return when (v) {", "}") {
44+
write("null -> true")
45+
write("is String,")
46+
write("is Boolean,")
47+
write("is java.time.Instant,")
48+
write("is Number -> v == other.v")
49+
write("is ByteArray -> v.contentEquals(other.v as ByteArray)")
50+
withBlock("is List<*> -> {", "}") {
51+
write("val o = other.v as List<*>")
52+
write("v.size == o.size && v.indices.all { i -> Wrapped(v[i]) == Wrapped(o[i]) }")
53+
}
54+
withBlock("is Map<*, *> -> {", "}") {
55+
write("val o = other.v as Map<*, *>")
56+
write("v.size == o.size && v.all { (k, value) -> o.containsKey(k) && Wrapped(value) == Wrapped(o[k]) }")
57+
}
58+
write("else -> v == other.v")
59+
}
60+
}
61+
withBlock("override fun hashCode(): Int = when (v) {", "}") {
62+
write("null -> 0")
63+
write("is ByteArray -> v.contentHashCode()")
64+
write("is List<*> -> v.fold(1) { acc, e -> 31 * acc + Wrapped(e).hashCode() }")
65+
write("is Map<*, *> -> v.entries.fold(1) { acc, (k, e) -> 31 * acc + Wrapped(k).hashCode() xor Wrapped(e).hashCode() }")
66+
write("else -> v.hashCode()")
67+
}
68+
}
69+
write("")
70+
write("val seen = HashSet<Wrapped>(elements.size)")
71+
write("for (e in elements) if (!seen.add(Wrapped(e))) return false")
72+
write("return true")
73+
}
74+
}
75+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package software.amazon.smithy.kotlin.codegen.service.contraints
2+
3+
import software.amazon.smithy.kotlin.codegen.core.KotlinWriter
4+
import software.amazon.smithy.kotlin.codegen.service.ServiceTypes
5+
import software.amazon.smithy.model.traits.LengthTrait
6+
7+
internal class LengthConstraintGenerator(val memberPrefix: String, val memberName: String, val trait: LengthTrait, val pkgName: String, val writer: KotlinWriter) : AbstractConstraintTraitGenerator() {
8+
override fun render() {
9+
val min = trait.min.orElse(null)
10+
val max = trait.max.orElse(null)
11+
val member = "$memberPrefix$memberName"
12+
13+
if (max != null && min != null) {
14+
writer.write("require(#T($member) in $min..$max) { #S }", ServiceTypes(pkgName).sizeOf, "The size of `$memberName` must be between $min and $max (inclusive)")
15+
} else if (max != null) {
16+
writer.write("require(#T($member) <= $max) { #S }", ServiceTypes(pkgName).sizeOf, "The size of `$memberName` must be less than or equal to $max")
17+
} else {
18+
writer.write("require(#T($member) >= $min) { #S }", ServiceTypes(pkgName).sizeOf, "The size of `$memberName` must be greater than or equal to $min")
19+
}
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package software.amazon.smithy.kotlin.codegen.service.contraints
2+
3+
import software.amazon.smithy.kotlin.codegen.core.KotlinWriter
4+
import software.amazon.smithy.model.traits.PatternTrait
5+
6+
internal class PatternConstraintGenerator(val memberPrefix: String, val memberName: String, val trait: PatternTrait, val pkgName: String, val writer: KotlinWriter) : AbstractConstraintTraitGenerator() {
7+
override fun render() {
8+
val member = "$memberPrefix$memberName"
9+
10+
writer.write("require(Regex(#S).containsMatchIn($member)) { #S }", trait.pattern.toString(), "Value `\${$member}` does not match required pattern: `${trait.pattern}`")
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package software.amazon.smithy.kotlin.codegen.service.contraints
2+
3+
import software.amazon.smithy.kotlin.codegen.core.KotlinWriter
4+
import software.amazon.smithy.model.traits.RangeTrait
5+
6+
internal class RangeConstraintGenerator(val memberPrefix: String, val memberName: String, val trait: RangeTrait, val pkgName: String, val writer: KotlinWriter) : AbstractConstraintTraitGenerator() {
7+
override fun render() {
8+
val min = trait.min.orElse(null)
9+
val max = trait.max.orElse(null)
10+
val member = "$memberPrefix$memberName"
11+
12+
if (max != null && min != null) {
13+
writer.write("require($member in $min..$max) { #S }", "`$memberName` must be between $min and $max (inclusive)")
14+
} else if (max != null) {
15+
writer.write("require($member <= $max) { #S }", "`$memberName` must be less than or equal to $max")
16+
} else {
17+
writer.write("require($member >= $min) { #S }", "`$memberName` must be greater than or equal to $min")
18+
}
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package software.amazon.smithy.kotlin.codegen.service.contraints
2+
3+
import software.amazon.smithy.kotlin.codegen.core.KotlinWriter
4+
import software.amazon.smithy.model.traits.RequiredTrait
5+
6+
internal class RequiredConstraintGenerator(val memberPrefix: String, val memberName: String, val trait: RequiredTrait, val pkgName: String, val writer: KotlinWriter) : AbstractConstraintTraitGenerator() {
7+
override fun render() {
8+
val member = "$memberPrefix$memberName"
9+
writer.write("require($member != null) { #S }", "`$memberName` must be provided")
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package software.amazon.smithy.kotlin.codegen.service.contraints
2+
3+
import software.amazon.smithy.kotlin.codegen.core.KotlinWriter
4+
import software.amazon.smithy.kotlin.codegen.service.ServiceTypes
5+
import software.amazon.smithy.model.traits.UniqueItemsTrait
6+
7+
internal class UniqueItemsConstraintGenerator(val memberPrefix: String, val memberName: String, val trait: UniqueItemsTrait, val pkgName: String, val writer: KotlinWriter) : AbstractConstraintTraitGenerator() {
8+
override fun render() {
9+
val member = "$memberPrefix$memberName"
10+
writer.write("require(#T($member)) { #S }", ServiceTypes(pkgName).hasAllUniqueElements, "`$memberName` must contain only unique items, duplicate values are not allowed")
11+
}
12+
}

0 commit comments

Comments
 (0)