Skip to content
Open
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
7 changes: 0 additions & 7 deletions Sources/OpenAPIRuntime/URICoder/Common/URICodeCodingKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
Expand Down
23 changes: 23 additions & 0 deletions Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
58 changes: 58 additions & 0 deletions Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
Loading