Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changes/8fa79617-cfb4-48f2-a031-32ee2f0341b3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"id": "8fa79617-cfb4-48f2-a031-32ee2f0341b3",
"type": "feature",
"description": "Implement ID prefix trimming for route53 resources.",
"issues": [
"awslabs/aws-sdk-kotlin#509"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package aws.sdk.kotlin.codegen.customization.route53

import aws.sdk.kotlin.codegen.sdkId
import software.amazon.smithy.kotlin.codegen.KotlinSettings
import software.amazon.smithy.kotlin.codegen.core.*
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration
import software.amazon.smithy.kotlin.codegen.model.expectShape
import software.amazon.smithy.kotlin.codegen.model.hasTrait
import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolGenerator
import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolMiddleware
import software.amazon.smithy.kotlin.codegen.utils.getOrNull
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.MemberShape
import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.model.shapes.ServiceShape
import software.amazon.smithy.model.shapes.StructureShape
import software.amazon.smithy.model.traits.HttpLabelTrait

class TrimResourcePrefix : KotlinIntegration {
override fun enabledForService(model: Model, settings: KotlinSettings) =
model.expectShape<ServiceShape>(settings.service).sdkId.equals("route 53", ignoreCase = true)

override fun customizeMiddleware(
ctx: ProtocolGenerator.GenerationContext,
resolved: List<ProtocolMiddleware>
): List<ProtocolMiddleware> =
resolved + TrimResourcePrefixMiddleware()
}

private class TrimResourcePrefixMiddleware : ProtocolMiddleware {
override val name: String
get() = "TrimResourcePrefix"

override val order: Byte = -128
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Why does this need to execute first?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense in my mind that doing any modification on the user input would occur before serialization (so basically, it should execute first).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important to note that the middleware itself also is ordered by phase. The initialization phase precedes serialization so it wouldn't matter what order they are in for codegen.


override fun isEnabledFor(ctx: ProtocolGenerator.GenerationContext, op: OperationShape): Boolean {
val input = op.input.getOrNull()?.let { ctx.model.expectShape<StructureShape>(it) }
return input?.members()?.any(MemberShape::shouldTrimResourcePrefix) ?: false
}

override fun render(ctx: ProtocolGenerator.GenerationContext, op: OperationShape, writer: KotlinWriter) {
val pathMember = ctx.model.expectShape<StructureShape>(op.input.get()).members().first(
MemberShape::shouldTrimResourcePrefix
).defaultName()

writer.withBlock("op.execution.initialize.intercept { req, next -> ", "}") {
write("""val prefix = "^/?.*?/".toRegex()""")
withBlock("if (req.subject.#L?.contains(prefix) == true) {", "}", pathMember) {
write(
"""val updated = req.subject.copy { #L = req.subject.#L?.replace(prefix, "") }""",
pathMember,
pathMember,
)
write("next.call(#T(req.context, updated))", RuntimeTypes.Http.Operation.OperationRequest)
}
withBlock("else {", "}") {
write("next.call(req)")
}
}
}
}

private fun MemberShape.shouldTrimResourcePrefix(): Boolean =
(target.name == "ResourceId" || target.name == "ChangeId") &&
hasTrait<HttpLabelTrait>()
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ aws.sdk.kotlin.codegen.customization.machinelearning.MachineLearningEndpointCust
aws.sdk.kotlin.codegen.customization.BackfillOptionalAuth
aws.sdk.kotlin.codegen.customization.RemoveEventStreamOperations
aws.sdk.kotlin.codegen.customization.RemoveChecksumSelectionFields
aws.sdk.kotlin.codegen.customization.route53.TrimResourcePrefix
81 changes: 81 additions & 0 deletions services/route53/model/route53-tests.smithy
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
$version: "1.0"

namespace com.amazonaws.route53

use smithy.test#httpRequestTests


apply ListResourceRecordSets @httpRequestTests([
{
id: "ListResourceRecordSetsNoTrim",
documentation: "Validates that HostedZoneId isn't trimmed when not prefixed.",
method: "GET",
protocol: "aws.protocols#restXml",
uri: "/2013-04-01/hostedzone/IDOFMYHOSTEDZONE/rrset",
bodyMediaType: "application/xml",
params: {
"HostedZoneId": "IDOFMYHOSTEDZONE"
}
},
{
id: "ListResourceRecordSetsTrim",
documentation: "Validates that HostedZoneId is trimmed.",
method: "GET",
protocol: "aws.protocols#restXml",
uri: "/2013-04-01/hostedzone/IDOFMYHOSTEDZONE/rrset",
bodyMediaType: "application/xml",
params: {
"HostedZoneId": "hostedzone/IDOFMYHOSTEDZONE"
}
},
{
id: "ListResourceRecordSetsTrimLeadingSlash",
documentation: "Validates that HostedZoneId is trimmed even with a leading slash.",
method: "GET",
protocol: "aws.protocols#restXml",
uri: "/2013-04-01/hostedzone/IDOFMYHOSTEDZONE/rrset",
bodyMediaType: "application/xml",
params: {
"HostedZoneId": "/hostedzone/IDOFMYHOSTEDZONE"
}
},
{
id: "ListResourceRecordSetsTrimMultislash",
documentation: "Validates that HostedZoneId isn't over-trimmed.",
method: "GET",
protocol: "aws.protocols#restXml",
uri: "/2013-04-01/hostedzone/IDOFMY%2FHOSTEDZONE/rrset",
bodyMediaType: "application/xml",
params: {
"HostedZoneId": "/hostedzone/IDOFMY/HOSTEDZONE"
}
},
])

apply GetChange @httpRequestTests([
{
id: "GetChangeTrimChangeId",
documentation: "This test validates that change id is correctly trimmed",
method: "GET",
protocol: "aws.protocols#restXml",
uri: "/2013-04-01/change/SOMECHANGEID",
bodyMediaType: "application/xml",
params: {
"Id": "/change/SOMECHANGEID"
}
},
])

apply GetReusableDelegationSet @httpRequestTests([
{
id: "GetReusableDelegationSetTrimDelegationSetId",
documentation: "This test validates that delegation set id is correctly trimmed",
method: "GET",
protocol: "aws.protocols#restXml",
uri: "/2013-04-01/delegationset/DELEGATIONSETID",
bodyMediaType: "application/xml",
params: {
"Id": "/delegationset/DELEGATIONSETID"
}
},
])
Comment on lines +69 to +81
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: How/why is delegationset being trimmed in this test?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • how - the generated regex catches it like anything else
  • why - Great question! this gets at the underlying issue that there's no real record of how this customization is meant to work other than how SDKs have implemented it. I learned today that this one in particular is pretty old, it goes back to 2012 in Java v1. Java v2's version basically came from porting over v1. There's apparently no reference for how it's supposed to work outside of how SDKs have written it.

This particular test case is one pulled from rust's codebase.