From 52746e5b5bdf8be8e1a134ace6066e792c190ea8 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 3 Oct 2024 09:40:32 +0200 Subject: [PATCH 1/2] Support nested arrays of primitive values inside of objects --- .../URICoder/Common/URIEncodedNode.swift | 10 +++ .../URICoder/Parsing/URIParser.swift | 1 - .../Serialization/URISerializer.swift | 75 +++++++++++++++++-- .../Test_URIValueFromNodeDecoder.swift | 17 +++++ .../Encoding/Test_URIValueToNodeEncoder.swift | 16 ++++ .../URICoder/Parsing/Test_URIParser.swift | 28 ++++--- .../Serialization/Test_URISerializer.swift | 18 +++-- 7 files changed, 133 insertions(+), 32 deletions(-) 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..d1c0b3e4 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,33 @@ 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 +140,18 @@ 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 +166,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 +251,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 +287,28 @@ 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..427c04b7 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,17 @@ 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..1c6e8999 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..f5c13398 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift @@ -124,18 +124,20 @@ final class Test_URISerializer: Test_Runtime { ), makeCase( value: .dictionary([ - "semi": .primitive(.string(";")), "dot": .primitive(.string(".")), + "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" ) ), ] From a7121900eae7abc303a35fc92b3653eacf5b5cbf Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 3 Oct 2024 09:52:17 +0200 Subject: [PATCH 2/2] Reformat --- .../Serialization/URISerializer.swift | 39 ++++++------------- .../Test_URIValueFromNodeDecoder.swift | 6 +-- .../URICoder/Parsing/Test_URIParser.swift | 6 +-- .../Serialization/Test_URISerializer.swift | 6 +-- 4 files changed, 18 insertions(+), 39 deletions(-) diff --git a/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift b/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift index d1c0b3e4..e7817720 100644 --- a/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift +++ b/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift @@ -82,14 +82,11 @@ extension URISerializer { /// - 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 .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)" + case .invalidConfiguration(let string): "URISerializer: Invalid configuration: \(string)" } } @@ -98,9 +95,7 @@ extension URISerializer { /// 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 - } + var errorDescription: String? { description } } /// Computes an escaped version of the provided string. @@ -140,12 +135,10 @@ 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) - } + 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) @@ -292,22 +285,12 @@ extension URISerializer { case .primitive(let primitive): try serializePrimitiveKeyValuePair(primitive, forKey: elementKey, separator: keyAndValueSeparator) case .arrayOfPrimitives(let array): - guard !array.isEmpty else { - return - } + guard !array.isEmpty else { return } for item in array.dropLast() { - try serializePrimitiveKeyValuePair( - item, - forKey: elementKey, - separator: keyAndValueSeparator - ) + try serializePrimitiveKeyValuePair(item, forKey: elementKey, separator: keyAndValueSeparator) data.append(pairSeparator) } - try serializePrimitiveKeyValuePair( - array.last!, - forKey: elementKey, - separator: keyAndValueSeparator - ) + try serializePrimitiveKeyValuePair(array.last!, forKey: elementKey, separator: keyAndValueSeparator) } } if let containerKeyAndValue = configuration.containerKeyAndValueSeparator { diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift index 427c04b7..f1236cb9 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift @@ -67,11 +67,7 @@ final class Test_URIValueFromNodeDecoder: Test_Runtime { // A struct with an array property. try test( - [ - "foo": ["bar"], - "bar": ["1", "2"], - "val": ["baz", "baq"] - ], + ["foo": ["bar"], "bar": ["1", "2"], "val": ["baz", "baq"]], StructWithArray(foo: "bar", bar: [1, 2], val: ["baz", "baq"]), key: "root" ) diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift index 1c6e8999..16a6e02d 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift @@ -79,8 +79,7 @@ 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: - "object%5Blist%5D=red&object%5Blist%5D=green&object%5Blist%5D=blue" + deepObjectExplode: "object%5Blist%5D=red&object%5Blist%5D=green&object%5Blist%5D=blue" ), value: ["list": ["red", "green", "blue"]] ), @@ -101,7 +100,8 @@ final class Test_URIParser: Test_Runtime { "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%5Blist%5D=one&keys%5Blist%5D=two&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": [","], "list": ["one", "two"]] ), diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift b/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift index f5c13398..f198b6eb 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift @@ -124,8 +124,7 @@ final class Test_URISerializer: Test_Runtime { ), makeCase( value: .dictionary([ - "semi": .primitive(.string(";")), - "dot": .primitive(.string(".")), + "semi": .primitive(.string(";")), "dot": .primitive(.string(".")), "comma": .primitive(.string(",")), "list": .array([.primitive(.string("one")), .primitive(.string("two"))]), ]), @@ -137,7 +136,8 @@ final class Test_URISerializer: Test_Runtime { 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" + deepObjectExplode: + "keys%5Bcomma%5D=%2C&keys%5Bdot%5D=.&keys%5Blist%5D=one&keys%5Blist%5D=two&keys%5Bsemi%5D=%3B" ) ), ]