diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URICodeCodingKey.swift b/Sources/OpenAPIRuntime/URICoder/Common/URICodeCodingKey.swift index 5a86dc11..b2b665b3 100644 --- a/Sources/OpenAPIRuntime/URICoder/Common/URICodeCodingKey.swift +++ b/Sources/OpenAPIRuntime/URICoder/Common/URICodeCodingKey.swift @@ -27,13 +27,6 @@ struct URICoderCodingKey { /// The value to use in an integer-indexed collection (e.g. an int-keyed /// dictionary). var intValue: Int? - - /// Creates a new key with the same string and int value as the provided key. - /// - Parameter key: The key whose values to copy. - init(_ key: some CodingKey) { - self.stringValue = key.stringValue - self.intValue = key.intValue - } } extension URICoderCodingKey: CodingKey { diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift index c1ac27d7..fedf39e3 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift @@ -154,7 +154,9 @@ extension URIKeyedDecodingContainer: KeyedDecodingContainerProtocol { case is UInt64.Type: return try decode(UInt64.self, forKey: key) as! T case is Date.Type: return try decoder.dateTranscoder.decode(String(_decodeValue(forKey: key))) as! T default: - decoder.push(.init(key)) + // Only push the string value of the key, as keys e.g. in a string-keyed + // dictionary can have an integer value that doesn't represent an array index. + decoder.push(.init(stringValue: key.stringValue)) defer { decoder.pop() } return try type.init(from: decoder) } diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift index dc4c572b..4e3bac26 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift @@ -29,12 +29,15 @@ extension URIKeyedEncodingContainer { /// Inserts the provided node into the underlying dictionary at /// the provided key. + /// + /// Only the string value of the key is used, as keys e.g. in a string-keyed + /// dictionary can have an integer value that doesn't represent an array index. /// - Parameters: /// - node: The child node to insert. /// - key: The key for the child node. /// - Throws: An error if inserting the child node into the underlying dictionary at the provided key fails. private func _insertValue(_ node: URIEncodedNode, atKey key: Key) throws { - try encoder.currentStackEntry.storage.insert(node, atKey: key) + try encoder.currentStackEntry.storage.insert(node, atKey: URICoderCodingKey(stringValue: key.stringValue)) } /// Inserts the provided primitive value into the underlying dictionary at @@ -126,7 +129,7 @@ extension URIKeyedEncodingContainer: KeyedEncodingContainerProtocol { case let value as Bool: try encode(value, forKey: key) case let value as Date: try _insertValue(.date(value), atKey: key) default: - encoder.push(key: .init(key), newStorage: .unset) + encoder.push(key: .init(stringValue: key.stringValue), newStorage: .unset) try value.encode(to: encoder) try encoder.pop() } diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift index a0d9079b..e5590fcc 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift @@ -119,6 +119,29 @@ final class Test_URIDecoder: Test_Runtime { } } + func testDecoding_freeformObjectWithNumericKey() throws { + let decoder = URIDecoder(configuration: .formExplode) + let decodedValue = try decoder.decode(OpenAPIObjectContainer.self, forKey: "filter", from: "5=x&name=y") + XCTAssertEqual(decodedValue, try .init(unvalidatedValue: ["5": "x", "name": "y"])) + } + + func testDecoding_dictionaryOfEnumsWithNumericKey() throws { + enum Color: String, Decodable, Equatable { case red } + _test( + ["7": Color.red], + forKey: "keys", + from: .init( + formExplode: "7=red", + formUnexplode: "keys=7,red", + simpleExplode: "7=red", + simpleUnexplode: "7,red", + formDataExplode: "7=red", + formDataUnexplode: "keys=7,red", + deepObjectExplode: "keys%5B7%5D=red" + ) + ) + } + func testDecoding_rootValue() throws { let decoder = URIDecoder(configuration: .formDataExplode) do { diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift index 9c17d4e4..ab5a7c00 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift @@ -341,6 +341,64 @@ final class Test_URICodingRoundtrip: Test_Runtime { ) ) + // A dictionary with a numeric-looking key, which must not be treated as an array index. + try _test( + ["5": "x"], + key: "keys", + .init( + formExplode: "5=x", + formUnexplode: "keys=5,x", + simpleExplode: "5=x", + simpleUnexplode: "5,x", + formDataExplode: "5=x", + formDataUnexplode: "keys=5,x", + deepObjectExplode: "keys%5B5%5D=x" + ) + ) + try _test( + ["0": "zero"], + key: "keys", + .init( + formExplode: "0=zero", + formUnexplode: "keys=0,zero", + simpleExplode: "0=zero", + simpleUnexplode: "0,zero", + formDataExplode: "0=zero", + formDataUnexplode: "keys=0,zero", + deepObjectExplode: "keys%5B0%5D=zero" + ) + ) + + // A dictionary of non-primitive values with a numeric-looking key. + try _test( + ["7": SimpleEnum.red], + key: "keys", + .init( + formExplode: "7=red", + formUnexplode: "keys=7,red", + simpleExplode: "7=red", + simpleUnexplode: "7,red", + formDataExplode: "7=red", + formDataUnexplode: "keys=7,red", + deepObjectExplode: "keys%5B7%5D=red" + ) + ) + + // A free-form object with a numeric-looking key. + try _test( + OpenAPIObjectContainer(unvalidatedValue: ["5": "x"]), + key: "keys", + .init( + formExplode: "5=x", + formUnexplode: "keys=5,x", + simpleExplode: "5=x", + simpleUnexplode: "5,x", + formDataExplode: "5=x", + formDataUnexplode: "keys=5,x", + deepObjectExplode: "keys%5B5%5D=x" + ) + ) + // An empty dictionary. try _test( [:] as [String: String],