diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift b/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift index 985b7715..4297f778 100644 --- a/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift +++ b/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift @@ -47,6 +47,16 @@ enum URIEncodedNode: Equatable { /// A date value. case date(Date) } + + /// A primitive value or an array of primitive values. + enum PrimitiveOrArrayOfPrimitives: Equatable { + + /// A primitive value. + case primitive(Primitive) + + /// An array of primitive values. + case arrayOfPrimitives([Primitive]) + } } extension URIEncodedNode { diff --git a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift index c1cb5940..ff224621 100644 --- a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift +++ b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift @@ -241,7 +241,6 @@ extension URIParser { appendPair(key, [value]) } } - for (key, value) in parseNode where value.count > 1 { throw ParsingError.malformedKeyValuePair(key) } return parseNode } } diff --git a/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift b/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift index 45d3b0da..e7817720 100644 --- a/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift +++ b/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift @@ -65,8 +65,7 @@ extension CharacterSet { extension URISerializer { /// A serializer error. - enum SerializationError: Swift.Error, Hashable { - + enum SerializationError: Swift.Error, Hashable, CustomStringConvertible, LocalizedError { /// Nested containers are not supported. case nestedContainersNotSupported /// Deep object arrays are not supported. @@ -75,6 +74,28 @@ extension URISerializer { case deepObjectsWithPrimitiveValuesNotSupported /// An invalid configuration was detected. case invalidConfiguration(String) + + /// A human-readable description of the serialization error. + /// + /// This computed property returns a string that includes information about the serialization error. + /// + /// - Returns: A string describing the serialization error and its associated details. + var description: String { + switch self { + case .nestedContainersNotSupported: "URISerializer: Nested containers are not supported" + case .deepObjectsArrayNotSupported: "URISerializer: Deep object arrays are not supported" + case .deepObjectsWithPrimitiveValuesNotSupported: + "URISerializer: Deep object with primitive values are not supported" + case .invalidConfiguration(let string): "URISerializer: Invalid configuration: \(string)" + } + } + + /// A localized description of the serialization error. + /// + /// This computed property provides a localized human-readable description of the serialization error, which is suitable for displaying to users. + /// + /// - Returns: A localized string describing the serialization error. + var errorDescription: String? { description } } /// Computes an escaped version of the provided string. @@ -114,6 +135,16 @@ extension URISerializer { guard case let .primitive(primitive) = node else { throw SerializationError.nestedContainersNotSupported } return primitive } + func unwrapPrimitiveOrArrayOfPrimitives(_ node: URIEncodedNode) throws + -> URIEncodedNode.PrimitiveOrArrayOfPrimitives + { + if case let .primitive(primitive) = node { return .primitive(primitive) } + if case let .array(array) = node { + let primitives = try array.map(unwrapPrimitiveValue) + return .arrayOfPrimitives(primitives) + } + throw SerializationError.nestedContainersNotSupported + } switch value { case .unset: // Nothing to serialize. @@ -128,7 +159,7 @@ extension URISerializer { try serializePrimitiveKeyValuePair(primitive, forKey: key, separator: keyAndValueSeparator) case .array(let array): try serializeArray(array.map(unwrapPrimitiveValue), forKey: key) case .dictionary(let dictionary): - try serializeDictionary(dictionary.mapValues(unwrapPrimitiveValue), forKey: key) + try serializeDictionary(dictionary.mapValues(unwrapPrimitiveOrArrayOfPrimitives), forKey: key) } } @@ -213,9 +244,10 @@ extension URISerializer { /// - key: The key to serialize the value under (details depend on the /// style and explode parameters in the configuration). /// - Throws: An error if serialization of the dictionary fails. - private mutating func serializeDictionary(_ dictionary: [String: URIEncodedNode.Primitive], forKey key: String) - throws - { + private mutating func serializeDictionary( + _ dictionary: [String: URIEncodedNode.PrimitiveOrArrayOfPrimitives], + forKey key: String + ) throws { guard !dictionary.isEmpty else { return } let sortedDictionary = dictionary.sorted { a, b in a.key.localizedCaseInsensitiveCompare(b.key) == .orderedAscending @@ -248,8 +280,18 @@ extension URISerializer { guard case .deepObject = configuration.style else { return elementKey } return rootKey + "[" + elementKey + "]" } - func serializeNext(_ element: URIEncodedNode.Primitive, forKey elementKey: String) throws { - try serializePrimitiveKeyValuePair(element, forKey: elementKey, separator: keyAndValueSeparator) + func serializeNext(_ element: URIEncodedNode.PrimitiveOrArrayOfPrimitives, forKey elementKey: String) throws { + switch element { + case .primitive(let primitive): + try serializePrimitiveKeyValuePair(primitive, forKey: elementKey, separator: keyAndValueSeparator) + case .arrayOfPrimitives(let array): + guard !array.isEmpty else { return } + for item in array.dropLast() { + try serializePrimitiveKeyValuePair(item, forKey: elementKey, separator: keyAndValueSeparator) + data.append(pairSeparator) + } + try serializePrimitiveKeyValuePair(array.last!, forKey: elementKey, separator: keyAndValueSeparator) + } } if let containerKeyAndValue = configuration.containerKeyAndValueSeparator { data.append(try stringifiedKey(key)) diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift index c805f3b6..f1236cb9 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift @@ -23,6 +23,12 @@ final class Test_URIValueFromNodeDecoder: Test_Runtime { var color: SimpleEnum? } + struct StructWithArray: Decodable, Equatable { + var foo: String + var bar: [Int]? + var val: [String] + } + enum SimpleEnum: String, Decodable, Equatable { case red case green @@ -59,6 +65,13 @@ final class Test_URIValueFromNodeDecoder: Test_Runtime { // A struct. try test(["foo": ["bar"]], SimpleStruct(foo: "bar"), key: "root") + // A struct with an array property. + try test( + ["foo": ["bar"], "bar": ["1", "2"], "val": ["baz", "baq"]], + StructWithArray(foo: "bar", bar: [1, 2], val: ["baz", "baq"]), + key: "root" + ) + // A struct with a nested enum. try test(["foo": ["bar"], "color": ["blue"]], SimpleStruct(foo: "bar", color: .blue), key: "root") diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIValueToNodeEncoder.swift b/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIValueToNodeEncoder.swift index 913511b6..80759c62 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIValueToNodeEncoder.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIValueToNodeEncoder.swift @@ -41,6 +41,12 @@ final class Test_URIValueToNodeEncoder: Test_Runtime { var val: SimpleEnum? } + struct StructWithArray: Encodable { + var foo: String + var bar: [Int]? + var val: [String] + } + struct NestedStruct: Encodable { var simple: SimpleStruct } let cases: [Case] = [ @@ -89,6 +95,16 @@ final class Test_URIValueToNodeEncoder: Test_Runtime { .dictionary(["foo": .primitive(.string("bar")), "val": .primitive(.string("foo"))]) ), + // A struct with an array property. + makeCase( + StructWithArray(foo: "bar", bar: [1, 2], val: ["baz", "baq"]), + .dictionary([ + "foo": .primitive(.string("bar")), + "bar": .array([.primitive(.integer(1)), .primitive(.integer(2))]), + "val": .array([.primitive(.string("baz")), .primitive(.string("baq"))]), + ]) + ), + // A nested struct. makeCase( NestedStruct(simple: SimpleStruct(foo: "bar")), diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift index 86c962e1..16a6e02d 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift @@ -79,33 +79,31 @@ final class Test_URIParser: Test_Runtime { simpleUnexplode: .custom("red,green,blue", value: ["": ["red", "green", "blue"]]), formDataExplode: "list=red&list=green&list=blue", formDataUnexplode: "list=red,green,blue", - deepObjectExplode: .custom( - "object%5Blist%5D=red&object%5Blist%5D=green&object%5Blist%5D=blue", - expectedError: .malformedKeyValuePair("list") - ) + deepObjectExplode: "object%5Blist%5D=red&object%5Blist%5D=green&object%5Blist%5D=blue" ), value: ["list": ["red", "green", "blue"]] ), makeCase( .init( - formExplode: "comma=%2C&dot=.&semi=%3B", + formExplode: "comma=%2C&dot=.&list=one&list=two&semi=%3B", formUnexplode: .custom( - "keys=comma,%2C,dot,.,semi,%3B", - value: ["keys": ["comma", ",", "dot", ".", "semi", ";"]] + "keys=comma,%2C,dot,.,list,one,list,two,semi,%3B", + value: ["keys": ["comma", ",", "dot", ".", "list", "one", "list", "two", "semi", ";"]] ), - simpleExplode: "comma=%2C,dot=.,semi=%3B", + simpleExplode: "comma=%2C,dot=.,list=one,list=two,semi=%3B", simpleUnexplode: .custom( - "comma,%2C,dot,.,semi,%3B", - value: ["": ["comma", ",", "dot", ".", "semi", ";"]] + "comma,%2C,dot,.,list,one,list,two,semi,%3B", + value: ["": ["comma", ",", "dot", ".", "list", "one", "list", "two", "semi", ";"]] ), - formDataExplode: "comma=%2C&dot=.&semi=%3B", + formDataExplode: "comma=%2C&dot=.&list=one&list=two&semi=%3B", formDataUnexplode: .custom( - "keys=comma,%2C,dot,.,semi,%3B", - value: ["keys": ["comma", ",", "dot", ".", "semi", ";"]] + "keys=comma,%2C,dot,.,list,one,list,two,semi,%3B", + value: ["keys": ["comma", ",", "dot", ".", "list", "one", "list", "two", "semi", ";"]] ), - deepObjectExplode: "keys%5Bcomma%5D=%2C&keys%5Bdot%5D=.&keys%5Bsemi%5D=%3B" + deepObjectExplode: + "keys%5Bcomma%5D=%2C&keys%5Bdot%5D=.&keys%5Blist%5D=one&keys%5Blist%5D=two&keys%5Bsemi%5D=%3B" ), - value: ["semi": [";"], "dot": ["."], "comma": [","]] + value: ["semi": [";"], "dot": ["."], "comma": [","], "list": ["one", "two"]] ), ] for testCase in cases { diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift b/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift index 688c508a..f198b6eb 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift @@ -126,16 +126,18 @@ final class Test_URISerializer: Test_Runtime { value: .dictionary([ "semi": .primitive(.string(";")), "dot": .primitive(.string(".")), "comma": .primitive(.string(",")), + "list": .array([.primitive(.string("one")), .primitive(.string("two"))]), ]), key: "keys", .init( - formExplode: "comma=%2C&dot=.&semi=%3B", - formUnexplode: "keys=comma,%2C,dot,.,semi,%3B", - simpleExplode: "comma=%2C,dot=.,semi=%3B", - simpleUnexplode: "comma,%2C,dot,.,semi,%3B", - formDataExplode: "comma=%2C&dot=.&semi=%3B", - formDataUnexplode: "keys=comma,%2C,dot,.,semi,%3B", - deepObjectExplode: "keys%5Bcomma%5D=%2C&keys%5Bdot%5D=.&keys%5Bsemi%5D=%3B" + formExplode: "comma=%2C&dot=.&list=one&list=two&semi=%3B", + formUnexplode: "keys=comma,%2C,dot,.,list,one,list,two,semi,%3B", + simpleExplode: "comma=%2C,dot=.,list=one,list=two,semi=%3B", + simpleUnexplode: "comma,%2C,dot,.,list,one,list,two,semi,%3B", + formDataExplode: "comma=%2C&dot=.&list=one&list=two&semi=%3B", + formDataUnexplode: "keys=comma,%2C,dot,.,list,one,list,two,semi,%3B", + deepObjectExplode: + "keys%5Bcomma%5D=%2C&keys%5Bdot%5D=.&keys%5Blist%5D=one&keys%5Blist%5D=two&keys%5Bsemi%5D=%3B" ) ), ]