Skip to content

Commit ef962dd

Browse files
authored
chore: Convert idempotency token middleware from closure to reusable type (#610)
1 parent 53fbde2 commit ef962dd

File tree

7 files changed

+136
-36
lines changed

7 files changed

+136
-36
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
public struct IdempotencyTokenMiddleware<OperationStackInput,
9+
OperationStackOutput: HttpResponseBinding,
10+
OperationStackError: HttpResponseErrorBinding>: ClientRuntime.Middleware {
11+
public let id: Swift.String = "IdempotencyTokenMiddleware"
12+
private let keyPath: WritableKeyPath<OperationStackInput, String?>
13+
14+
public init(keyPath: WritableKeyPath<OperationStackInput, String?>) {
15+
self.keyPath = keyPath
16+
}
17+
18+
public func handle<H>(context: Context,
19+
input: MInput,
20+
next: H) async throws -> MOutput
21+
where H: Handler, Self.MInput == H.Input, Self.MOutput == H.Output, Self.Context == H.Context {
22+
var copiedInput = input
23+
if input[keyPath: keyPath] == nil {
24+
let idempotencyTokenGenerator = context.getIdempotencyTokenGenerator()
25+
copiedInput[keyPath: keyPath] = idempotencyTokenGenerator.generateToken()
26+
}
27+
return try await next.handle(context: context, input: copiedInput)
28+
}
29+
30+
public typealias MInput = OperationStackInput
31+
public typealias MOutput = OperationOutput<OperationStackOutput>
32+
public typealias Context = HttpContext
33+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
import ClientRuntime
10+
import XCTest
11+
12+
class IdempotencyTokenMiddlewareTests: XCTestCase {
13+
14+
private typealias Subject = IdempotencyTokenMiddleware<TestInputType, TestOutputType, TestOutputErrorType>
15+
16+
let token = "def"
17+
let previousToken = "abc"
18+
private var tokenGenerator: IdempotencyTokenGenerator!
19+
private var context: HttpContext!
20+
private var subject: Subject!
21+
22+
override func setUp() async throws {
23+
try await super.setUp()
24+
tokenGenerator = TestIdempotencyTokenGenerator(token: token)
25+
context = HttpContextBuilder().withIdempotencyTokenGenerator(value: tokenGenerator).build()
26+
subject = Subject(keyPath: \.tokenMember)
27+
}
28+
29+
func test_handle_itSetsAnIdempotencyTokenIfNoneIsSet() async throws {
30+
let input = TestInputType(tokenMember: nil)
31+
let next = MockHandler<TestInputType, TestOutputType> { (context, input) in
32+
XCTAssertEqual(input.tokenMember, self.token)
33+
}
34+
_ = try await subject.handle(context: context, input: input, next: next)
35+
}
36+
37+
func test_handle_itDoesNotChangeTheIdempotencyTokenIfAlreadySet() async throws {
38+
let input = TestInputType(tokenMember: previousToken)
39+
let next = MockHandler<TestInputType, TestOutputType> { (context, input) in
40+
XCTAssertEqual(input.tokenMember, self.previousToken)
41+
}
42+
_ = try await subject.handle(context: context, input: input, next: next)
43+
}
44+
}
45+
46+
// MARK: - Test fixtures & types
47+
48+
private struct TestIdempotencyTokenGenerator: IdempotencyTokenGenerator {
49+
let token: String
50+
func generateToken() -> String { token }
51+
}
52+
53+
private struct TestInputType {
54+
var tokenMember: String?
55+
}
56+
57+
private struct TestOutputType: HttpResponseBinding {
58+
init(httpResponse: ClientRuntime.HttpResponse, decoder: ClientRuntime.ResponseDecoder?) async throws {
59+
// no-op
60+
}
61+
}
62+
63+
private enum TestOutputErrorType: HttpResponseErrorBinding {
64+
static func makeError(httpResponse: ClientRuntime.HttpResponse, decoder: ClientRuntime.ResponseDecoder?) async throws -> Error {
65+
return TestError()
66+
}
67+
}
68+
69+
private struct TestError: Error {}
70+
71+
private struct MockHandler<I, O: HttpResponseBinding>: Handler {
72+
typealias Output = OperationOutput<O>
73+
typealias Context = HttpContext
74+
typealias MockHandlerCallback = (Context, I) async throws -> Void
75+
76+
private let handleCallback: MockHandlerCallback
77+
78+
init(handleCallback: @escaping MockHandlerCallback) {
79+
self.handleCallback = handleCallback
80+
}
81+
82+
func handle(context: Context, input: I) async throws -> Output {
83+
try await handleCallback(context, input)
84+
return OperationOutput(httpResponse: HttpResponse())
85+
}
86+
}

smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ClientRuntimeTypes.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ object ClientRuntimeTypes {
7575
val HeaderMiddleware = runtimeSymbol("HeaderMiddleware")
7676
val SerializableBodyMiddleware = runtimeSymbol("SerializableBodyMiddleware")
7777
val RetryMiddleware = runtimeSymbol("RetryMiddleware")
78+
val IdempotencyTokenMiddleware = runtimeSymbol("IdempotencyTokenMiddleware")
7879
val NoopHandler = runtimeSymbol("NoopHandler")
7980

8081
object Providers {

smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/IdempotencyTokenMiddleware.kt

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,26 @@ class IdempotencyTokenMiddleware(
2424
override val name = "IdempotencyTokenMiddleware"
2525
override val middlewareStep = MiddlewareStep.INITIALIZESTEP
2626
override val position = MiddlewarePosition.AFTER
27-
override fun render(writer: SwiftWriter, op: OperationShape, operationStackName: String) {
2827

28+
override fun render(writer: SwiftWriter, op: OperationShape, operationStackName: String) {
2929
val inputShape = model.expectShape(op.input.get())
3030
val idempotentMember = inputShape.members().firstOrNull { it.hasTrait<IdempotencyTokenTrait>() }
3131
idempotentMember?.let {
3232
val idempotentMemberName = it.memberName.decapitalize()
33+
val inputShapeName = MiddlewareShapeUtils.inputSymbol(symbolProvider, model, op).name
3334
val outputShapeName = MiddlewareShapeUtils.outputSymbol(symbolProvider, model, op).name
3435
val outputErrorShapeName = MiddlewareShapeUtils.outputErrorSymbolName(op)
35-
writer.openBlock(
36-
"$operationStackName.${middlewareStep.stringValue()}.intercept(position: ${position.stringValue()}, id: \"${name}\") { (context, input, next) -> \$N<$outputShapeName> in", "}",
37-
ClientRuntimeTypes.Middleware.OperationOutput
38-
) {
39-
writer.write("let idempotencyTokenGenerator = context.getIdempotencyTokenGenerator()")
40-
writer.write("var copiedInput = input")
41-
writer.openBlock("if input.$idempotentMemberName == nil {", "}") {
42-
writer.write("copiedInput.$idempotentMemberName = idempotencyTokenGenerator.generateToken()")
43-
}
44-
writer.write("return try await next.handle(context: context, input: copiedInput)")
45-
}
36+
writer.write(
37+
"\$L.\$L.intercept(position: \$L, middleware: \$N<\$L, \$L, \$L>(keyPath: \\.\$L))",
38+
operationStackName,
39+
middlewareStep.stringValue(),
40+
position.stringValue(),
41+
ClientRuntimeTypes.Middleware.IdempotencyTokenMiddleware,
42+
inputShapeName,
43+
outputShapeName,
44+
outputErrorShapeName,
45+
idempotentMemberName
46+
)
4647
}
4748
}
4849
}

smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,7 @@ class ContentMd5MiddlewareTests {
2727
.withPartitionID(value: config.partitionID)
2828
.build()
2929
var operation = ClientRuntime.OperationStack<IdempotencyTokenWithStructureInput, IdempotencyTokenWithStructureOutput, IdempotencyTokenWithStructureOutputError>(id: "idempotencyTokenWithStructure")
30-
operation.initializeStep.intercept(position: .after, id: "IdempotencyTokenMiddleware") { (context, input, next) -> ClientRuntime.OperationOutput<IdempotencyTokenWithStructureOutput> in
31-
let idempotencyTokenGenerator = context.getIdempotencyTokenGenerator()
32-
var copiedInput = input
33-
if input.token == nil {
34-
copiedInput.token = idempotencyTokenGenerator.generateToken()
35-
}
36-
return try await next.handle(context: context, input: copiedInput)
37-
}
30+
operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware<IdempotencyTokenWithStructureInput, IdempotencyTokenWithStructureOutput, IdempotencyTokenWithStructureOutputError>(keyPath: \.token))
3831
operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware<IdempotencyTokenWithStructureInput, IdempotencyTokenWithStructureOutput, IdempotencyTokenWithStructureOutputError>())
3932
operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware<IdempotencyTokenWithStructureInput, IdempotencyTokenWithStructureOutput>())
4033
operation.buildStep.intercept(position: .before, middleware: ClientRuntime.ContentMD5Middleware<IdempotencyTokenWithStructureOutput>())

smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,7 @@ class HttpProtocolClientGeneratorTests {
125125
.withPartitionID(value: config.partitionID)
126126
.build()
127127
var operation = ClientRuntime.OperationStack<AllocateWidgetInput, AllocateWidgetOutput, AllocateWidgetOutputError>(id: "allocateWidget")
128-
operation.initializeStep.intercept(position: .after, id: "IdempotencyTokenMiddleware") { (context, input, next) -> ClientRuntime.OperationOutput<AllocateWidgetOutput> in
129-
let idempotencyTokenGenerator = context.getIdempotencyTokenGenerator()
130-
var copiedInput = input
131-
if input.clientToken == nil {
132-
copiedInput.clientToken = idempotencyTokenGenerator.generateToken()
133-
}
134-
return try await next.handle(context: context, input: copiedInput)
135-
}
128+
operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware<AllocateWidgetInput, AllocateWidgetOutput, AllocateWidgetOutputError>(keyPath: \.clientToken))
136129
operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware<AllocateWidgetInput, AllocateWidgetOutput, AllocateWidgetOutputError>())
137130
operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware<AllocateWidgetInput, AllocateWidgetOutput>())
138131
operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware<AllocateWidgetInput, AllocateWidgetOutput>(contentType: "application/json"))

smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,7 @@ class IdempotencyTokenTraitTests {
2727
.withPartitionID(value: config.partitionID)
2828
.build()
2929
var operation = ClientRuntime.OperationStack<IdempotencyTokenWithStructureInput, IdempotencyTokenWithStructureOutput, IdempotencyTokenWithStructureOutputError>(id: "idempotencyTokenWithStructure")
30-
operation.initializeStep.intercept(position: .after, id: "IdempotencyTokenMiddleware") { (context, input, next) -> ClientRuntime.OperationOutput<IdempotencyTokenWithStructureOutput> in
31-
let idempotencyTokenGenerator = context.getIdempotencyTokenGenerator()
32-
var copiedInput = input
33-
if input.token == nil {
34-
copiedInput.token = idempotencyTokenGenerator.generateToken()
35-
}
36-
return try await next.handle(context: context, input: copiedInput)
37-
}
30+
operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware<IdempotencyTokenWithStructureInput, IdempotencyTokenWithStructureOutput, IdempotencyTokenWithStructureOutputError>(keyPath: \.token))
3831
operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware<IdempotencyTokenWithStructureInput, IdempotencyTokenWithStructureOutput, IdempotencyTokenWithStructureOutputError>())
3932
operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware<IdempotencyTokenWithStructureInput, IdempotencyTokenWithStructureOutput>())
4033
operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware<IdempotencyTokenWithStructureInput, IdempotencyTokenWithStructureOutput>(contentType: "application/xml"))

0 commit comments

Comments
 (0)