From 0de114dd68ee975f89bca8845a61447ca629ddc6 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Mon, 10 Aug 2026 13:48:23 -0500 Subject: [PATCH 1/2] fix: Protocol tests for Infinity, -Infinity, NaN --- Sources/SmithyHTTPAPI/URLEncodingUtils.swift | 12 ++++ .../HttpRequestTestBase+FormURL.swift | 2 +- .../RequestTestUtil/HttpRequestTestBase.swift | 19 ++---- .../providers/HttpHeaderProvider.kt | 12 ++-- .../providers/HttpQueryItemProvider.kt | 65 +++++++++++-------- .../swiftmodules/SmithyHTTPAPITypes.kt | 1 + 6 files changed, 64 insertions(+), 47 deletions(-) diff --git a/Sources/SmithyHTTPAPI/URLEncodingUtils.swift b/Sources/SmithyHTTPAPI/URLEncodingUtils.swift index 35eb70958..4c393686f 100644 --- a/Sources/SmithyHTTPAPI/URLEncodingUtils.swift +++ b/Sources/SmithyHTTPAPI/URLEncodingUtils.swift @@ -32,4 +32,16 @@ public enum URLEncodingUtils { public static func urlPercentEncodedForQuery(_ string: String) -> String { string.addingPercentEncoding(withAllowedCharacters: allowedForQuery) ?? string } + + public static func encodeNumber(_ value: FP) -> String { + guard !value.isNaN else { return "NaN" } + switch value { + case .infinity: + return "Infinity" + case -.infinity: + return "-Infinity" + default: + return "\(value)" + } + } } diff --git a/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase+FormURL.swift b/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase+FormURL.swift index 68c29e299..9c3a68d74 100644 --- a/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase+FormURL.swift +++ b/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase+FormURL.swift @@ -37,7 +37,7 @@ extension HttpRequestTestBase { return [] } let name: String = keyValueArray[0] - let value = keyValueArray.count >= 2 ? sanitizeStringForNonConformingValues(keyValueArray[1]) : nil + let value = keyValueArray.count >= 2 ? keyValueArray[1] : nil queryItems.append(URIQueryItem(name: name, value: value)) } return queryItems diff --git a/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase.swift b/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase.swift index d6331f644..6b30df835 100644 --- a/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase.swift +++ b/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase.swift @@ -65,8 +65,7 @@ open class HttpRequestTestBase: XCTestCase { if let headers = headers { for (headerName, headerValue) in headers { - let value = sanitizeStringForNonConformingValues(headerValue) - builder.withHeader(name: headerName, value: value) + builder.withHeader(name: headerName, value: headerValue) } } @@ -123,7 +122,7 @@ open class HttpRequestTestBase: XCTestCase { for queryParam in queryParams { let queryParamComponents = queryParam.components(separatedBy: "=") if queryParamComponents.count > 1 { - let value = sanitizeStringForNonConformingValues(queryParamComponents[1]) + let value = queryParamComponents[1] builder.withQueryItem(URIQueryItem(name: queryParamComponents[0], value: value)) @@ -137,7 +136,7 @@ open class HttpRequestTestBase: XCTestCase { for queryParam in queryParams { let queryParamComponents = queryParam.components(separatedBy: "=") if queryParamComponents.count > 1 { - let value = sanitizeStringForNonConformingValues(queryParamComponents[1]) + let value = queryParamComponents[1] builder.withForbiddenQueryItem(URIQueryItem(name: queryParamComponents[0], value: value)) @@ -151,7 +150,7 @@ open class HttpRequestTestBase: XCTestCase { for queryParam in queryParams { let queryParamComponents = queryParam.components(separatedBy: "=") if queryParamComponents.count > 1 { - let value = sanitizeStringForNonConformingValues(queryParamComponents[1]) + let value = queryParamComponents[1] builder.withRequiredQueryItem(URIQueryItem(name: queryParamComponents[0], value: value)) @@ -161,16 +160,6 @@ open class HttpRequestTestBase: XCTestCase { } } - func sanitizeStringForNonConformingValues(_ input: String) -> String { - switch input { - case "Infinity": return "inf" - case "-Infinity": return "-inf" - case "NaN": return "nan" - default: - return input - } - } - /** Check if a Query Item with given name exists in array of `URLQueryItem` */ diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpHeaderProvider.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpHeaderProvider.kt index 2d8759f36..145fce6ae 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpHeaderProvider.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpHeaderProvider.kt @@ -94,7 +94,7 @@ class HttpHeaderProvider( private fun generateHeaders() { headerBindings.forEach { - var memberName = ctx.symbolProvider.toMemberName(it.member) + val memberName = ctx.symbolProvider.toMemberName(it.member) val memberTarget = ctx.model.expectShape(it.member.target) val paramName = it.locationName val isBoxed = ctx.symbolProvider.toSymbol(it.member).isBoxed() @@ -148,20 +148,22 @@ class HttpHeaderProvider( ) } } else if (inCollection && ctx.model.expectShape(member.target) !is TimestampShape) { + val createValueCall = HttpQueryItemProvider.renderCreateValueCall(ctx, writer, member) writer.write( - "items.add(\$N(name: \"\$L\", value: \$N(\$N(\$L))))", + "items.add(\$N(name: \"\$L\", value: \$N(\$L(\$L))))", SmithyHTTPAPITypes.Header, paramName, ClientRuntimeTypes.Core.quoteHeaderValue, - SwiftTypes.String, + createValueCall, memberNameWithExtension, ) } else { + val createValueCall = HttpQueryItemProvider.renderCreateValueCall(ctx, writer, member) writer.write( - "items.add(\$N(name: \"\$L\", value: \$N(\$L)))", + "items.add(\$N(name: \"\$L\", value: \$L(\$L)))", SmithyHTTPAPITypes.Header, paramName, - SwiftTypes.String, + createValueCall, memberNameWithExtension, ) } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpQueryItemProvider.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpQueryItemProvider.kt index ce552b075..8766274d8 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpQueryItemProvider.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpQueryItemProvider.kt @@ -8,7 +8,10 @@ package software.amazon.smithy.swift.codegen.integration.middlewares.providers import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.model.knowledge.HttpBinding import software.amazon.smithy.model.knowledge.HttpBindingIndex +import software.amazon.smithy.model.shapes.BigDecimalShape import software.amazon.smithy.model.shapes.CollectionShape +import software.amazon.smithy.model.shapes.DoubleShape +import software.amazon.smithy.model.shapes.FloatShape import software.amazon.smithy.model.shapes.MapShape import software.amazon.smithy.model.shapes.MemberShape import software.amazon.smithy.model.shapes.OperationShape @@ -26,6 +29,7 @@ import software.amazon.smithy.swift.codegen.model.hasTrait import software.amazon.smithy.swift.codegen.model.isBoxed import software.amazon.smithy.swift.codegen.model.needsDefaultValueCheck import software.amazon.smithy.swift.codegen.model.toMemberNames +import software.amazon.smithy.swift.codegen.swiftmodules.SmithyHTTPAPITypes import software.amazon.smithy.swift.codegen.swiftmodules.SmithyTypes import software.amazon.smithy.swift.codegen.swiftmodules.SwiftTypes import software.amazon.smithy.swift.codegen.utils.SDKFileUtils @@ -73,6 +77,20 @@ class HttpQueryItemProvider( } } } + + fun renderCreateValueCall( + ctx: ProtocolGenerator.GenerationContext, + writer: SwiftWriter, + member: MemberShape, + ): String { + val targetShape = ctx.model.expectShape(member.target) + return when (targetShape) { + is DoubleShape, is FloatShape, is BigDecimalShape -> + writer.format("\$N.encodeNumber", SmithyHTTPAPITypes.URLEncodingUtils) + else -> + writer.format("\$N", SwiftTypes.String) + } + } } fun renderProvider(writer: SwiftWriter) { @@ -103,7 +121,7 @@ class HttpQueryItemProvider( var httpQueryParamBinding: HttpBindingDescriptor? = null queryBindings.forEach { - var memberName = ctx.symbolProvider.toMemberName(it.member) + val memberName = ctx.symbolProvider.toMemberName(it.member) val memberTarget = ctx.model.expectShape(it.member.target) val paramName = it.locationName val bindingIndex = HttpBindingIndex.of(ctx.model) @@ -117,7 +135,7 @@ class HttpQueryItemProvider( } httpQueryParamBinding?.let { val memberTarget = ctx.model.expectShape(it.member.target) - var memberName = ctx.symbolProvider.toMemberName(it.member) + val memberName = ctx.symbolProvider.toMemberName(it.member) if (memberTarget is MapShape) { renderHttpQueryParamMap(memberTarget, memberName) } @@ -205,7 +223,7 @@ class HttpQueryItemProvider( paramName: String, unwrapped: Boolean, ) { - var (memberName, requiresDoCatch) = + val (memberName, requiresDoCatch) = formatHeaderOrQueryValue( ctx, writer, @@ -226,41 +244,36 @@ class HttpQueryItemProvider( memberName, member.defaultValue(ctx.symbolProvider), ) { - val queryItemName = "${ctx.symbolProvider.toMemberNames(member).second}QueryItem" - writer.write( - "let \$L = \$N(name: \$S.urlPercentEncoding(), value: \$N(\$L\$L).urlPercentEncoding())", - queryItemName, - SmithyTypes.URIQueryItem, - paramName, - SwiftTypes.String, - prefix, - memberName, - ) - writer.write("items.append($queryItemName)") + renderConstruction(member, paramName, prefix, memberName) } } else { - val queryItemName = "${ctx.symbolProvider.toMemberNames(member).second}QueryItem" - writer.write( - "let \$L = \$N(name: \$S.urlPercentEncoding(), value: \$N(\$L\$L).urlPercentEncoding())", - queryItemName, - SmithyTypes.URIQueryItem, - paramName, - SwiftTypes.String, - prefix, - memberName, - ) - writer.write("items.append($queryItemName)") + renderConstruction(member, paramName, prefix, memberName) } } } + private fun renderConstruction(member: MemberShape, paramName: String, prefix: String, memberName: String) { + val queryItemName = "${ctx.symbolProvider.toMemberNames(member).second}QueryItem" + val createValueCall = renderCreateValueCall(ctx, writer, member) + writer.write( + "let \$L = \$N(name: \$S.urlPercentEncoding(), value: \$L(\$L\$L).urlPercentEncoding())", + queryItemName, + SmithyTypes.URIQueryItem, + paramName, + createValueCall, + prefix, + memberName, + ) + writer.write("items.append($queryItemName)") + } + private fun renderListOrSet( memberTarget: CollectionShape, bindingIndex: HttpBindingIndex, memberName: String, paramName: String, ) { - var (queryItemValue, requiresDoCatch) = + val (queryItemValue, requiresDoCatch) = formatHeaderOrQueryValue( ctx, writer, diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyHTTPAPITypes.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyHTTPAPITypes.kt index b55648e11..3ee5b43b4 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyHTTPAPITypes.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyHTTPAPITypes.kt @@ -20,6 +20,7 @@ object SmithyHTTPAPITypes { val HTTPRequest = runtimeSymbol("HTTPRequest", SwiftDeclaration.CLASS) val HTTPResponse = runtimeSymbol("HTTPResponse", SwiftDeclaration.CLASS) val HTTPStatusCode = runtimeSymbol("HTTPStatusCode", SwiftDeclaration.ENUM) + val URLEncodingUtils = runtimeSymbol("URLEncodingUtils", SwiftDeclaration.ENUM) } private fun runtimeSymbol( From a21c96bc1a5189f39d982842fed0a2a848d8692f Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Mon, 10 Aug 2026 15:37:20 -0500 Subject: [PATCH 2/2] Claude additions --- .../HTTPBindingProtocolGenerator.kt | 25 ++++++++ .../providers/HttpHeaderProvider.kt | 8 ++- .../providers/HttpQueryItemProvider.kt | 34 +++++------ .../HttpHeaderProviderGeneratorTests.kt | 39 +++++++++++++ .../HttpQueryItemProviderGeneratorTests.kt | 44 ++++++++++++++ .../test/resources/http-float-bindings.smithy | 58 +++++++++++++++++++ 6 files changed, 185 insertions(+), 23 deletions(-) create mode 100644 smithy-swift-codegen/src/test/resources/http-float-bindings.smithy diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HTTPBindingProtocolGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HTTPBindingProtocolGenerator.kt index e04c54b60..89586146a 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HTTPBindingProtocolGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HTTPBindingProtocolGenerator.kt @@ -11,8 +11,11 @@ import software.amazon.smithy.model.knowledge.HttpBindingIndex import software.amazon.smithy.model.knowledge.TopDownIndex import software.amazon.smithy.model.neighbor.RelationshipType import software.amazon.smithy.model.neighbor.Walker +import software.amazon.smithy.model.shapes.BigDecimalShape import software.amazon.smithy.model.shapes.BlobShape import software.amazon.smithy.model.shapes.CollectionShape +import software.amazon.smithy.model.shapes.DoubleShape +import software.amazon.smithy.model.shapes.FloatShape import software.amazon.smithy.model.shapes.IntEnumShape import software.amazon.smithy.model.shapes.MemberShape import software.amazon.smithy.model.shapes.OperationShape @@ -71,6 +74,8 @@ import software.amazon.smithy.swift.codegen.model.isInputEventStream import software.amazon.smithy.swift.codegen.model.isOutputEventStream import software.amazon.smithy.swift.codegen.supportsStreamingAndIsRPC import software.amazon.smithy.swift.codegen.swiftmodules.ClientRuntimeTypes +import software.amazon.smithy.swift.codegen.swiftmodules.SmithyHTTPAPITypes +import software.amazon.smithy.swift.codegen.swiftmodules.SwiftTypes import software.amazon.smithy.swift.codegen.utils.SDKFileUtils import software.amazon.smithy.utils.OptionalUtils import java.util.Optional @@ -126,6 +131,26 @@ fun formatHeaderOrQueryValue( else -> Pair(memberName, false) } +/** + * Provides the Swift expression that renders a header or query value as a `String`. + * + * Floating-point values are rendered by `URLEncodingUtils.encodeNumber(_:)`, which uses the + * Smithy-defined tokens for the non-finite values NaN, Infinity, and -Infinity. Swift's own + * string interpolation would render those as `nan`, `inf`, and `-inf`, which are not valid on + * the wire. All other values are rendered by `String.init`. + */ +fun renderCreateValueCall( + ctx: ProtocolGenerator.GenerationContext, + writer: SwiftWriter, + member: MemberShape, +): String = + when (ctx.model.expectShape(member.target)) { + is DoubleShape, is FloatShape, is BigDecimalShape -> + writer.format("\$N.encodeNumber", SmithyHTTPAPITypes.URLEncodingUtils) + else -> + writer.format("\$N", SwiftTypes.String) + } + /** * Abstract implementation useful for all HTTP protocols */ diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpHeaderProvider.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpHeaderProvider.kt index 145fce6ae..3babd57bb 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpHeaderProvider.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpHeaderProvider.kt @@ -19,6 +19,7 @@ import software.amazon.smithy.swift.codegen.integration.HttpBindingResolver import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.formatHeaderOrQueryValue import software.amazon.smithy.swift.codegen.integration.middlewares.handlers.MiddlewareShapeUtils +import software.amazon.smithy.swift.codegen.integration.renderCreateValueCall import software.amazon.smithy.swift.codegen.model.defaultValue import software.amazon.smithy.swift.codegen.model.isBoxed import software.amazon.smithy.swift.codegen.model.needsDefaultValueCheck @@ -148,7 +149,7 @@ class HttpHeaderProvider( ) } } else if (inCollection && ctx.model.expectShape(member.target) !is TimestampShape) { - val createValueCall = HttpQueryItemProvider.renderCreateValueCall(ctx, writer, member) + val createValueCall = renderCreateValueCall(ctx, writer, member) writer.write( "items.add(\$N(name: \"\$L\", value: \$N(\$L(\$L))))", SmithyHTTPAPITypes.Header, @@ -158,7 +159,7 @@ class HttpHeaderProvider( memberNameWithExtension, ) } else { - val createValueCall = HttpQueryItemProvider.renderCreateValueCall(ctx, writer, member) + val createValueCall = renderCreateValueCall(ctx, writer, member) writer.write( "items.add(\$N(name: \"\$L\", value: \$L(\$L)))", SmithyHTTPAPITypes.Header, @@ -209,6 +210,9 @@ class HttpHeaderProvider( } } + // `String.init` is used to render the value here, rather than `renderCreateValueCall`, because + // only base64-encoded values reach this method. `requiresDoCatch` is set only for blobs and + // media-typed strings, so a floating-point value is never rendered here. private fun renderDoCatch( headerValueWithExtension: String, headerName: String, diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpQueryItemProvider.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpQueryItemProvider.kt index 8766274d8..a29acee79 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpQueryItemProvider.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpQueryItemProvider.kt @@ -8,10 +8,7 @@ package software.amazon.smithy.swift.codegen.integration.middlewares.providers import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.model.knowledge.HttpBinding import software.amazon.smithy.model.knowledge.HttpBindingIndex -import software.amazon.smithy.model.shapes.BigDecimalShape import software.amazon.smithy.model.shapes.CollectionShape -import software.amazon.smithy.model.shapes.DoubleShape -import software.amazon.smithy.model.shapes.FloatShape import software.amazon.smithy.model.shapes.MapShape import software.amazon.smithy.model.shapes.MemberShape import software.amazon.smithy.model.shapes.OperationShape @@ -24,12 +21,12 @@ import software.amazon.smithy.swift.codegen.integration.HttpBindingResolver import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.formatHeaderOrQueryValue import software.amazon.smithy.swift.codegen.integration.middlewares.handlers.MiddlewareShapeUtils +import software.amazon.smithy.swift.codegen.integration.renderCreateValueCall import software.amazon.smithy.swift.codegen.model.defaultValue import software.amazon.smithy.swift.codegen.model.hasTrait import software.amazon.smithy.swift.codegen.model.isBoxed import software.amazon.smithy.swift.codegen.model.needsDefaultValueCheck import software.amazon.smithy.swift.codegen.model.toMemberNames -import software.amazon.smithy.swift.codegen.swiftmodules.SmithyHTTPAPITypes import software.amazon.smithy.swift.codegen.swiftmodules.SmithyTypes import software.amazon.smithy.swift.codegen.swiftmodules.SwiftTypes import software.amazon.smithy.swift.codegen.utils.SDKFileUtils @@ -77,20 +74,6 @@ class HttpQueryItemProvider( } } } - - fun renderCreateValueCall( - ctx: ProtocolGenerator.GenerationContext, - writer: SwiftWriter, - member: MemberShape, - ): String { - val targetShape = ctx.model.expectShape(member.target) - return when (targetShape) { - is DoubleShape, is FloatShape, is BigDecimalShape -> - writer.format("\$N.encodeNumber", SmithyHTTPAPITypes.URLEncodingUtils) - else -> - writer.format("\$N", SwiftTypes.String) - } - } } fun renderProvider(writer: SwiftWriter) { @@ -252,7 +235,12 @@ class HttpQueryItemProvider( } } - private fun renderConstruction(member: MemberShape, paramName: String, prefix: String, memberName: String) { + private fun renderConstruction( + member: MemberShape, + paramName: String, + prefix: String, + memberName: String, + ) { val queryItemName = "${ctx.symbolProvider.toMemberNames(member).second}QueryItem" val createValueCall = renderCreateValueCall(ctx, writer, member) writer.write( @@ -288,16 +276,20 @@ class HttpQueryItemProvider( if (requiresDoCatch) { renderDoCatch(queryItemValue, paramName) } else { + val createValueCall = renderCreateValueCall(ctx, writer, memberTarget.member) writer.write( - "let queryItem = \$N(name: \"$paramName\".urlPercentEncoding(), value: \$N($queryItemValue).urlPercentEncoding())", + "let queryItem = \$N(name: \"$paramName\".urlPercentEncoding(), value: \$L($queryItemValue).urlPercentEncoding())", SmithyTypes.URIQueryItem, - SwiftTypes.String, + createValueCall, ) writer.write("items.append(queryItem)") } } } + // `String.init` is used to render the value here, rather than `renderCreateValueCall`, because + // only base64-encoded values reach this method. `requiresDoCatch` is set only for blobs and + // media-typed strings, so a floating-point value is never rendered here. private fun renderDoCatch( queryItemValueWithExtension: String, paramName: String, diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/requestandresponse/requestflow/HttpHeaderProviderGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/requestandresponse/requestflow/HttpHeaderProviderGeneratorTests.kt index 2460fa84e..bf5277137 100644 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/requestandresponse/requestflow/HttpHeaderProviderGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/requestandresponse/requestflow/HttpHeaderProviderGeneratorTests.kt @@ -116,6 +116,45 @@ extension TimestampInputInput { contents.shouldContainOnlyOnce(expectedContents) } + @Test + fun `it encodes float and double headers with encodeNumber`() { + // Floats & doubles must be rendered by URLEncodingUtils.encodeNumber so that the non-finite + // values are rendered as the Smithy-defined tokens NaN, Infinity, and -Infinity. Swift's own + // string interpolation would render them as nan, inf, and -inf, which are invalid on the wire. + val context = TestContext.initContextFrom("http-float-bindings.smithy", "com.test#Example") + context.generator.generateSerializers(context.generationCtx) + context.generationCtx.delegator.flushWriters() + val contents = getModelFileContents("example/Sources/example", "FloatBindingsInput+HeaderProvider.swift", context.manifest) + contents.shouldSyntacticSanityCheck() + val expectedContents = """ +extension FloatBindingsInput { + + static func headerProvider(_ value: FloatBindingsInput) -> SmithyHTTPAPI.Headers { + var items = SmithyHTTPAPI.Headers() + if let headerDouble = value.headerDouble { + items.add(SmithyHTTPAPI.Header(name: "X-Double", value: SmithyHTTPAPI.URLEncodingUtils.encodeNumber(headerDouble))) + } + if let headerFloat = value.headerFloat { + items.add(SmithyHTTPAPI.Header(name: "X-Float", value: SmithyHTTPAPI.URLEncodingUtils.encodeNumber(headerFloat))) + } + if let headerFloatList = value.headerFloatList { + if headerFloatList.isEmpty { + items.add(name: "X-FloatList", value: "") + } + headerFloatList.forEach { headerValue in + items.add(SmithyHTTPAPI.Header(name: "X-FloatList", value: ClientRuntime.quoteHeaderValue(SmithyHTTPAPI.URLEncodingUtils.encodeNumber(headerValue)))) + } + } + if let headerString = value.headerString { + items.add(SmithyHTTPAPI.Header(name: "X-String", value: Swift.String(headerString))) + } + return items + } +} +""" + contents.shouldContainOnlyOnce(expectedContents) + } + private fun newTestContext(): TestContext { val settings = model.defaultSettings() model = AddOperationShapes.execute(model, settings.getService(model), settings.moduleName) diff --git a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/requestandresponse/requestflow/HttpQueryItemProviderGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/requestandresponse/requestflow/HttpQueryItemProviderGeneratorTests.kt index 7b700bbc3..e56be91bd 100644 --- a/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/requestandresponse/requestflow/HttpQueryItemProviderGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/requestandresponse/requestflow/HttpQueryItemProviderGeneratorTests.kt @@ -230,6 +230,50 @@ extension RequiredHttpFieldsInput { contents.shouldContainOnlyOnce(expectedContents) } + @Test + fun `009 it encodes float and double query items with encodeNumber`() { + // Floats & doubles must be rendered by URLEncodingUtils.encodeNumber so that the non-finite + // values are rendered as the Smithy-defined tokens NaN, Infinity, and -Infinity. Swift's own + // string interpolation would render them as nan, inf, and -inf, which are invalid on the wire. + val context = setupTests("http-float-bindings.smithy", "com.test#Example") + val contents = getModelFileContents("example/Sources/example", "FloatBindingsInput+QueryItemProvider.swift", context.manifest) + contents.shouldSyntacticSanityCheck() + val expectedContents = """ +extension FloatBindingsInput { + + static func queryItemProvider(_ value: FloatBindingsInput) throws -> [Smithy.URIQueryItem] { + var items = [Smithy.URIQueryItem]() + if let queryFloatList = value.queryFloatList { + queryFloatList.forEach { queryItemValue in + let queryItem = Smithy.URIQueryItem(name: "FloatList".urlPercentEncoding(), value: SmithyHTTPAPI.URLEncodingUtils.encodeNumber(queryItemValue).urlPercentEncoding()) + items.append(queryItem) + } + } + if let queryFloat = value.queryFloat { + let queryFloatQueryItem = Smithy.URIQueryItem(name: "Float".urlPercentEncoding(), value: SmithyHTTPAPI.URLEncodingUtils.encodeNumber(queryFloat).urlPercentEncoding()) + items.append(queryFloatQueryItem) + } + if let queryDoubleList = value.queryDoubleList { + queryDoubleList.forEach { queryItemValue in + let queryItem = Smithy.URIQueryItem(name: "DoubleList".urlPercentEncoding(), value: SmithyHTTPAPI.URLEncodingUtils.encodeNumber(queryItemValue).urlPercentEncoding()) + items.append(queryItem) + } + } + if let queryString = value.queryString { + let queryStringQueryItem = Smithy.URIQueryItem(name: "String".urlPercentEncoding(), value: Swift.String(queryString).urlPercentEncoding()) + items.append(queryStringQueryItem) + } + if let queryDouble = value.queryDouble { + let queryDoubleQueryItem = Smithy.URIQueryItem(name: "Double".urlPercentEncoding(), value: SmithyHTTPAPI.URLEncodingUtils.encodeNumber(queryDouble).urlPercentEncoding()) + items.append(queryDoubleQueryItem) + } + return items + } +} +""" + contents.shouldContainOnlyOnce(expectedContents) + } + private fun setupTests( smithyFile: String, serviceShapeId: String, diff --git a/smithy-swift-codegen/src/test/resources/http-float-bindings.smithy b/smithy-swift-codegen/src/test/resources/http-float-bindings.smithy new file mode 100644 index 000000000..4bd91cb7d --- /dev/null +++ b/smithy-swift-codegen/src/test/resources/http-float-bindings.smithy @@ -0,0 +1,58 @@ +$version: "1.0" + +namespace com.test + +use aws.api#service +use aws.protocols#restJson1 + +@service(sdkId: "Rest Json Protocol") +@restJson1 +service Example { + version: "2019-12-16", + operations: [ + FloatBindings + ] +} + +@readonly +@http(uri: "/FloatBindingsInput", method: "GET") +operation FloatBindings { + input: FloatBindingsInput +} + +structure FloatBindingsInput { + @httpQuery("Float") + queryFloat: Float, + + @httpQuery("Double") + queryDouble: Double, + + @httpQuery("FloatList") + queryFloatList: FloatList, + + @httpQuery("DoubleList") + queryDoubleList: DoubleList, + + @httpQuery("String") + queryString: String, + + @httpHeader("X-Float") + headerFloat: Float, + + @httpHeader("X-Double") + headerDouble: Double, + + @httpHeader("X-FloatList") + headerFloatList: FloatList, + + @httpHeader("X-String") + headerString: String, +} + +list FloatList { + member: Float, +} + +list DoubleList { + member: Double, +}