Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support nested arrays of primitive values inside of objects #120

Merged
merged 2 commits into from
Oct 3, 2024
Merged
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
10 changes: 10 additions & 0 deletions Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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])
czechboy0 marked this conversation as resolved.
Show resolved Hide resolved
}
}

extension URIEncodedNode {
Expand Down
1 change: 0 additions & 1 deletion Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,6 @@ extension URIParser {
appendPair(key, [value])
}
}
for (key, value) in parseNode where value.count > 1 { throw ParsingError.malformedKeyValuePair(key) }
return parseNode
}
}
Expand Down
58 changes: 50 additions & 8 deletions Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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] = [
Expand Down Expand Up @@ -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")),
Expand Down
28 changes: 13 additions & 15 deletions Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
),
]
Expand Down