From 4d6716f7bb9eca0a593bae264712ba1c28f8223e Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 13 Nov 2024 11:56:41 +0100 Subject: [PATCH 1/9] Refactor URIDecoder/URIParser to improve handling of the deepObject style --- .../Common/URICoderConfiguration.swift | 7 + .../URICoder/Common/URIEncodedNode.swift | 12 + .../URICoder/Common/URIParsedNode.swift | 37 +- .../URICoder/Decoding/URIDecoder.swift | 70 +- .../URIValueFromNodeDecoder+Keyed.swift | 17 +- .../URIValueFromNodeDecoder+Single.swift | 67 +- .../URIValueFromNodeDecoder+Unkeyed.swift | 28 +- .../Decoding/URIValueFromNodeDecoder.swift | 369 +++++---- .../URIValueToNodeEncoder+Unkeyed.swift | 4 + .../URICoder/Parsing/URIParser.swift | 352 +++++---- .../Serialization/URISerializer.swift | 2 +- .../Conversion/Test_Converter+Server.swift | 36 + .../URICoder/Decoder/Test_URIDecoder.swift | 138 +++- .../Test_URIValueFromNodeDecoder.swift | 47 +- .../URICoder/Parsing/Test_URIParser.swift | 747 ++++++++++++++---- .../Serialization/Test_URISerializer.swift | 4 - .../URICoder/Test_URICodingRoundtrip.swift | 47 +- .../URICoder/URICoderTestUtils.swift | 9 + 18 files changed, 1328 insertions(+), 665 deletions(-) diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift b/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift index ccbdb8c5..51bcd2bc 100644 --- a/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift +++ b/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift @@ -21,11 +21,18 @@ struct URICoderConfiguration { enum Style { /// A style for simple string variable expansion. + /// + /// The whole string always belongs to the root key. case simple /// A style for form-based URI expansion. + /// + /// Only some key/value pairs can belong to the root key, rest are ignored. case form + /// A style for nested variable expansion + /// + /// Only some key/value pairs can belong to the root key, rest are ignored. case deepObject } diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift b/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift index 4297f778..725cdabd 100644 --- a/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift +++ b/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift @@ -128,6 +128,18 @@ extension URIEncodedNode { } } + /// Marks the node as an array, starting of empty. + /// - Throws: If the node is already set to be anything else but an array. + mutating func markAsArray() throws { + switch self { + case .array: + // Already an array. + break + case .unset: self = .array([]) + default: throw InsertionError.appendingToNonArrayContainer + } + } + /// Appends a value to the array node. /// - Parameter childValue: The node to append to the underlying array. /// - Throws: If the node is already set to be anything else but an array. diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URIParsedNode.swift b/Sources/OpenAPIRuntime/URICoder/Common/URIParsedNode.swift index 51ecbe2a..c93a7c47 100644 --- a/Sources/OpenAPIRuntime/URICoder/Common/URIParsedNode.swift +++ b/Sources/OpenAPIRuntime/URICoder/Common/URIParsedNode.swift @@ -15,7 +15,27 @@ import Foundation /// The type used for keys by `URIParser`. -typealias URIParsedKey = String.SubSequence +typealias URIParsedKeyComponent = String.SubSequence + +struct URIParsedKey: Hashable, CustomStringConvertible { + + private(set) var components: [URIParsedKeyComponent] + + init(_ components: [URIParsedKeyComponent]) { self.components = components } + + static var empty: Self { .init([]) } + + func appending(_ component: URIParsedKeyComponent) -> Self { + var copy = self + copy.components.append(component) + return copy + } + + var description: String { + if components.isEmpty { return "" } + return components.joined(separator: "/") + } +} /// The type used for values by `URIParser`. typealias URIParsedValue = String.SubSequence @@ -23,5 +43,16 @@ typealias URIParsedValue = String.SubSequence /// The type used for an array of values by `URIParser`. typealias URIParsedValueArray = [URIParsedValue] -/// The type used for a node and a dictionary by `URIParser`. -typealias URIParsedNode = [URIParsedKey: URIParsedValueArray] +/// A key-value pair. +struct URIParsedPair: Equatable { + var key: URIParsedKey + var value: URIParsedValue +} + +typealias URIParsedPairArray = [URIParsedPair] + +typealias URIDecodedPrimitive = URIParsedValue + +typealias URIDecodedDictionary = [Substring: URIParsedValueArray] + +typealias URIDecodedArray = URIParsedValueArray diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift index 72cc077f..d9658d64 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift @@ -72,7 +72,7 @@ extension URIDecoder { /// - Returns: The decoded value. /// - Throws: An error if decoding fails, for example, due to incompatible data or key. func decode(_ type: T.Type = T.self, forKey key: String = "", from data: Substring) throws -> T { - try withCachedParser(from: data) { decoder in try decoder.decode(type, forKey: key) } + try withDecoder(from: data, forKey: key) { decoder in try decoder.decodeRoot(type) } } /// Attempt to decode an object from an URI string, if present. @@ -90,76 +90,22 @@ extension URIDecoder { /// - Throws: An error if decoding fails, for example, due to incompatible data or key. func decodeIfPresent(_ type: T.Type = T.self, forKey key: String = "", from data: Substring) throws -> T? - { try withCachedParser(from: data) { decoder in try decoder.decodeIfPresent(type, forKey: key) } } + { try withDecoder(from: data, forKey: key) { decoder in try decoder.decodeRootIfPresent(type) } } /// Make multiple decode calls on the parsed URI. /// /// Use to avoid repeatedly reparsing the raw string. /// - Parameters: /// - data: The URI-encoded string. + /// - key: The root key to decode. /// - calls: The closure that contains 0 or more calls to - /// the `decode` method on `URICachedDecoder`. + /// the `decode` method on `URIDecoderImpl`. /// - Returns: The result of the closure invocation. /// - Throws: An error if parsing or decoding fails. - func withCachedParser(from data: Substring, calls: (URICachedDecoder) throws -> R) throws -> R { - var parser = URIParser(configuration: configuration, data: data) - let parsedNode = try parser.parseRoot() - let decoder = URICachedDecoder(configuration: configuration, node: parsedNode) + func withDecoder(from data: Substring, forKey key: String, calls: (URIValueFromNodeDecoder) throws -> R) throws + -> R + { + let decoder = URIValueFromNodeDecoder(data: data, rootKey: key[...], configuration: configuration) return try calls(decoder) } } - -struct URICachedDecoder { - - /// The configuration used by the decoder. - fileprivate let configuration: URICoderConfiguration - - /// The node from which to decode a value on demand. - fileprivate let node: URIParsedNode - - /// Attempt to decode an object from an URI-encoded string. - /// - /// Under the hood, `URICachedDecoder` already has a pre-parsed - /// `URIParsedNode` and uses `URIValueFromNodeDecoder` to decode - /// the `Decodable` value. - /// - /// - Parameters: - /// - type: The type to decode. - /// - key: The key of the decoded value. Only used with certain styles - /// and explode options, ignored otherwise. - /// - Returns: The decoded value. - /// - Throws: An error if decoding fails. - func decode(_ type: T.Type = T.self, forKey key: String = "") throws -> T { - let decoder = URIValueFromNodeDecoder( - node: node, - rootKey: key[...], - style: configuration.style, - explode: configuration.explode, - dateTranscoder: configuration.dateTranscoder - ) - return try decoder.decodeRoot() - } - - /// Attempt to decode an object from an URI-encoded string, if present. - /// - /// Under the hood, `URICachedDecoder` already has a pre-parsed - /// `URIParsedNode` and uses `URIValueFromNodeDecoder` to decode - /// the `Decodable` value. - /// - /// - Parameters: - /// - type: The type to decode. - /// - key: The key of the decoded value. Only used with certain styles - /// and explode options, ignored otherwise. - /// - Returns: The decoded value. - /// - Throws: An error if decoding fails. - func decodeIfPresent(_ type: T.Type = T.self, forKey key: String = "") throws -> T? { - let decoder = URIValueFromNodeDecoder( - node: node, - rootKey: key[...], - style: configuration.style, - explode: configuration.explode, - dateTranscoder: configuration.dateTranscoder - ) - return try decoder.decodeRootIfPresent() - } -} diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift index fd47d462..9359f16a 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift @@ -19,9 +19,6 @@ struct URIKeyedDecodingContainer { /// The associated decoder. let decoder: URIValueFromNodeDecoder - - /// The underlying dictionary. - let values: URIParsedNode } extension URIKeyedDecodingContainer { @@ -32,7 +29,7 @@ extension URIKeyedDecodingContainer { /// - Returns: The value found for the provided key. /// - Throws: An error if no value for the key was found. private func _decodeValue(forKey key: Key) throws -> URIParsedValue { - guard let value = values[key.stringValue[...]]?.first else { + guard let value = try decoder.nestedElementInCurrentDictionary(forKey: key.stringValue) else { throw DecodingError.keyNotFound(key, .init(codingPath: codingPath, debugDescription: "Key not found.")) } return value @@ -97,9 +94,15 @@ extension URIKeyedDecodingContainer { extension URIKeyedDecodingContainer: KeyedDecodingContainerProtocol { - var allKeys: [Key] { values.keys.map { key in Key.init(stringValue: String(key))! } } + var allKeys: [Key] { + do { return try decoder.elementKeysInCurrentDictionary().compactMap { .init(stringValue: $0) } } catch { + return [] + } + } - func contains(_ key: Key) -> Bool { values[key.stringValue[...]] != nil } + func contains(_ key: Key) -> Bool { + do { return try decoder.containsElementInCurrentDictionary(forKey: key.stringValue) } catch { return false } + } var codingPath: [any CodingKey] { decoder.codingPath } @@ -153,7 +156,7 @@ 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: - try decoder.push(.init(key)) + decoder.push(.init(key)) defer { decoder.pop() } return try type.init(from: decoder) } diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift index 3c829873..57e3fcb0 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift @@ -24,7 +24,7 @@ struct URISingleValueDecodingContainer { extension URISingleValueDecodingContainer { /// The underlying value as a single value. - var value: URIParsedValue { get throws { try decoder.currentElementAsSingleValue() } } + var value: URIParsedValue? { get throws { try decoder.currentElementAsSingleValue() } } /// Returns the value found in the underlying node converted to /// the provided type. @@ -33,7 +33,17 @@ extension URISingleValueDecodingContainer { /// - Returns: The converted value found. /// - Throws: An error if the conversion failed. private func _decodeBinaryFloatingPoint(_: T.Type = T.self) throws -> T { - guard let double = try Double(value) else { + guard let value = try value else { + throw DecodingError.valueNotFound( + T.self, + DecodingError.Context.init( + codingPath: codingPath, + debugDescription: "Value not found.", + underlyingError: nil + ) + ) + } + guard let double = Double(value) else { throw DecodingError.typeMismatch( T.self, .init(codingPath: codingPath, debugDescription: "Failed to convert to Double.") @@ -49,7 +59,17 @@ extension URISingleValueDecodingContainer { /// - Returns: The converted value found. /// - Throws: An error if the conversion failed. private func _decodeFixedWidthInteger(_: T.Type = T.self) throws -> T { - guard let parsedValue = try T(value) else { + guard let value = try value else { + throw DecodingError.valueNotFound( + T.self, + DecodingError.Context.init( + codingPath: codingPath, + debugDescription: "Value not found.", + underlyingError: nil + ) + ) + } + guard let parsedValue = T(value) else { throw DecodingError.typeMismatch( T.self, .init(codingPath: codingPath, debugDescription: "Failed to convert to the requested type.") @@ -65,7 +85,17 @@ extension URISingleValueDecodingContainer { /// - Returns: The converted value found. /// - Throws: An error if the conversion failed. private func _decodeLosslessStringConvertible(_: T.Type = T.self) throws -> T { - guard let parsedValue = try T(String(value)) else { + guard let value = try value else { + throw DecodingError.valueNotFound( + T.self, + DecodingError.Context.init( + codingPath: codingPath, + debugDescription: "Value not found.", + underlyingError: nil + ) + ) + } + guard let parsedValue = T(String(value)) else { throw DecodingError.typeMismatch( T.self, .init(codingPath: codingPath, debugDescription: "Failed to convert to the requested type.") @@ -79,11 +109,23 @@ extension URISingleValueDecodingContainer: SingleValueDecodingContainer { var codingPath: [any CodingKey] { decoder.codingPath } - func decodeNil() -> Bool { false } + func decodeNil() -> Bool { do { return try value == nil } catch { return false } } func decode(_ type: Bool.Type) throws -> Bool { try _decodeLosslessStringConvertible() } - func decode(_ type: String.Type) throws -> String { try String(value) } + func decode(_ type: String.Type) throws -> String { + guard let value = try value else { + throw DecodingError.valueNotFound( + String.self, + DecodingError.Context.init( + codingPath: codingPath, + debugDescription: "Value not found.", + underlyingError: nil + ) + ) + } + return String(value) + } func decode(_ type: Double.Type) throws -> Double { try _decodeBinaryFloatingPoint() } @@ -125,7 +167,18 @@ extension URISingleValueDecodingContainer: SingleValueDecodingContainer { case is UInt16.Type: return try decode(UInt16.self) as! T case is UInt32.Type: return try decode(UInt32.self) as! T case is UInt64.Type: return try decode(UInt64.self) as! T - case is Date.Type: return try decoder.dateTranscoder.decode(String(value)) as! T + case is Date.Type: + guard let value = try value else { + throw DecodingError.valueNotFound( + T.self, + DecodingError.Context.init( + codingPath: codingPath, + debugDescription: "Value not found.", + underlyingError: nil + ) + ) + } + return try decoder.dateTranscoder.decode(String(value)) as! T default: return try T.init(from: decoder) } } diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift index 44a5cd28..061efaf5 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift @@ -16,25 +16,11 @@ import Foundation /// An unkeyed container used by `URIValueFromNodeDecoder`. struct URIUnkeyedDecodingContainer { - /// The associated decoder. let decoder: URIValueFromNodeDecoder - /// The underlying array. - let values: URIParsedValueArray - - /// The index of the item being currently decoded. - private var index: Int - - /// Creates a new unkeyed container ready to decode the first key. - /// - Parameters: - /// - decoder: The underlying decoder. - /// - values: The underlying array. - init(decoder: URIValueFromNodeDecoder, values: URIParsedValueArray) { - self.decoder = decoder - self.values = values - self.index = values.startIndex - } + /// The index of the next item to be decoded. + private var index: URIParsedValueArray.Index = 0 } extension URIUnkeyedDecodingContainer { @@ -46,7 +32,7 @@ extension URIUnkeyedDecodingContainer { /// - Throws: An error if the container ran out of items. private mutating func _decodingNext(in work: () throws -> R) throws -> R { guard !isAtEnd else { throw URIValueFromNodeDecoder.GeneralError.reachedEndOfUnkeyedContainer } - defer { values.formIndex(after: &index) } + defer { index += 1 } return try work() } @@ -55,7 +41,7 @@ extension URIUnkeyedDecodingContainer { /// - Returns: The next value found. /// - Throws: An error if the container ran out of items. private mutating func _decodeNext() throws -> URIParsedValue { - try _decodingNext { [values, index] in values[index] } + try _decodingNext { [decoder, index] in try decoder.nestedElementInCurrentArray(atIndex: index) } } /// Returns the next value converted to the provided type. @@ -111,9 +97,9 @@ extension URIUnkeyedDecodingContainer { extension URIUnkeyedDecodingContainer: UnkeyedDecodingContainer { - var count: Int? { values.count } + var count: Int? { try? decoder.countOfCurrentArray() } - var isAtEnd: Bool { index == values.endIndex } + var isAtEnd: Bool { index == count } var currentIndex: Int { index } @@ -168,7 +154,7 @@ extension URIUnkeyedDecodingContainer: UnkeyedDecodingContainer { case is Date.Type: return try decoder.dateTranscoder.decode(String(_decodeNext())) as! T default: return try _decodingNext { [decoder, currentIndex] in - try decoder.push(.init(intValue: currentIndex)) + decoder.push(.init(intValue: currentIndex)) defer { decoder.pop() } return try type.init(from: decoder) } diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift index 55982755..8d7ab6c2 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift @@ -14,46 +14,38 @@ import Foundation -/// A type that allows decoding `Decodable` values from a `URIParsedNode`. +/// A type that allows decoding `Decodable` values from `URIParser`'s output. final class URIValueFromNodeDecoder { - /// The coder used for serializing Date values. - let dateTranscoder: any DateTranscoder - - /// The underlying root node. - private let node: URIParsedNode + private let data: Substring + private let configuration: URICoderConfiguration + let dateTranscoder: any DateTranscoder + private let parser: URIParser + struct ParsedState { + var primitive: Result? + var array: Result? + var dictionary: Result? + } + private var state: ParsedState /// The key of the root value in the node. - private let rootKey: URIParsedKey - - /// The variable expansion style. - private let style: URICoderConfiguration.Style - - /// The explode parameter of the expansion style. - private let explode: Bool + let rootKey: URIParsedKeyComponent /// The stack of nested values within the root node. private var codingStack: [CodingStackEntry] /// Creates a new decoder. /// - Parameters: - /// - node: The underlying root node. + /// - data: The data to parse. /// - rootKey: The key of the root value in the node. - /// - style: The variable expansion style. - /// - explode: The explode parameter of the expansion style. - /// - dateTranscoder: The coder used for serializing Date values. - init( - node: URIParsedNode, - rootKey: URIParsedKey, - style: URICoderConfiguration.Style, - explode: Bool, - dateTranscoder: any DateTranscoder - ) { - self.node = node + /// - configuration: The configuration of the decoder. + init(data: Substring, rootKey: URIParsedKeyComponent, configuration: URICoderConfiguration) { + self.configuration = configuration + self.dateTranscoder = configuration.dateTranscoder + self.data = data + self.parser = .init(configuration: configuration, data: data) + self.state = .init() self.rootKey = rootKey - self.style = style - self.explode = explode - self.dateTranscoder = dateTranscoder self.codingStack = [] } @@ -81,8 +73,17 @@ final class URIValueFromNodeDecoder { /// - Returns: The decoded value. /// - Throws: When a decoding error occurs. func decodeRootIfPresent(_ type: T.Type = T.self) throws -> T? { - // The root is only nil if the node is empty. - if try currentElementAsArray().isEmpty { return nil } + switch configuration.style { + case .simple: + // Root is never nil, empty data just means an element with an empty string. + break + case .form: + // Try to parse as an array, check the number of elements. + if try withParsedRootAsArray({ $0.count == 0 }) { return nil } + case .deepObject: + // Try to parse as a dictionary, check the number of elements. + if try withParsedRootAsDictionary({ $0.count == 0 }) { return nil } + } return try decodeRoot(type) } } @@ -98,29 +99,6 @@ extension URIValueFromNodeDecoder { /// The decoder was asked for more items, but it was already at the /// end of the unkeyed container. case reachedEndOfUnkeyedContainer - - /// The provided coding key does not have a valid integer value, but - /// it is being used for accessing items in an unkeyed container. - case codingKeyNotInt - - /// The provided coding key is out of bounds of the unkeyed container. - case codingKeyOutOfBounds - - /// The coding key is of a value not found in the keyed container. - case codingKeyNotFound - } - - /// A node materialized by the decoder. - private enum URIDecodedNode { - - /// A single value. - case single(URIParsedValue) - - /// An array of values. - case array(URIParsedValueArray) - - /// A dictionary of values. - case dictionary(URIParsedNode) } /// An entry in the coding stack for `URIValueFromNodeDecoder`. @@ -130,30 +108,13 @@ extension URIValueFromNodeDecoder { /// The key at which the entry was found. var key: URICoderCodingKey - - /// The node at the key inside its parent. - var element: URIDecodedNode } - /// The element at the current head of the coding stack. - private var currentElement: URIDecodedNode { codingStack.last?.element ?? .dictionary(node) } - /// Pushes a new container on top of the current stack, nesting into the /// value at the provided key. /// - Parameter codingKey: The coding key for the value that is then put /// at the top of the stack. - /// - Throws: An error if an issue occurs during the container push operation. - func push(_ codingKey: URICoderCodingKey) throws { - let nextElement: URIDecodedNode - if let intValue = codingKey.intValue { - let value = try nestedValueInCurrentElementAsArray(at: intValue) - nextElement = .single(value) - } else { - let values = try nestedValuesInCurrentElementAsDictionary(forKey: codingKey.stringValue) - nextElement = .array(values) - } - codingStack.append(CodingStackEntry(key: codingKey, element: nextElement)) - } + func push(_ codingKey: URICoderCodingKey) { codingStack.append(CodingStackEntry(key: codingKey)) } /// Pops the top container from the stack and restores the previously top /// container to be the current top container. @@ -167,135 +128,169 @@ extension URIValueFromNodeDecoder { throw DecodingError.typeMismatch(String.self, .init(codingPath: codingPath, debugDescription: message)) } - /// Extracts the root value of the provided node using the root key. - /// - Parameter node: The node which to expect for the root key. - /// - Returns: The value found at the root key in the provided node. - /// - Throws: A `DecodingError` if the value is not found at the root key - private func rootValue(in node: URIParsedNode) throws -> URIParsedValueArray { - guard let value = node[rootKey] else { - if style == .simple, let valueForFallbackKey = node[""] { - // The simple style doesn't encode the key, so single values - // get encoded as a value only, and parsed under the empty - // string key. - return valueForFallbackKey + // MARK: - withParsed methods + + private func withParsedRootAsPrimitive(_ work: (URIDecodedPrimitive?) throws -> R) throws -> R { + let value: URIDecodedPrimitive? + if let cached = state.primitive { + value = try cached.get() + } else { + let result: Result + do { + value = try parser.parseRootAsPrimitive(rootKey: rootKey)?.value + result = .success(value) + } catch { + result = .failure(error) + throw error } - return [] + state.primitive = result } - return value + return try work(value) } - /// Extracts the node at the top of the coding stack and tries to treat it - /// as a dictionary. - /// - Returns: The value if it can be treated as a dictionary. - /// - Throws: An error if the current element cannot be treated as a dictionary. - private func currentElementAsDictionary() throws -> URIParsedNode { try nodeAsDictionary(currentElement) } - - /// Checks if the provided node can be treated as a dictionary, and returns - /// it if so. - /// - Parameter node: The node to check. - /// - Returns: The value if it can be treated as a dictionary. - /// - Throws: An error if the node cannot be treated as a valid dictionary. - private func nodeAsDictionary(_ node: URIDecodedNode) throws -> URIParsedNode { - // There are multiple ways a valid dictionary is represented in a node, - // depends on the explode parameter. - // 1. exploded: Key-value pairs in the node: ["R":["100"]] - // 2. unexploded form: Flattened key-value pairs in the only top level - // key's value array: ["":["R","100"]] - // To simplify the code, when asked for a keyed container here and explode - // is false, we convert (2) to (1), and then treat everything as (1). - // The conversion only works if the number of values is even, including 0. - if explode { - guard case let .dictionary(values) = node else { - try throwMismatch("Cannot treat a single value or an array as a dictionary.") + private func withParsedRootAsDictionary(_ work: (URIDecodedDictionary) throws -> R) throws -> R { + let value: URIDecodedDictionary + if let cached = state.dictionary { + value = try cached.get() + } else { + let result: Result + do { + func normalizedDictionaryKey(_ key: URIParsedKey) throws -> Substring { + func validateComponentCount(_ count: Int) throws { + guard key.components.count == count else { + try throwMismatch( + "Decoding a dictionary key encountered an unexpected number of components (expected: \(count), got: \(key.components.count)." + ) + } + } + switch (configuration.style, configuration.explode) { + case (.form, true), (.simple, _): + try validateComponentCount(1) + return key.components[0] + case (.form, false), (.deepObject, true): + try validateComponentCount(2) + return key.components[1] + case (.deepObject, false): try throwMismatch("Decoding deepObject + unexplode is not supported.") + } + } + + let tuples = try parser.parseRootAsDictionary(rootKey: rootKey) + let normalizedTuples: [(Substring, [URIParsedValue])] = try tuples.map { pair in + try (normalizedDictionaryKey(pair.key), [pair.value]) + } + value = Dictionary(normalizedTuples, uniquingKeysWith: +) + result = .success(value) + } catch { + result = .failure(error) + throw error } - return values - } - let values = try nodeAsArray(node) - if values == [""] && style == .simple { - // An unexploded simple combination produces a ["":[""]] for an - // empty string. It should be parsed as an empty dictionary. - return ["": [""]] - } - guard values.count % 2 == 0 else { - try throwMismatch("Cannot parse an unexploded dictionary an odd number of elements.") + state.dictionary = result } - let pairs = stride(from: values.startIndex, to: values.endIndex, by: 2) - .map { firstIndex in (values[firstIndex], [values[firstIndex + 1]]) } - let convertedNode = Dictionary(pairs, uniquingKeysWith: { $0 + $1 }) - return convertedNode + return try work(value) } - /// Extracts the node at the top of the coding stack and tries to treat it - /// as an array. - /// - Returns: The value if it can be treated as an array. - /// - Throws: An error if the node cannot be treated as an array. - private func currentElementAsArray() throws -> URIParsedValueArray { try nodeAsArray(currentElement) } - - /// Checks if the provided node can be treated as an array, and returns - /// it if so. - /// - Parameter node: The node to check. - /// - Returns: The value if it can be treated as an array. - /// - Throws: An error if the node cannot be treated as a valid array. - private func nodeAsArray(_ node: URIDecodedNode) throws -> URIParsedValueArray { - switch node { - case .single(let value): return [value] - case .array(let values): return values - case .dictionary(let values): return try rootValue(in: values) + private func withParsedRootAsArray(_ work: (URIDecodedArray) throws -> R) throws -> R { + let value: URIDecodedArray + if let cached = state.array { + value = try cached.get() + } else { + let result: Result + do { + value = try parser.parseRootAsArray(rootKey: rootKey).map(\.value) + result = .success(value) + } catch { + result = .failure(error) + throw error + } + state.array = result } + return try work(value) } - /// Extracts the node at the top of the coding stack and tries to treat it - /// as a primitive value. - /// - Returns: The value if it can be treated as a primitive value. - /// - Throws: An error if the node cannot be treated as a primitive value. - func currentElementAsSingleValue() throws -> URIParsedValue { try nodeAsSingleValue(currentElement) } - - /// Checks if the provided node can be treated as a primitive value, and - /// returns it if so. - /// - Parameter node: The node to check. - /// - Returns: The value if it can be treated as a primitive value. - /// - Throws: An error if the node cannot be treated as a primitive value. - private func nodeAsSingleValue(_ node: URIDecodedNode) throws -> URIParsedValue { - // A single value can be parsed from a node that: - // 1. Has a single key-value pair - // 2. The value array has a single element. - let array: URIParsedValueArray - switch node { - case .single(let value): return value - case .array(let values): array = values - case .dictionary(let values): array = try rootValue(in: values) + // MARK: - decoding utilities + func primitiveValue(forKey key: String, in dictionary: URIDecodedDictionary) throws -> URIParsedValue? { + let values = dictionary[key[...], default: []] + if values.isEmpty { return nil } + if values.count > 1 { try throwMismatch("Dictionary value contains multiple values.") } + return values[0] + } + // MARK: - withCurrent methods + + private func withCurrentPrimitiveElement(_ work: (URIDecodedPrimitive?) throws -> R) throws -> R { + if !codingStack.isEmpty { + // Nesting is involved. + // There are exactly three scenarios we support: + // - primitive in a top level array + // - primitive in a top level dictionary + // - primitive in a nested array inside a top level dictionary + if codingStack.count == 1 { + let key = codingStack[0].key + if let intKey = key.intValue { + // Top level array. + return try withParsedRootAsArray { array in try work(array[intKey]) } + } else { + // Top level dictionary. + return try withParsedRootAsDictionary { dictionary in + try work(primitiveValue(forKey: key.stringValue, in: dictionary)) + } + } + } else if codingStack.count == 2 { + // Nested array within a top level dictionary. + let dictionaryKey = codingStack[0].key.stringValue[...] + guard let nestedArrayKey = codingStack[1].key.intValue else { + try throwMismatch("Nested coding key is not an integer, hinting at unsupported nesting.") + } + return try withParsedRootAsDictionary { dictionary in + try work(dictionary[dictionaryKey, default: []][nestedArrayKey]) + } + } else { + try throwMismatch("Arbitrary nesting of containers is not supported.") + } + } else { + // Top level primitive. + return try withParsedRootAsPrimitive { try work($0) } } - guard array.count == 1 else { - if style == .simple { return Substring(array.joined(separator: ",")) } - let reason = array.isEmpty ? "an empty node" : "a node with multiple values" - try throwMismatch("Cannot parse a value from \(reason).") + } + + private func withCurrentArrayElements(_ work: (URIDecodedArray) throws -> R) throws -> R { + if let nestedArrayParentKey = codingStack.first?.key { + // Top level is dictionary, first level nesting is array. + // Get all the elements that match this rootKey + nested key path. + return try withParsedRootAsDictionary { dictionary in + try work(dictionary[nestedArrayParentKey.stringValue[...]] ?? []) + } + } else { + // Top level array. + return try withParsedRootAsArray { try work($0) } } - let value = array[0] - return value } - /// Returns the nested value at the provided index inside the node at the - /// top of the coding stack. - /// - Parameter index: The index of the nested value. - /// - Returns: The nested value. - /// - Throws: An error if the current node is not a valid array, or if the - /// index is out of bounds. - private func nestedValueInCurrentElementAsArray(at index: Int) throws -> URIParsedValue { - let values = try currentElementAsArray() - guard index < values.count else { throw GeneralError.codingKeyOutOfBounds } - return values[index] + private func withCurrentDictionaryElements(_ work: (URIDecodedDictionary) throws -> R) throws -> R { + if !codingStack.isEmpty { + try throwMismatch("Nesting a dictionary inside another container is not supported.") + } else { + // Top level dictionary. + return try withParsedRootAsDictionary { try work($0) } + } } - /// Returns the nested value at the provided key inside the node at the - /// top of the coding stack. - /// - Parameter key: The key of the nested value. - /// - Returns: The nested value. - /// - Throws: An error if the current node is not a valid dictionary, or - /// if no value exists for the key. - private func nestedValuesInCurrentElementAsDictionary(forKey key: String) throws -> URIParsedValueArray { - let values = try currentElementAsDictionary() - guard let value = values[key[...]] else { throw GeneralError.codingKeyNotFound } - return value + // MARK: - metadata and data accessors + + func currentElementAsSingleValue() throws -> URIParsedValue? { try withCurrentPrimitiveElement { $0 } } + + func countOfCurrentArray() throws -> Int { try withCurrentArrayElements { $0.count } } + + func nestedElementInCurrentArray(atIndex index: Int) throws -> URIParsedValue { + try withCurrentArrayElements { $0[index] } + } + func nestedElementInCurrentDictionary(forKey key: String) throws -> URIParsedValue? { + try withCurrentDictionaryElements { dictionary in try primitiveValue(forKey: key, in: dictionary) } + } + func containsElementInCurrentDictionary(forKey key: String) throws -> Bool { + try withCurrentDictionaryElements { dictionary in dictionary[key[...]] != nil } + } + func elementKeysInCurrentDictionary() throws -> [String] { + try withCurrentDictionaryElements { dictionary in dictionary.keys.map(String.init) } } } @@ -306,14 +301,10 @@ extension URIValueFromNodeDecoder: Decoder { var userInfo: [CodingUserInfoKey: Any] { [:] } func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key: CodingKey { - let values = try currentElementAsDictionary() - return .init(URIKeyedDecodingContainer(decoder: self, values: values)) + KeyedDecodingContainer(URIKeyedDecodingContainer(decoder: self)) } - func unkeyedContainer() throws -> any UnkeyedDecodingContainer { - let values = try currentElementAsArray() - return URIUnkeyedDecodingContainer(decoder: self, values: values) - } + func unkeyedContainer() throws -> any UnkeyedDecodingContainer { URIUnkeyedDecodingContainer(decoder: self) } func singleValueContainer() throws -> any SingleValueDecodingContainer { URISingleValueDecodingContainer(decoder: self) diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift index 35f71884..caf84b67 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift @@ -19,6 +19,10 @@ struct URIUnkeyedEncodingContainer { /// The associated encoder. let encoder: URIValueToNodeEncoder + init(encoder: URIValueToNodeEncoder) { + self.encoder = encoder + try? encoder.currentStackEntry.storage.markAsArray() + } } extension URIUnkeyedEncodingContainer { diff --git a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift index ff224621..6e9fbd86 100644 --- a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift +++ b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift @@ -14,24 +14,22 @@ import Foundation -/// A type that parses a `URIParsedNode` from a URI-encoded string. +/// A type that can parse a primitive, array, and a dictionary from a URI-encoded string. struct URIParser: Sendable { /// The configuration instructing the parser how to interpret the raw /// string. private let configuration: URICoderConfiguration - /// The underlying raw string storage. - private var data: Raw + private let data: Raw /// Creates a new parser. /// - Parameters: - /// - configuration: The configuration instructing the parser how - /// to interpret the raw string. + /// - configuration: The configuration instructing the parser how to interpret the raw string. /// - data: The string to parse. - init(configuration: URICoderConfiguration, data: Substring) { + init(configuration: URICoderConfiguration, data: Substring, ) { self.configuration = configuration - self.data = data[...] + self.data = data } } @@ -43,6 +41,7 @@ enum ParsingError: Swift.Error, Hashable { /// A malformed key-value pair was detected. case malformedKeyValuePair(Raw) + /// An invalid configuration was detected. case invalidConfiguration(String) } @@ -51,197 +50,252 @@ enum ParsingError: Swift.Error, Hashable { extension URIParser { - /// Parses the root node from the underlying string, selecting the logic - /// based on the configuration. - /// - Returns: The parsed root node. - /// - Throws: An error if parsing fails. - mutating func parseRoot() throws -> URIParsedNode { - // A completely empty string should get parsed as a single - // empty key with a single element array with an empty string - // if the style is simple, otherwise it's an empty dictionary. - if data.isEmpty { - switch configuration.style { - case .form: return [:] - case .simple: return ["": [""]] - case .deepObject: return [:] - } - } + func parseRootAsPrimitive(rootKey: URIParsedKeyComponent) throws -> URIParsedPair? { + var data = data switch (configuration.style, configuration.explode) { - case (.form, true): return try parseExplodedFormRoot() - case (.form, false): return try parseUnexplodedFormRoot() - case (.simple, true): return try parseExplodedSimpleRoot() - case (.simple, false): return try parseUnexplodedSimpleRoot() - case (.deepObject, true): return try parseExplodedDeepObjectRoot() + case (.form, _): + let keyValueSeparator: Character = "=" + let pairSeparator: Character = "&" + while !data.isEmpty { + let (firstResult, firstValue) = data.parseUpToEitherCharacterOrEnd( + first: keyValueSeparator, + second: pairSeparator + ) + switch firstResult { + case .foundFirst: + let unescapedKey = unescapeValue(firstValue) + if unescapedKey == rootKey { + let secondValue = data.parseUpToCharacterOrEnd(pairSeparator) + let key = URIParsedKey([unescapedKey]) + return .init(key: key, value: unescapeValue(secondValue)) + } else { + // Ignore the value, skip to the end of the pair. + _ = data.parseUpToCharacterOrEnd(pairSeparator) + } + case .foundSecondOrEnd: throw ParsingError.malformedKeyValuePair(firstValue) + } + } + return nil + case (.simple, _): return .init(key: .empty, value: unescapeValue(data)) + case (.deepObject, true): + throw ParsingError.invalidConfiguration("deepObject does not support primitive values, only dictionaries") case (.deepObject, false): - let reason = "Deep object style is only valid with explode set to true" - throw ParsingError.invalidConfiguration(reason) + throw ParsingError.invalidConfiguration("deepObject + explode: false is not supported") } } - /// Parses the root node assuming the raw string uses the form style - /// and the explode parameter is enabled. - /// - Returns: The parsed root node. - /// - Throws: An error if parsing fails. - private mutating func parseExplodedFormRoot() throws -> URIParsedNode { - try parseGenericRoot { data, appendPair in + func parseRootAsArray(rootKey: URIParsedKeyComponent) throws -> URIParsedPairArray { + var data = data + switch (configuration.style, configuration.explode) { + case (.form, true): let keyValueSeparator: Character = "=" let pairSeparator: Character = "&" - + var items: URIParsedPairArray = [] while !data.isEmpty { let (firstResult, firstValue) = data.parseUpToEitherCharacterOrEnd( first: keyValueSeparator, second: pairSeparator ) - let key: Raw - let value: Raw switch firstResult { case .foundFirst: - // Hit the key/value separator, so a value will follow. - let secondValue = data.parseUpToCharacterOrEnd(pairSeparator) - key = firstValue - value = secondValue - case .foundSecondOrEnd: - // No key/value separator, treat the string as the key. - key = firstValue - value = .init() + let unescapedKey = unescapeValue(firstValue) + if unescapedKey == rootKey { + let secondValue = data.parseUpToCharacterOrEnd(pairSeparator) + let key = URIParsedKey([unescapedKey]) + items.append(.init(key: key, value: unescapeValue(secondValue))) + } else { + // Ignore the value, skip to the end of the pair. + _ = data.parseUpToCharacterOrEnd(pairSeparator) + } + case .foundSecondOrEnd: throw ParsingError.malformedKeyValuePair(firstValue) } - appendPair(key, [value]) } - } - } - - /// Parses the root node assuming the raw string uses the form style - /// and the explode parameter is disabled. - /// - Returns: The parsed root node. - /// - Throws: An error if parsing fails. - private mutating func parseUnexplodedFormRoot() throws -> URIParsedNode { - try parseGenericRoot { data, appendPair in + return items + case (.form, false): let keyValueSeparator: Character = "=" let pairSeparator: Character = "&" - let valueSeparator: Character = "," - + let arrayElementSeparator: Character = "," + var items: URIParsedPairArray = [] while !data.isEmpty { let (firstResult, firstValue) = data.parseUpToEitherCharacterOrEnd( first: keyValueSeparator, second: pairSeparator ) - let key: Raw - let values: [Raw] switch firstResult { case .foundFirst: - // Hit the key/value separator, so one or more values will follow. - var accumulatedValues: [Raw] = [] - valueLoop: while !data.isEmpty { - let (secondResult, secondValue) = data.parseUpToEitherCharacterOrEnd( - first: valueSeparator, - second: pairSeparator - ) - accumulatedValues.append(secondValue) - switch secondResult { - case .foundFirst: - // Hit the value separator, so ended one value and - // another one is coming. - continue - case .foundSecondOrEnd: - // Hit the pair separator or the end, this is the - // last value. - break valueLoop + let unescapedKey = unescapeValue(firstValue) + if unescapedKey == rootKey { + let key = URIParsedKey([unescapedKey]) + elementScan: while !data.isEmpty { + let (secondResult, secondValue) = data.parseUpToEitherCharacterOrEnd( + first: arrayElementSeparator, + second: pairSeparator + ) + items.append(.init(key: key, value: unescapeValue(secondValue))) + switch secondResult { + case .foundFirst: continue elementScan + case .foundSecondOrEnd: break elementScan + } } + } else { + // Ignore the value, skip to the end of the pair. + _ = data.parseUpToCharacterOrEnd(pairSeparator) } - if accumulatedValues.isEmpty { - // We hit the key/value separator, so always write - // at least one empty value. - accumulatedValues.append("") - } - key = firstValue - values = accumulatedValues case .foundSecondOrEnd: throw ParsingError.malformedKeyValuePair(firstValue) } - appendPair(key, values) } + return items + case (.simple, _): + let pairSeparator: Character = "," + var items: URIParsedPairArray = [] + while !data.isEmpty { + let value = data.parseUpToCharacterOrEnd(pairSeparator) + items.append(.init(key: .empty, value: unescapeValue(value))) + } + return items + case (.deepObject, true): + throw ParsingError.invalidConfiguration("deepObject does not support array values, only dictionaries") + case (.deepObject, false): + throw ParsingError.invalidConfiguration("deepObject + explode: false is not supported") } } - /// Parses the root node assuming the raw string uses the simple style - /// and the explode parameter is enabled. - /// - Returns: The parsed root node. - /// - Throws: An error if parsing fails. - private mutating func parseExplodedSimpleRoot() throws -> URIParsedNode { - try parseGenericRoot { data, appendPair in + func parseRootAsDictionary(rootKey: URIParsedKeyComponent) throws -> URIParsedPairArray { + var data = data + switch (configuration.style, configuration.explode) { + case (.form, true): + let keyValueSeparator: Character = "=" + let pairSeparator: Character = "&" + var items: URIParsedPairArray = [] + while !data.isEmpty { + let (firstResult, firstValue) = data.parseUpToEitherCharacterOrEnd( + first: keyValueSeparator, + second: pairSeparator + ) + switch firstResult { + case .foundFirst: + let secondValue = data.parseUpToCharacterOrEnd(pairSeparator) + let key = URIParsedKey([unescapeValue(firstValue)]) + items.append(.init(key: key, value: unescapeValue(secondValue))) + case .foundSecondOrEnd: throw ParsingError.malformedKeyValuePair(firstValue) + } + } + return items + case (.form, false): + let keyValueSeparator: Character = "=" + let pairSeparator: Character = "&" + let arrayElementSeparator: Character = "," + var items: URIParsedPairArray = [] + while !data.isEmpty { + let (firstResult, firstValue) = data.parseUpToEitherCharacterOrEnd( + first: keyValueSeparator, + second: pairSeparator + ) + switch firstResult { + case .foundFirst: + let unescapedKey = unescapeValue(firstValue) + if unescapedKey == rootKey { + let parentKey = URIParsedKey([unescapedKey]) + elementScan: while !data.isEmpty { + let (innerKeyResult, innerKeyValue) = data.parseUpToEitherCharacterOrEnd( + first: arrayElementSeparator, + second: pairSeparator + ) + switch innerKeyResult { + case .foundFirst: + let (innerValueResult, innerValueValue) = data.parseUpToEitherCharacterOrEnd( + first: arrayElementSeparator, + second: pairSeparator + ) + items.append( + .init( + key: parentKey.appending(innerKeyValue), + value: unescapeValue(innerValueValue) + ) + ) + switch innerValueResult { + case .foundFirst: continue elementScan + case .foundSecondOrEnd: break elementScan + } + case .foundSecondOrEnd: throw ParsingError.malformedKeyValuePair(innerKeyValue) + } + } + } else { + // Ignore the value, skip to the end of the pair. + _ = data.parseUpToCharacterOrEnd(pairSeparator) + } + case .foundSecondOrEnd: throw ParsingError.malformedKeyValuePair(firstValue) + } + } + return items + case (.simple, true): let keyValueSeparator: Character = "=" let pairSeparator: Character = "," - + var items: URIParsedPairArray = [] while !data.isEmpty { let (firstResult, firstValue) = data.parseUpToEitherCharacterOrEnd( first: keyValueSeparator, second: pairSeparator ) - let key: Raw - let value: Raw + let key: URIParsedKey + let value: URIParsedValue switch firstResult { case .foundFirst: - // Hit the key/value separator, so a value will follow. let secondValue = data.parseUpToCharacterOrEnd(pairSeparator) - key = firstValue + key = URIParsedKey([unescapeValue(firstValue)]) value = secondValue - case .foundSecondOrEnd: - // No key/value separator, treat the string as the value. - key = .init() - value = firstValue + items.append(.init(key: key, value: unescapeValue(value))) + case .foundSecondOrEnd: throw ParsingError.malformedKeyValuePair(firstValue) } - appendPair(key, [value]) } - } - } - - /// Parses the root node assuming the raw string uses the simple style - /// and the explode parameter is disabled. - /// - Returns: The parsed root node. - /// - Throws: An error if parsing fails. - private mutating func parseUnexplodedSimpleRoot() throws -> URIParsedNode { - // Unexploded simple dictionary cannot be told apart from - // an array, so we just accumulate all pairs as standalone - // values and add them to the array. It'll be the higher - // level decoder's responsibility to parse this properly. - - try parseGenericRoot { data, appendPair in + return items + case (.simple, false): let pairSeparator: Character = "," + var items: URIParsedPairArray = [] while !data.isEmpty { - let value = data.parseUpToCharacterOrEnd(pairSeparator) - appendPair(.init(), [value]) + let rawKey = data.parseUpToCharacterOrEnd(pairSeparator) + let value: URIParsedValue + if data.isEmpty { value = "" } else { value = data.parseUpToCharacterOrEnd(pairSeparator) } + let key = URIParsedKey([unescapeValue(rawKey)]) + items.append(.init(key: key, value: unescapeValue(value))) } - } - } - /// Parses the root node assuming the raw string uses the deepObject style - /// and the explode parameter is enabled. - /// - Returns: The parsed root node. - /// - Throws: An error if parsing fails. - private mutating func parseExplodedDeepObjectRoot() throws -> URIParsedNode { - let parseNode = try parseGenericRoot { data, appendPair in + return items + case (.deepObject, true): let keyValueSeparator: Character = "=" let pairSeparator: Character = "&" - let nestedKeyStartingCharacter: Character = "[" - let nestedKeyEndingCharacter: Character = "]" - func nestedKey(from deepObjectKey: String.SubSequence) -> Raw { - var unescapedDeepObjectKey = Substring(deepObjectKey.removingPercentEncoding ?? "") - let topLevelKey = unescapedDeepObjectKey.parseUpToCharacterOrEnd(nestedKeyStartingCharacter) - let nestedKey = unescapedDeepObjectKey.parseUpToCharacterOrEnd(nestedKeyEndingCharacter) - return nestedKey.isEmpty ? topLevelKey : nestedKey - } + let nestedKeyStart: Character = "[" + let nestedKeyEnd: Character = "]" + var items: URIParsedPairArray = [] while !data.isEmpty { let (firstResult, firstValue) = data.parseUpToEitherCharacterOrEnd( first: keyValueSeparator, second: pairSeparator ) - guard case .foundFirst = firstResult else { throw ParsingError.malformedKeyValuePair(firstValue) } - // Hit the key/value separator, so a value will follow. - let secondValue = data.parseUpToCharacterOrEnd(pairSeparator) - let key = nestedKey(from: firstValue) - let value = secondValue - appendPair(key, [value]) + switch firstResult { + case .foundFirst: + var unescapedComposedKey = unescapeValue(firstValue) + if unescapedComposedKey.contains("[") && unescapedComposedKey.contains("]") { + // Do a quick check whether this is even a deepObject-encoded key, as + // we need to safely skip any unrelated keys, which might be formatted + // some other way. + let parentParsedKey = unescapedComposedKey.parseUpToCharacterOrEnd(nestedKeyStart) + let childParsedKey = unescapedComposedKey.parseUpToCharacterOrEnd(nestedKeyEnd) + if parentParsedKey == rootKey { + let key = URIParsedKey([parentParsedKey, childParsedKey]) + let secondValue = data.parseUpToCharacterOrEnd(pairSeparator) + items.append(.init(key: key, value: unescapeValue(secondValue))) + continue + } + } + // Ignore the value, skip to the end of the pair. + _ = data.parseUpToCharacterOrEnd(pairSeparator) + case .foundSecondOrEnd: throw ParsingError.malformedKeyValuePair(firstValue) + } } + return items + case (.deepObject, false): + throw ParsingError.invalidConfiguration("deepObject + explode: false is not supported") } - return parseNode } } @@ -249,24 +303,6 @@ extension URIParser { extension URIParser { - /// Parses the underlying string using a parser closure. - /// - Parameter parser: A closure that accepts another closure, which should - /// be called 0 or more times, once for each parsed key-value pair. - /// - Returns: The accumulated node. - /// - Throws: An error if parsing using the provided parser closure fails, - private mutating func parseGenericRoot(_ parser: (inout Raw, (Raw, [Raw]) -> Void) throws -> Void) throws - -> URIParsedNode - { - var root = URIParsedNode() - let spaceEscapingCharacter = configuration.spaceEscapingCharacter - let unescapeValue: (Raw) -> Raw = { Self.unescapeValue($0, spaceEscapingCharacter: spaceEscapingCharacter) } - try parser(&data) { key, values in - let newItem = [unescapeValue(key): values.map(unescapeValue)] - root.merge(newItem) { $0 + $1 } - } - return root - } - /// Removes escaping from the provided string. /// - Parameter escapedValue: An escaped string. /// - Returns: The provided string with escaping removed. diff --git a/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift b/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift index e7817720..838ca9b1 100644 --- a/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift +++ b/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift @@ -205,7 +205,6 @@ extension URISerializer { /// style and explode parameters in the configuration). /// - Throws: An error if serialization of the array fails. private mutating func serializeArray(_ array: [URIEncodedNode.Primitive], forKey key: String) throws { - guard !array.isEmpty else { return } let keyAndValueSeparator: String? let pairSeparator: String switch (configuration.style, configuration.explode) { @@ -220,6 +219,7 @@ extension URISerializer { pairSeparator = "," case (.deepObject, _): throw SerializationError.deepObjectsArrayNotSupported } + guard !array.isEmpty else { return } func serializeNext(_ element: URIEncodedNode.Primitive) throws { if let keyAndValueSeparator { try serializePrimitiveKeyValuePair(element, forKey: key, separator: keyAndValueSeparator) diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift index b2305a08..179d40a3 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift @@ -171,6 +171,30 @@ final class Test_ServerConverterExtensions: Test_Runtime { XCTAssertEqual(value, ["foo", "bar"]) } + func test_getOptionalQueryItemAsURI_deepObject_exploded() throws { + let query: Substring = "sort%5Bid%5D=ascending&sort%5Bname%5D=descending&unrelated=foo" + let value: [String: String]? = try converter.getOptionalQueryItemAsURI( + in: query, + style: .deepObject, + explode: true, + name: "sort", + as: [String: String].self + ) + XCTAssertEqual(value, ["id": "ascending", "name": "descending"]) + } + + func test_getOptionalQueryItemAsURI_deepObject_exploded_empty() throws { + let query: Substring = "foo=bar" + let value: [String: String]? = try converter.getOptionalQueryItemAsURI( + in: query, + style: .deepObject, + explode: true, + name: "sort", + as: [String: String].self + ) + XCTAssertNil(value) + } + func test_getRequiredQueryItemAsURI_arrayOfStrings() throws { let query: Substring = "search=foo&search=bar" let value: [String] = try converter.getRequiredQueryItemAsURI( @@ -195,6 +219,18 @@ final class Test_ServerConverterExtensions: Test_Runtime { XCTAssertEqual(value, ["foo", "bar"]) } + func test_getRequiredQueryItemAsURI_deepObject_exploded() throws { + let query: Substring = "sort%5Bid%5D=ascending&unrelated=foo&sort%5Bname%5D=descending" + let value: [String: String] = try converter.getRequiredQueryItemAsURI( + in: query, + style: .deepObject, + explode: true, + name: "sort", + as: [String: String].self + ) + XCTAssertEqual(value, ["id": "ascending", "name": "descending"]) + } + func test_getOptionalQueryItemAsURI_date() throws { let query: Substring = "search=\(testDateEscapedString)" let value: Date? = try converter.getOptionalQueryItemAsURI( diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift index c02c83c3..0e611789 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift @@ -16,11 +16,76 @@ import XCTest final class Test_URIDecoder: Test_Runtime { - func testDecoding() throws { + func testDecoding_string() throws { + _test( + "hello world", + forKey: "message", + from: .init( + formExplode: "message=hello%20world", + formUnexplode: "message=hello%20world", + simpleExplode: "hello%20world", + simpleUnexplode: "hello%20world", + formDataExplode: "message=hello+world", + formDataUnexplode: "message=hello+world", + deepObjectExplode: nil + ) + ) + } + + func testDecoding_maxNesting() throws { + struct Filter: Decodable, Equatable { + enum State: String, Decodable, Equatable { + case enabled + case disabled + } + var state: [State] + } + _test( + Filter(state: [.enabled, .disabled]), + forKey: "filter", + from: .init( + formExplode: "state=enabled&state=disabled", + formUnexplode: "filter=state,enabled,state,disabled", + simpleExplode: "state=enabled,state=disabled", + simpleUnexplode: "state,enabled,state,disabled", + formDataExplode: "state=enabled&state=disabled", + formDataUnexplode: "filter=state,enabled,state,disabled", + deepObjectExplode: "filter%5Bstate%5D=enabled&filter%5Bstate%5D=disabled" + ) + ) + } + + func testDecoding_array() throws { + _test( + ["hello world", "goodbye world"], + forKey: "message", + from: .init( + formExplode: "message=hello%20world&message=goodbye%20world", + formUnexplode: "message=hello%20world,goodbye%20world", + simpleExplode: "hello%20world,goodbye%20world", + simpleUnexplode: "hello%20world,goodbye%20world", + formDataExplode: "message=hello+world&message=goodbye+world", + formDataUnexplode: "message=hello+world,goodbye+world", + deepObjectExplode: nil + ) + ) + } + + func testDecoding_struct() throws { struct Foo: Decodable, Equatable { var bar: String } - let decoder = URIDecoder(configuration: .formDataExplode) - let decodedValue = try decoder.decode(Foo.self, forKey: "", from: "bar=hello+world") - XCTAssertEqual(decodedValue, Foo(bar: "hello world")) + _test( + Foo(bar: "hello world"), + forKey: "message", + from: .init( + formExplode: "bar=hello%20world", + formUnexplode: "message=bar,hello%20world", + simpleExplode: "bar=hello%20world", + simpleUnexplode: "bar,hello%20world", + formDataExplode: "bar=hello+world", + formDataUnexplode: "message=bar,hello+world", + deepObjectExplode: "message%5Bbar%5D=hello%20world" + ) + ) } func testDecoding_structWithOptionalProperty() throws { @@ -39,6 +104,21 @@ final class Test_URIDecoder: Test_Runtime { } } + func testDecoding_freeformObject() throws { + let decoder = URIDecoder(configuration: .formDataExplode) + do { + let decodedValue = try decoder.decode( + OpenAPIObjectContainer.self, + forKey: "", + from: "baz=1&bar=hello+world&bar=goodbye+world" + ) + XCTAssertEqual( + decodedValue, + try .init(unvalidatedValue: ["bar": ["hello world", "goodbye world"], "baz": 1]) + ) + } + } + func testDecoding_rootValue() throws { let decoder = URIDecoder(configuration: .formDataExplode) do { @@ -73,3 +153,53 @@ final class Test_URIDecoder: Test_Runtime { } } } + +extension Test_URIDecoder { + + struct Inputs { + var formExplode: Substring? + var formUnexplode: Substring? + var simpleExplode: Substring? + var simpleUnexplode: Substring? + var formDataExplode: Substring? + var formDataUnexplode: Substring? + var deepObjectExplode: Substring? + } + + func _test( + _ value: T, + forKey key: String, + from inputs: Inputs, + file: StaticString = #file, + line: UInt = #line + ) { + func _run(name: String, configuration: URICoderConfiguration, sourceString: Substring) { + let decoder = URIDecoder(configuration: configuration) + do { + let decodedValue = try decoder.decode(T.self, forKey: key, from: sourceString) + XCTAssertEqual(decodedValue, value, "Failed in \(name)", file: file, line: line) + } catch { XCTFail("Threw an error in \(name): \(error)", file: file, line: line) } + } + if let value = inputs.formExplode { + _run(name: "formExplode", configuration: .formExplode, sourceString: value) + } + if let value = inputs.formUnexplode { + _run(name: "formUnexplode", configuration: .formUnexplode, sourceString: value) + } + if let value = inputs.simpleExplode { + _run(name: "simpleExplode", configuration: .simpleExplode, sourceString: value) + } + if let value = inputs.simpleUnexplode { + _run(name: "simpleUnexplode", configuration: .simpleUnexplode, sourceString: value) + } + if let value = inputs.formDataExplode { + _run(name: "formDataExplode", configuration: .formDataExplode, sourceString: value) + } + if let value = inputs.formDataUnexplode { + _run(name: "formDataUnexplode", configuration: .formDataUnexplode, sourceString: value) + } + if let value = inputs.deepObjectExplode { + _run(name: "deepObjectExplode", configuration: .deepObjectExplode, sourceString: value) + } + } +} diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift index f1236cb9..6ce7037b 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift @@ -36,60 +36,56 @@ final class Test_URIValueFromNodeDecoder: Test_Runtime { } // An empty string. - try test(["root": [""]], "", key: "root") + try test("root=", "", key: "root") // An empty string with a simple style. - try test(["root": [""]], "", key: "root", style: .simple) + try test("", "", key: "root", style: .simple) // A string with a space. - try test(["root": ["Hello World"]], "Hello World", key: "root") + try test("root=Hello%20world", "Hello world", key: "root") // An enum. - try test(["root": ["red"]], SimpleEnum.red, key: "root") + try test("root=red", SimpleEnum.red, key: "root") // An integer. - try test(["root": ["1234"]], 1234, key: "root") + try test("root=1234", 1234, key: "root") // A float. - try test(["root": ["12.34"]], 12.34, key: "root") + try test("root=12.34", 12.34, key: "root") // A bool. - try test(["root": ["true"]], true, key: "root") + try test("root=true", true, key: "root") // A simple array of strings. - try test(["root": ["a", "b", "c"]], ["a", "b", "c"], key: "root") + try test("root=a&root=b&root=c", ["a", "b", "c"], key: "root") // A simple array of enums. - try test(["root": ["red", "green", "blue"]], [.red, .green, .blue] as [SimpleEnum], key: "root") + try test("root=red&root=green&root=blue", [.red, .green, .blue] as [SimpleEnum], key: "root") // A struct. - try test(["foo": ["bar"]], SimpleStruct(foo: "bar"), key: "root") + 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"]], + "foo=bar&bar=1&bar=2&val=baz&val=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") + try test("foo=bar&color=blue", SimpleStruct(foo: "bar", color: .blue), key: "root") // A simple dictionary. - try test(["one": ["1"], "two": ["2"]], ["one": 1, "two": 2], key: "root") + try test("one=1&two=2", ["one": 1, "two": 2], key: "root") // A unexploded simple dictionary. - try test(["root": ["one", "1", "two", "2"]], ["one": 1, "two": 2], key: "root", explode: false) + try test("one,1,two,2", ["one": 1, "two": 2], key: "root", style: .simple, explode: false) // A dictionary of enums. - try test( - ["one": ["blue"], "two": ["green"]], - ["one": .blue, "two": .green] as [String: SimpleEnum], - key: "root" - ) + try test("one=blue&two=green", ["one": .blue, "two": .green] as [String: SimpleEnum], key: "root") func test( - _ node: URIParsedNode, + _ data: String, _ expectedValue: T, key: String, style: URICoderConfiguration.Style = .form, @@ -98,11 +94,14 @@ final class Test_URIValueFromNodeDecoder: Test_Runtime { line: UInt = #line ) throws { let decoder = URIValueFromNodeDecoder( - node: node, + data: data[...], rootKey: key[...], - style: style, - explode: explode, - dateTranscoder: .iso8601 + configuration: .init( + style: style, + explode: explode, + spaceEscapingCharacter: .percentEncoded, + dateTranscoder: .iso8601 + ) ) let decodedValue = try decoder.decodeRoot(T.self) XCTAssertEqual(decodedValue, expectedValue, file: file, line: line) diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift index 16a6e02d..dd8c2215 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift @@ -14,137 +14,495 @@ import XCTest @testable import OpenAPIRuntime +/// Tests for URIParser. +/// +/// Guiding examples: +/// +/// rootKey: "color" +/// +/// form explode: +/// - nil: "" -> nil +/// - empty: "color=" -> ("color/0", "") +/// - primitive: "color=blue" -> ("color/0", "blue") +/// - array: "color=blue&color=black&color=brown" -> [("color/0", "blue"), ("color/1", "black"), ("color/2", "brown)] +/// - dictionary+array: "R=100&G=200&G=150" -> [("color/R/0", "100"), ("color/G/0", "200"), ("color/G/1", "150")] +/// +/// form unexplode: +/// - nil: "" -> nil +/// - empty: "color=" -> ("color/0", "") +/// - primitive: "color=blue" -> ("color/0", "blue") +/// - array: "color=blue,black,brown" -> [("color/0", "blue"), ("color/1", "black"), ("color/2", "brown)] +/// - dictionary: "color=R,100,G,200,G,150" -> [("color/R/0", "100"), ("color/G/0", "200"), ("color/G/1", "150")] +/// +/// simple explode: +/// - nil: "" -> ("color/0", "") +/// - empty: "" -> ("color/0", "") +/// - primitive: "blue" -> ("color/0", "blue") +/// - array: "blue,black,brown" -> [("color/0", "blue"), ("color/1", "black"), ("color/2", "brown)] +/// - dictionary+array: "R=100,G=200,G=150" -> [("color/R/0", "100"), ("color/G/0", "200"), ("color/G/1", "150")] +/// +/// simple unexplode: +/// - nil: "" -> ("color/0", "") +/// - empty: "" -> ("color/0", "") +/// - primitive: "blue" -> ("color/0", "blue") +/// - array: "blue,black,brown" -> [("color/0", "blue"), ("color/1", "black"), ("color/2", "brown)] +/// - dictionary: "R,100,G,200,G,150" -> [("color/R/0", "100"), ("color/G/0", "200"), ("color/G/1", "150")] +/// +/// deepObject unexplode: unsupported +/// +/// deepObject explode: +/// - nil: -> unsupported +/// - empty: -> unsupported +/// - primitive: -> unsupported +/// - array: -> unsupported +/// - dictionary: "color%5BR%5D=100&color%5BG%5D=200&color%5BG%5D=150" +/// -> [("color/R/0", "100"), ("color/G/0", "200"), ("color/G/1", "150")] final class Test_URIParser: Test_Runtime { - let testedVariants: [URICoderConfiguration] = [ - .formExplode, .formUnexplode, .simpleExplode, .simpleUnexplode, .formDataExplode, .formDataUnexplode, - .deepObjectExplode, - ] - func testParsing() throws { - let cases: [Case] = [ - makeCase( - .init( - formExplode: "empty=", - formUnexplode: "empty=", - simpleExplode: .custom("", value: ["": [""]]), - simpleUnexplode: .custom("", value: ["": [""]]), - formDataExplode: "empty=", - formDataUnexplode: "empty=", - deepObjectExplode: "object%5Bempty%5D=" - ), - value: ["empty": [""]] - ), - makeCase( - .init( - formExplode: "", - formUnexplode: "", - simpleExplode: .custom("", value: ["": [""]]), - simpleUnexplode: .custom("", value: ["": [""]]), - formDataExplode: "", - formDataUnexplode: "", - deepObjectExplode: "" - ), - value: [:] - ), - makeCase( - .init( - formExplode: "who=fred", - formUnexplode: "who=fred", - simpleExplode: .custom("fred", value: ["": ["fred"]]), - simpleUnexplode: .custom("fred", value: ["": ["fred"]]), - formDataExplode: "who=fred", - formDataUnexplode: "who=fred", - deepObjectExplode: "object%5Bwho%5D=fred" - ), - value: ["who": ["fred"]] - ), - makeCase( - .init( - formExplode: "hello=Hello%20World", - formUnexplode: "hello=Hello%20World", - simpleExplode: .custom("Hello%20World", value: ["": ["Hello World"]]), - simpleUnexplode: .custom("Hello%20World", value: ["": ["Hello World"]]), - formDataExplode: "hello=Hello+World", - formDataUnexplode: "hello=Hello+World", - deepObjectExplode: "object%5Bhello%5D=Hello%20World" - ), - value: ["hello": ["Hello World"]] - ), - makeCase( - .init( - formExplode: "list=red&list=green&list=blue", - formUnexplode: "list=red,green,blue", - simpleExplode: .custom("red,green,blue", value: ["": ["red", "green", "blue"]]), - 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" - ), - value: ["list": ["red", "green", "blue"]] - ), - makeCase( - .init( - formExplode: "comma=%2C&dot=.&list=one&list=two&semi=%3B", - formUnexplode: .custom( - "keys=comma,%2C,dot,.,list,one,list,two,semi,%3B", - value: ["keys": ["comma", ",", "dot", ".", "list", "one", "list", "two", "semi", ";"]] - ), - simpleExplode: "comma=%2C,dot=.,list=one,list=two,semi=%3B", - simpleUnexplode: .custom( - "comma,%2C,dot,.,list,one,list,two,semi,%3B", - value: ["": ["comma", ",", "dot", ".", "list", "one", "list", "two", "semi", ";"]] - ), - formDataExplode: "comma=%2C&dot=.&list=one&list=two&semi=%3B", - formDataUnexplode: .custom( - "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" - ), - value: ["semi": [";"], "dot": ["."], "comma": [","], "list": ["one", "two"]] - ), - ] - for testCase in cases { - func testVariant(_ variant: Case.Variant, _ input: Case.Variants.Input) throws { - var parser = URIParser(configuration: variant.config, data: input.string[...]) - do { - let parsedNode = try parser.parseRoot() - XCTAssertEqual( - parsedNode, - input.valueOverride ?? testCase.value, - "Failed for config: \(variant.name)", - file: testCase.file, - line: testCase.line - ) - } catch { - guard let expectedError = input.expectedError, let parsingError = error as? ParsingError else { - XCTAssert(false, "Unexpected error thrown: \(error)", file: testCase.file, line: testCase.line) - return - } - XCTAssertEqual( - expectedError, - parsingError, - "Failed for config: \(variant.name)", - file: testCase.file, - line: testCase.line - ) - } - } - let variants = testCase.variants - try testVariant(.formExplode, variants.formExplode) - try testVariant(.formUnexplode, variants.formUnexplode) - try testVariant(.simpleExplode, variants.simpleExplode) - try testVariant(.simpleUnexplode, variants.simpleUnexplode) - try testVariant(.formDataExplode, variants.formDataExplode) - try testVariant(.formDataUnexplode, variants.formDataUnexplode) - try testVariant(.deepObjectExplode, variants.deepObjectExplode) - } + // Guiding examples, test filtering relevant keys for the rootKey + try testCase( + formExplode: .init( + rootKey: "color", + primitive: .assert("prefix=bar&color=blue&suffix=baz", equals: .init(key: "color", value: "blue")), + array: .assert( + "prefix=bar&color=blue&color=black&color=brown&suffix=baz", + equals: [ + .init(key: "color", value: "blue"), .init(key: "color", value: "black"), + .init(key: "color", value: "brown"), + ] + ), + dictionary: .assert( + "R=100&G=200&G=150", + equals: [ + .init(key: "R", value: "100"), .init(key: "G", value: "200"), .init(key: "G", value: "150"), + ] + ) + ), + formUnexplode: .init( + rootKey: "color", + primitive: .assert("prefix=bar&color=blue&suffix=baz", equals: .init(key: "color", value: "blue")), + array: .assert( + "prefix=bar&color=blue,black,brown&suffix=baz", + equals: [ + .init(key: "color", value: "blue"), .init(key: "color", value: "black"), + .init(key: "color", value: "brown"), + ] + ), + dictionary: .assert( + "prefix=bar&color=R,100,G,200,G,150&suffix=baz", + equals: [ + .init(key: "color/R", value: "100"), .init(key: "color/G", value: "200"), + .init(key: "color/G", value: "150"), + ] + ) + ), + simpleExplode: .init( + rootKey: "color", + primitive: .assert("blue", equals: .init(key: .empty, value: "blue")), + array: .assert( + "blue,black,brown", + equals: [ + .init(key: .empty, value: "blue"), .init(key: .empty, value: "black"), + .init(key: .empty, value: "brown"), + ] + ), + dictionary: .assert( + "R=100,G=200,G=150", + equals: [ + .init(key: "R", value: "100"), .init(key: "G", value: "200"), .init(key: "G", value: "150"), + ] + ) + ), + simpleUnexplode: .init( + rootKey: "color", + primitive: .assert("blue", equals: .init(key: .empty, value: "blue")), + array: .assert( + "blue,black,brown", + equals: [ + .init(key: .empty, value: "blue"), .init(key: .empty, value: "black"), + .init(key: .empty, value: "brown"), + ] + ), + dictionary: .assert( + "R,100,G,200,G,150", + equals: [ + .init(key: "R", value: "100"), .init(key: "G", value: "200"), .init(key: "G", value: "150"), + ] + ) + ), + formDataExplode: .init( + rootKey: "color", + primitive: .assert("prefix=bar&color=blue&suffix=baz", equals: .init(key: "color", value: "blue")), + array: .assert( + "prefix=bar&color=blue&color=black&color=brown&suffix=baz", + equals: [ + .init(key: "color", value: "blue"), .init(key: "color", value: "black"), + .init(key: "color", value: "brown"), + ] + ), + dictionary: .assert( + "R=100&G=200&G=150", + equals: [ + .init(key: "R", value: "100"), .init(key: "G", value: "200"), .init(key: "G", value: "150"), + ] + ) + ), + formDataUnexplode: .init( + rootKey: "color", + primitive: .assert("prefix=bar&color=blue&suffix=baz", equals: .init(key: "color", value: "blue")), + array: .assert( + "prefix=bar&color=blue,black,brown&suffix=baz", + equals: [ + .init(key: "color", value: "blue"), .init(key: "color", value: "black"), + .init(key: "color", value: "brown"), + ] + ), + dictionary: .assert( + "prefix=bar&color=R,100,G,200,G,150&suffix=baz", + equals: [ + .init(key: "color/R", value: "100"), .init(key: "color/G", value: "200"), + .init(key: "color/G", value: "150"), + ] + ) + ), + deepObjectExplode: .init( + rootKey: "color", + primitive: .init( + string: "", + result: .failure({ error in + guard case .invalidConfiguration = error else { + XCTFail("Unexpected error: \(error)") + return + } + }) + ), + array: .init( + string: "", + result: .failure({ error in + guard case .invalidConfiguration = error else { + XCTFail("Unexpected error: \(error)") + return + } + }) + ), + dictionary: .assert( + "prefix%5Bfoo%5D=1&color%5BR%5D=100&color%5BG%5D=200&color%5BG%5D=150&suffix%5Bbaz%5D=2", + equals: [ + .init(key: "color/R", value: "100"), .init(key: "color/G", value: "200"), + .init(key: "color/G", value: "150"), + ] + ) + ) + ) + + // Test escaping + try testCase( + formExplode: .init( + rootKey: "message", + primitive: .assert("message=Hello%20world", equals: .init(key: "message", value: "Hello world")), + array: .assert( + "message=Hello%20world&message=%240", + equals: [.init(key: "message", value: "Hello world"), .init(key: "message", value: "$0")] + ), + dictionary: .assert( + "R=Hello%20world&G=%24%24%24&G=%40%40%40", + equals: [ + .init(key: "R", value: "Hello world"), .init(key: "G", value: "$$$"), + .init(key: "G", value: "@@@"), + ] + ) + ), + formUnexplode: .init( + rootKey: "message", + primitive: .assert("message=Hello%20world", equals: .init(key: "message", value: "Hello world")), + array: .assert( + "message=Hello%20world,%240", + equals: [.init(key: "message", value: "Hello world"), .init(key: "message", value: "$0")] + ), + dictionary: .assert( + "message=R,Hello%20world,G,%24%24%24,G,%40%40%40", + equals: [ + .init(key: "message/R", value: "Hello world"), .init(key: "message/G", value: "$$$"), + .init(key: "message/G", value: "@@@"), + ] + ) + ), + simpleExplode: .init( + rootKey: "message", + primitive: .assert("Hello%20world", equals: .init(key: .empty, value: "Hello world")), + array: .assert( + "Hello%20world,%24%24%24,%40%40%40", + equals: [ + .init(key: .empty, value: "Hello world"), .init(key: .empty, value: "$$$"), + .init(key: .empty, value: "@@@"), + ] + ), + dictionary: .assert( + "R=Hello%20world,G=%24%24%24,G=%40%40%40", + equals: [ + .init(key: "R", value: "Hello world"), .init(key: "G", value: "$$$"), + .init(key: "G", value: "@@@"), + ] + ) + ), + simpleUnexplode: .init( + rootKey: "message", + primitive: .assert("Hello%20world", equals: .init(key: .empty, value: "Hello world")), + array: .assert( + "Hello%20world,%24%24%24,%40%40%40", + equals: [ + .init(key: .empty, value: "Hello world"), .init(key: .empty, value: "$$$"), + .init(key: .empty, value: "@@@"), + ] + ), + dictionary: .assert( + "R,Hello%20world,G,%24%24%24,G,%40%40%40", + equals: [ + .init(key: "R", value: "Hello world"), .init(key: "G", value: "$$$"), + .init(key: "G", value: "@@@"), + ] + ) + ), + formDataExplode: .init( + rootKey: "message", + primitive: .assert("message=Hello+world", equals: .init(key: "message", value: "Hello world")), + array: .assert( + "message=Hello+world&message=%240", + equals: [.init(key: "message", value: "Hello world"), .init(key: "message", value: "$0")] + ), + dictionary: .assert( + "R=Hello+world&G=%24%24%24&G=%40%40%40", + equals: [ + .init(key: "R", value: "Hello world"), .init(key: "G", value: "$$$"), + .init(key: "G", value: "@@@"), + ] + ) + ), + formDataUnexplode: .init( + rootKey: "message", + primitive: .assert("message=Hello+world", equals: .init(key: "message", value: "Hello world")), + array: .assert( + "message=Hello+world,%240", + equals: [.init(key: "message", value: "Hello world"), .init(key: "message", value: "$0")] + ), + dictionary: .assert( + "message=R,Hello+world,G,%24%24%24,G,%40%40%40", + equals: [ + .init(key: "message/R", value: "Hello world"), .init(key: "message/G", value: "$$$"), + .init(key: "message/G", value: "@@@"), + ] + ) + ), + deepObjectExplode: .init( + rootKey: "message", + primitive: .init( + string: "", + result: .failure({ error in + guard case .invalidConfiguration = error else { + XCTFail("Unexpected error: \(error)") + return + } + }) + ), + array: .init( + string: "", + result: .failure({ error in + guard case .invalidConfiguration = error else { + XCTFail("Unexpected error: \(error)") + return + } + }) + ), + dictionary: .assert( + "message%5BR%5D=Hello%20world&message%5BG%5D=%24%24%24&message%5BG%5D=%40%40%40", + equals: [ + .init(key: "message/R", value: "Hello world"), .init(key: "message/G", value: "$$$"), + .init(key: "message/G", value: "@@@"), + ] + ) + ) + ) + + // Missing/nil + try testCase( + formExplode: .init( + rootKey: "color", + primitive: .assert("prefix=bar&suffix=baz", equals: nil), + array: .assert("prefix=bar&suffix=baz", equals: []), + dictionary: .assert("", equals: []) + ), + formUnexplode: .init( + rootKey: "color", + primitive: .assert("prefix=bar&suffix=baz", equals: nil), + array: .assert("prefix=bar&suffix=baz", equals: []), + dictionary: .assert("prefix=bar&suffix=baz", equals: []) + ), + simpleExplode: .init( + rootKey: "color", + primitive: .assert("", equals: .init(key: .empty, value: "")), + array: .assert("", equals: []), + dictionary: .assert("", equals: []) + ), + simpleUnexplode: .init( + rootKey: "color", + primitive: .assert("", equals: .init(key: .empty, value: "")), + array: .assert("", equals: []), + dictionary: .assert("", equals: []) + ), + formDataExplode: .init( + rootKey: "color", + primitive: .assert("prefix=bar&suffix=baz", equals: nil), + array: .assert("prefix=bar&suffix=baz", equals: []), + dictionary: .assert("", equals: []) + ), + formDataUnexplode: .init( + rootKey: "color", + primitive: .assert("prefix=bar&suffix=baz", equals: nil), + array: .assert("prefix=bar&suffix=baz", equals: []), + dictionary: .assert("prefix=bar&suffix=baz", equals: []) + ), + deepObjectExplode: .init( + rootKey: "color", + primitive: .init( + string: "", + result: .failure({ error in + guard case .invalidConfiguration = error else { + XCTFail("Unexpected error: \(error)") + return + } + }) + ), + array: .init( + string: "", + result: .failure({ error in + guard case .invalidConfiguration = error else { + XCTFail("Unexpected error: \(error)") + return + } + }) + ), + dictionary: .assert("prefix%5Bfoo%5D=1&suffix%5Bbaz%5D=2", equals: []) + ) + ) + + // Empty value (distinct from missing/nil, but some cases overlap) + try testCase( + formExplode: .init( + rootKey: "color", + primitive: .assert("prefix=bar&color=&suffix=baz", equals: .init(key: "color", value: "")), + array: .assert("prefix=bar&color=&suffix=baz", equals: [.init(key: "color", value: "")]), + dictionary: .assert( + "R=&G=200&G=150", + equals: [.init(key: "R", value: ""), .init(key: "G", value: "200"), .init(key: "G", value: "150")] + ) + ), + formUnexplode: .init( + rootKey: "color", + primitive: .assert("prefix=bar&color=&suffix=baz", equals: .init(key: "color", value: "")), + array: .assert("prefix=bar&color=&suffix=baz", equals: [.init(key: "color", value: "")]), + dictionary: .assert( + "prefix=bar&color=R,,G,200,G,150&suffix=baz", + equals: [ + .init(key: "color/R", value: ""), .init(key: "color/G", value: "200"), + .init(key: "color/G", value: "150"), + ] + ) + ), + simpleExplode: .init( + rootKey: "color", + primitive: .assert("", equals: .init(key: .empty, value: "")), + array: .assert( + ",black,brown", + equals: [ + .init(key: .empty, value: ""), .init(key: .empty, value: "black"), + .init(key: .empty, value: "brown"), + ] + ), + dictionary: .assert( + "R=,G=200,G=150", + equals: [.init(key: "R", value: ""), .init(key: "G", value: "200"), .init(key: "G", value: "150")] + ) + ), + simpleUnexplode: .init( + rootKey: "color", + primitive: .assert("", equals: .init(key: .empty, value: "")), + array: .assert( + ",black,brown", + equals: [ + .init(key: .empty, value: ""), .init(key: .empty, value: "black"), + .init(key: .empty, value: "brown"), + ] + ), + dictionary: .assert( + "R,,G,200,G,150", + equals: [.init(key: "R", value: ""), .init(key: "G", value: "200"), .init(key: "G", value: "150")] + ) + ), + formDataExplode: .init( + rootKey: "color", + primitive: .assert("prefix=bar&color=&suffix=baz", equals: .init(key: "color", value: "")), + array: .assert( + "prefix=bar&color=&color=black&color=brown&suffix=baz", + equals: [ + .init(key: "color", value: ""), .init(key: "color", value: "black"), + .init(key: "color", value: "brown"), + ] + ), + dictionary: .assert( + "R=&G=200&G=150", + equals: [.init(key: "R", value: ""), .init(key: "G", value: "200"), .init(key: "G", value: "150")] + ) + ), + formDataUnexplode: .init( + rootKey: "color", + primitive: .assert("prefix=bar&color=&suffix=baz", equals: .init(key: "color", value: "")), + array: .assert( + "prefix=bar&color=,black,brown&suffix=baz", + equals: [ + .init(key: "color", value: ""), .init(key: "color", value: "black"), + .init(key: "color", value: "brown"), + ] + ), + dictionary: .assert( + "prefix=bar&color=R,,G,200,G,150&suffix=baz", + equals: [ + .init(key: "color/R", value: ""), .init(key: "color/G", value: "200"), + .init(key: "color/G", value: "150"), + ] + ) + ), + deepObjectExplode: .init( + rootKey: "color", + primitive: .init( + string: "", + result: .failure({ error in + guard case .invalidConfiguration = error else { + XCTFail("Unexpected error: \(error)") + return + } + }) + ), + array: .init( + string: "", + result: .failure({ error in + guard case .invalidConfiguration = error else { + XCTFail("Unexpected error: \(error)") + return + } + }) + ), + dictionary: .assert( + "prefix%5Bfoo%5D=1&color%5BR%5D=&color%5BG%5D=200&color%5BG%5D=150&suffix%5Bbaz%5D=2", + equals: [ + .init(key: "color/R", value: ""), .init(key: "color/G", value: "200"), + .init(key: "color/G", value: "150"), + ] + ) + ) + ) } -} -extension Test_URIParser { struct Case { struct Variant { var name: String @@ -158,33 +516,33 @@ extension Test_URIParser { static let formDataUnexplode: Self = .init(name: "formDataUnexplode", config: .formDataUnexplode) static let deepObjectExplode: Self = .init(name: "deepObjectExplode", config: .deepObjectExplode) } - struct Variants { - - struct Input: ExpressibleByStringLiteral { - var string: String - var valueOverride: URIParsedNode? - var expectedError: ParsingError? - - init(string: String, valueOverride: URIParsedNode? = nil, expectedError: ParsingError? = nil) { - self.string = string - self.valueOverride = valueOverride - self.expectedError = expectedError - } - - static func custom(_ string: String, value: URIParsedNode) -> Self { - .init(string: string, valueOverride: value, expectedError: nil) - } - static func custom(_ string: String, expectedError: ParsingError) -> Self { - .init(string: string, valueOverride: nil, expectedError: expectedError) - } + struct RootInput { + var string: String + enum ExpectedResult { + case success(RootType) + case failure((ParsingError) -> Void) + } + var result: ExpectedResult - init(stringLiteral value: String) { - self.string = value - self.valueOverride = nil - self.expectedError = nil - } + init(string: String, result: ExpectedResult) { + self.string = string + self.result = result } + static func assert(_ string: String, equals value: RootType) -> Self { + .init(string: string, result: .success(value)) + } + static func assert(_ string: String, validateError: @escaping (ParsingError) -> Void) -> Self { + .init(string: string, result: .failure(validateError)) + } + } + struct Input { + var rootKey: URIParsedKeyComponent + var primitive: RootInput + var array: RootInput + var dictionary: RootInput + } + struct Variants { var formExplode: Input var formUnexplode: Input var simpleExplode: Input @@ -194,11 +552,92 @@ extension Test_URIParser { var deepObjectExplode: Input } var variants: Variants - var value: URIParsedNode var file: StaticString = #file var line: UInt = #line } - func makeCase(_ variants: Case.Variants, value: URIParsedNode, file: StaticString = #file, line: UInt = #line) - -> Case - { .init(variants: variants, value: value, file: file, line: line) } + + func testCase(_ variants: Case.Variants, file: StaticString = #file, line: UInt = #line) throws { + let caseValue = Case(variants: variants, file: file, line: line) + func testVariant(_ variant: Case.Variant, _ input: Case.Input) throws { + func testRoot( + rootName: String, + _ root: Case.RootInput, + parse: (URIParser) throws -> RootType + ) throws { + let parser = URIParser(configuration: variant.config, data: root.string[...]) + switch root.result { + case .success(let expectedValue): + let parsedValue = try parse(parser) + XCTAssertEqual( + parsedValue, + expectedValue, + "Failed for config: \(variant.name), root: \(rootName)", + file: caseValue.file, + line: caseValue.line + ) + case .failure(let validateError): + do { + _ = try parse(parser) + XCTFail("Should have thrown an error", file: caseValue.file, line: caseValue.line) + } catch { + guard let parsingError = error as? ParsingError else { + XCTAssert( + false, + "Unexpected error thrown: \(error)", + file: caseValue.file, + line: caseValue.line + ) + return + } + validateError(parsingError) + } + } + } + try testRoot( + rootName: "primitive", + input.primitive, + parse: { try $0.parseRootAsPrimitive(rootKey: input.rootKey) } + ) + try testRoot(rootName: "array", input.array, parse: { try $0.parseRootAsArray(rootKey: input.rootKey) }) + try testRoot( + rootName: "dictionary", + input.dictionary, + parse: { try $0.parseRootAsDictionary(rootKey: input.rootKey) } + ) + } + let variants = caseValue.variants + try testVariant(.formExplode, variants.formExplode) + try testVariant(.formUnexplode, variants.formUnexplode) + try testVariant(.simpleExplode, variants.simpleExplode) + try testVariant(.simpleUnexplode, variants.simpleUnexplode) + try testVariant(.formDataExplode, variants.formDataExplode) + try testVariant(.formDataUnexplode, variants.formDataUnexplode) + try testVariant(.deepObjectExplode, variants.deepObjectExplode) + } + + func testCase( + formExplode: Case.Input, + formUnexplode: Case.Input, + simpleExplode: Case.Input, + simpleUnexplode: Case.Input, + formDataExplode: Case.Input, + formDataUnexplode: Case.Input, + deepObjectExplode: Case.Input, + file: StaticString = #file, + line: UInt = #line + ) throws { + try testCase( + .init( + formExplode: formExplode, + formUnexplode: formUnexplode, + simpleExplode: simpleExplode, + simpleUnexplode: simpleUnexplode, + formDataExplode: formDataExplode, + formDataUnexplode: formDataUnexplode, + deepObjectExplode: deepObjectExplode + ), + file: file, + line: line + ) + } } diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift b/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift index f198b6eb..a42c9913 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift @@ -16,10 +16,6 @@ import XCTest final class Test_URISerializer: Test_Runtime { - let testedVariants: [URICoderConfiguration] = [ - .formExplode, .formUnexplode, .simpleExplode, .simpleUnexplode, .formDataExplode, .formDataUnexplode, - ] - func testSerializing() throws { let cases: [Case] = [ makeCase( diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift index 3f351768..ac1fc00f 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift @@ -97,7 +97,7 @@ final class Test_URICodingRoundtrip: Test_Runtime { simpleUnexplode: "", formDataExplode: "root=", formDataUnexplode: "root=", - deepObjectExplode: .custom("root=", expectedError: .deepObjectsWithPrimitiveValuesNotSupported) + deepObjectExplode: .custom("", expectedError: .deepObjectsWithPrimitiveValuesNotSupported) ) ) @@ -112,10 +112,7 @@ final class Test_URICodingRoundtrip: Test_Runtime { simpleUnexplode: "Hello%20World%21", formDataExplode: "root=Hello+World%21", formDataUnexplode: "root=Hello+World%21", - deepObjectExplode: .custom( - "root=Hello%20World%21", - expectedError: .deepObjectsWithPrimitiveValuesNotSupported - ) + deepObjectExplode: .custom("", expectedError: .deepObjectsWithPrimitiveValuesNotSupported) ) ) @@ -130,7 +127,7 @@ final class Test_URICodingRoundtrip: Test_Runtime { simpleUnexplode: "red", formDataExplode: "root=red", formDataUnexplode: "root=red", - deepObjectExplode: .custom("root=red", expectedError: .deepObjectsWithPrimitiveValuesNotSupported) + deepObjectExplode: .custom("", expectedError: .deepObjectsWithPrimitiveValuesNotSupported) ) ) @@ -145,7 +142,7 @@ final class Test_URICodingRoundtrip: Test_Runtime { simpleUnexplode: "1234", formDataExplode: "root=1234", formDataUnexplode: "root=1234", - deepObjectExplode: .custom("root=1234", expectedError: .deepObjectsWithPrimitiveValuesNotSupported) + deepObjectExplode: .custom("", expectedError: .deepObjectsWithPrimitiveValuesNotSupported) ) ) @@ -160,7 +157,7 @@ final class Test_URICodingRoundtrip: Test_Runtime { simpleUnexplode: "12.34", formDataExplode: "root=12.34", formDataUnexplode: "root=12.34", - deepObjectExplode: .custom("root=12.34", expectedError: .deepObjectsWithPrimitiveValuesNotSupported) + deepObjectExplode: .custom("", expectedError: .deepObjectsWithPrimitiveValuesNotSupported) ) ) @@ -175,7 +172,7 @@ final class Test_URICodingRoundtrip: Test_Runtime { simpleUnexplode: "true", formDataExplode: "root=true", formDataUnexplode: "root=true", - deepObjectExplode: .custom("root=true", expectedError: .deepObjectsWithPrimitiveValuesNotSupported) + deepObjectExplode: .custom("", expectedError: .deepObjectsWithPrimitiveValuesNotSupported) ) ) @@ -190,10 +187,7 @@ final class Test_URICodingRoundtrip: Test_Runtime { simpleUnexplode: "2023-08-25T07%3A34%3A59Z", formDataExplode: "root=2023-08-25T07%3A34%3A59Z", formDataUnexplode: "root=2023-08-25T07%3A34%3A59Z", - deepObjectExplode: .custom( - "root=2023-08-25T07%3A34%3A59Z", - expectedError: .deepObjectsWithPrimitiveValuesNotSupported - ) + deepObjectExplode: .custom("", expectedError: .deepObjectsWithPrimitiveValuesNotSupported) ) ) @@ -208,7 +202,7 @@ final class Test_URICodingRoundtrip: Test_Runtime { simpleUnexplode: "a,b,c", formDataExplode: "list=a&list=b&list=c", formDataUnexplode: "list=a,b,c", - deepObjectExplode: .custom("list=a&list=b&list=c", expectedError: .deepObjectsArrayNotSupported) + deepObjectExplode: .custom("", expectedError: .deepObjectsArrayNotSupported) ) ) @@ -223,10 +217,7 @@ final class Test_URICodingRoundtrip: Test_Runtime { simpleUnexplode: "2023-08-25T07%3A34%3A59Z,2023-08-25T07%3A35%3A01Z", formDataExplode: "list=2023-08-25T07%3A34%3A59Z&list=2023-08-25T07%3A35%3A01Z", formDataUnexplode: "list=2023-08-25T07%3A34%3A59Z,2023-08-25T07%3A35%3A01Z", - deepObjectExplode: .custom( - "list=2023-08-25T07%3A34%3A59Z&list=2023-08-25T07%3A35%3A01Z", - expectedError: .deepObjectsArrayNotSupported - ) + deepObjectExplode: .custom("", expectedError: .deepObjectsArrayNotSupported) ) ) @@ -237,8 +228,8 @@ final class Test_URICodingRoundtrip: Test_Runtime { .init( formExplode: "", formUnexplode: "", - simpleExplode: .custom("", value: [""]), - simpleUnexplode: .custom("", value: [""]), + simpleExplode: "", + simpleUnexplode: "", formDataExplode: "", formDataUnexplode: "", deepObjectExplode: .custom("", expectedError: .deepObjectsArrayNotSupported) @@ -256,10 +247,7 @@ final class Test_URICodingRoundtrip: Test_Runtime { simpleUnexplode: "red,green,blue", formDataExplode: "list=red&list=green&list=blue", formDataUnexplode: "list=red,green,blue", - deepObjectExplode: .custom( - "list=red&list=green&list=blue", - expectedError: .deepObjectsArrayNotSupported - ) + deepObjectExplode: .custom("", expectedError: .deepObjectsArrayNotSupported) ) ) @@ -291,10 +279,7 @@ final class Test_URICodingRoundtrip: Test_Runtime { simpleUnexplode: "2023-01-18T10%3A04%3A11Z", formDataExplode: "root=2023-01-18T10%3A04%3A11Z", formDataUnexplode: "root=2023-01-18T10%3A04%3A11Z", - deepObjectExplode: .custom( - "root=2023-01-18T10%3A04%3A11Z", - expectedError: .deepObjectsWithPrimitiveValuesNotSupported - ) + deepObjectExplode: .custom("", expectedError: .deepObjectsWithPrimitiveValuesNotSupported) ) ) try _test( @@ -307,7 +292,7 @@ final class Test_URICodingRoundtrip: Test_Runtime { simpleUnexplode: "green", formDataExplode: "root=green", formDataUnexplode: "root=green", - deepObjectExplode: .custom("root=green", expectedError: .deepObjectsWithPrimitiveValuesNotSupported) + deepObjectExplode: .custom("", expectedError: .deepObjectsWithPrimitiveValuesNotSupported) ) ) try _test( @@ -362,8 +347,8 @@ final class Test_URICodingRoundtrip: Test_Runtime { .init( formExplode: "", formUnexplode: "", - simpleExplode: .custom("", value: ["": ""]), - simpleUnexplode: .custom("", value: ["": ""]), + simpleExplode: "", + simpleUnexplode: "", formDataExplode: "", formDataUnexplode: "", deepObjectExplode: "" diff --git a/Tests/OpenAPIRuntimeTests/URICoder/URICoderTestUtils.swift b/Tests/OpenAPIRuntimeTests/URICoder/URICoderTestUtils.swift index 65235d82..38462a6a 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/URICoderTestUtils.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/URICoderTestUtils.swift @@ -59,6 +59,7 @@ extension URICoderConfiguration { spaceEscapingCharacter: .plus, dateTranscoder: defaultDateTranscoder ) + static let deepObjectExplode: Self = .init( style: .deepObject, explode: true, @@ -66,3 +67,11 @@ extension URICoderConfiguration { dateTranscoder: defaultDateTranscoder ) } + +extension URIParsedKey: ExpressibleByStringLiteral { + + /// Creates an instance initialized to the given string value. + /// + /// - Parameter value: The value of the new instance. + public init(stringLiteral value: StringLiteralType) { self.init(value.split(separator: "/").map { $0[...] }) } +} From 5b65724fa45b2e0b9ee533caea251010d77918a5 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 13 Nov 2024 12:13:53 +0100 Subject: [PATCH 2/9] Fix build errors --- .../URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift | 4 ++++ Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift index 061efaf5..e7c29017 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift @@ -21,6 +21,10 @@ struct URIUnkeyedDecodingContainer { /// The index of the next item to be decoded. private var index: URIParsedValueArray.Index = 0 + + /// Creates a new unkeyed container ready to decode the first key. + /// - Parameter decoder: The underlying decoder. + init(decoder: URIValueFromNodeDecoder) { self.decoder = decoder } } extension URIUnkeyedDecodingContainer { diff --git a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift index 6e9fbd86..4c794824 100644 --- a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift +++ b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift @@ -27,7 +27,7 @@ struct URIParser: Sendable { /// - Parameters: /// - configuration: The configuration instructing the parser how to interpret the raw string. /// - data: The string to parse. - init(configuration: URICoderConfiguration, data: Substring, ) { + init(configuration: URICoderConfiguration, data: Substring) { self.configuration = configuration self.data = data } From 49eef872594d5ac2f5938a8272b7e9bed2ce6624 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 13 Nov 2024 14:31:18 +0100 Subject: [PATCH 3/9] More polish, added doc comments to the new code --- .../Conversion/ParameterStyles.swift | 5 +- .../Common/URICoderConfiguration.swift | 4 +- .../URICoder/Common/URIDecodedTypes.swift | 22 +++ .../URICoder/Common/URIEncodedNode.swift | 2 +- .../URICoder/Common/URIParsedNode.swift | 58 ------- .../URICoder/Common/URIParsedTypes.swift | 91 ++++++++++ .../URICoder/Decoding/URIDecoder.swift | 42 ++--- .../Decoding/URIValueFromNodeDecoder.swift | 163 ++++++++++++------ .../URICoder/Encoding/URIEncoder.swift | 9 +- .../URIValueToNodeEncoder+Unkeyed.swift | 3 + .../URICoder/Parsing/URIParser.swift | 22 ++- 11 files changed, 270 insertions(+), 151 deletions(-) create mode 100644 Sources/OpenAPIRuntime/URICoder/Common/URIDecodedTypes.swift delete mode 100644 Sources/OpenAPIRuntime/URICoder/Common/URIParsedNode.swift create mode 100644 Sources/OpenAPIRuntime/URICoder/Common/URIParsedTypes.swift diff --git a/Sources/OpenAPIRuntime/Conversion/ParameterStyles.swift b/Sources/OpenAPIRuntime/Conversion/ParameterStyles.swift index 07aa6092..31dda63c 100644 --- a/Sources/OpenAPIRuntime/Conversion/ParameterStyles.swift +++ b/Sources/OpenAPIRuntime/Conversion/ParameterStyles.swift @@ -14,7 +14,7 @@ /// The serialization style used by a parameter. /// -/// Details: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#fixed-fields-10 +/// Details: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.4.md#fixed-fields-10 @_spi(Generated) public enum ParameterStyle: Sendable { /// The form style. @@ -26,9 +26,10 @@ /// /// Details: https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.2 case simple + /// The deepObject style. /// - /// Details: https://spec.openapis.org/oas/v3.1.0.html#style-values + /// Details: https://spec.openapis.org/oas/v3.0.4.html#style-values case deepObject } diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift b/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift index 51bcd2bc..3f7b380e 100644 --- a/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift +++ b/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift @@ -17,7 +17,7 @@ import Foundation /// A bag of configuration values used by the URI encoder and decoder. struct URICoderConfiguration { - /// A variable expansion style as described by RFC 6570 and OpenAPI 3.0.3. + /// A variable expansion style as described by RFC 6570 and OpenAPI 3.0.4. enum Style { /// A style for simple string variable expansion. @@ -50,7 +50,7 @@ struct URICoderConfiguration { var style: Style /// A Boolean value indicating whether the key should be repeated with - /// each value, as described by RFC 6570 and OpenAPI 3.0.3. + /// each value, as described by RFC 6570 and OpenAPI 3.0.4. var explode: Bool /// The character used to escape the space character. diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URIDecodedTypes.swift b/Sources/OpenAPIRuntime/URICoder/Common/URIDecodedTypes.swift new file mode 100644 index 00000000..56b3f23c --- /dev/null +++ b/Sources/OpenAPIRuntime/URICoder/Common/URIDecodedTypes.swift @@ -0,0 +1,22 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// A primitive value produced by `URIValueFromNodeDecoder`. +typealias URIDecodedPrimitive = URIParsedValue + +/// An array value produced by `URIValueFromNodeDecoder`. +typealias URIDecodedArray = URIParsedValueArray + +/// A dictionary value produced by `URIValueFromNodeDecoder`. +typealias URIDecodedDictionary = [Substring: URIParsedValueArray] diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift b/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift index 725cdabd..d04d0f68 100644 --- a/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift +++ b/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift @@ -128,7 +128,7 @@ extension URIEncodedNode { } } - /// Marks the node as an array, starting of empty. + /// Marks the node as an array, starting as empty. /// - Throws: If the node is already set to be anything else but an array. mutating func markAsArray() throws { switch self { diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URIParsedNode.swift b/Sources/OpenAPIRuntime/URICoder/Common/URIParsedNode.swift deleted file mode 100644 index c93a7c47..00000000 --- a/Sources/OpenAPIRuntime/URICoder/Common/URIParsedNode.swift +++ /dev/null @@ -1,58 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftOpenAPIGenerator open source project -// -// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Foundation - -/// The type used for keys by `URIParser`. -typealias URIParsedKeyComponent = String.SubSequence - -struct URIParsedKey: Hashable, CustomStringConvertible { - - private(set) var components: [URIParsedKeyComponent] - - init(_ components: [URIParsedKeyComponent]) { self.components = components } - - static var empty: Self { .init([]) } - - func appending(_ component: URIParsedKeyComponent) -> Self { - var copy = self - copy.components.append(component) - return copy - } - - var description: String { - if components.isEmpty { return "" } - return components.joined(separator: "/") - } -} - -/// The type used for values by `URIParser`. -typealias URIParsedValue = String.SubSequence - -/// The type used for an array of values by `URIParser`. -typealias URIParsedValueArray = [URIParsedValue] - -/// A key-value pair. -struct URIParsedPair: Equatable { - var key: URIParsedKey - var value: URIParsedValue -} - -typealias URIParsedPairArray = [URIParsedPair] - -typealias URIDecodedPrimitive = URIParsedValue - -typealias URIDecodedDictionary = [Substring: URIParsedValueArray] - -typealias URIDecodedArray = URIParsedValueArray diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URIParsedTypes.swift b/Sources/OpenAPIRuntime/URICoder/Common/URIParsedTypes.swift new file mode 100644 index 00000000..8229f530 --- /dev/null +++ b/Sources/OpenAPIRuntime/URICoder/Common/URIParsedTypes.swift @@ -0,0 +1,91 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +/// A component of a `URIParsedKey`. +typealias URIParsedKeyComponent = String.SubSequence + +/// A parsed key for a parsed value. +/// +/// For example, `foo=bar` in a `form` string would parse the key as `foo` (single component). +/// In an unexploded `form` string `root=foo,bar`, the key would be `root/foo` (two components). +/// In a `simple` string `bar`, the key would be empty (0 components). +struct URIParsedKey: Hashable { + + /// The individual string components. + let components: [URIParsedKeyComponent] + + /// Creates a new parsed key. + /// - Parameter components: The key components. + init(_ components: [URIParsedKeyComponent]) { self.components = components } + + /// A new empty key. + static var empty: Self { .init([]) } +} + +/// A primitive value produced by `URIParser`. +typealias URIParsedValue = String.SubSequence + +/// An array of primitive values produced by `URIParser`. +typealias URIParsedValueArray = [URIParsedValue] + +/// A key-value produced by `URIParser`. +struct URIParsedPair: Equatable { + + /// The key of the pair. + /// + /// In `foo=bar`, `foo` is the key. + var key: URIParsedKey + + /// The value of the pair. + /// + /// In `foo=bar`, `bar` is the value. + var value: URIParsedValue +} + +/// An array of key-value pairs produced by `URIParser`. +typealias URIParsedPairArray = [URIParsedPair] + +// MARK: - Extensions + +extension URIParsedKey: CustomStringConvertible { + /// A textual representation of this instance. + /// + /// Calling this property directly is discouraged. Instead, convert an + /// instance of any type to a string by using the `String(describing:)` + /// initializer. This initializer works with any type, and uses the custom + /// `description` property for types that conform to + /// `CustomStringConvertible`: + /// + /// struct Point: CustomStringConvertible { + /// let x: Int, y: Int + /// + /// var description: String { + /// return "(\(x), \(y))" + /// } + /// } + /// + /// let p = Point(x: 21, y: 30) + /// let s = String(describing: p) + /// print(s) + /// // Prints "(21, 30)" + /// + /// The conversion of `p` to a string in the assignment to `s` uses the + /// `Point` type's `description` property. + var description: String { + if components.isEmpty { return "" } + return components.joined(separator: "/") + } +} diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift index d9658d64..8862a098 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift @@ -15,7 +15,7 @@ import Foundation /// A type that decodes a `Decodable` value from an URI-encoded string -/// using the rules from RFC 6570, RFC 1866, and OpenAPI 3.0.3, depending on +/// using the rules from RFC 6570, RFC 1866, and OpenAPI 3.0.4, depending on /// the configuration. /// /// [RFC 6570 - Form-style query expansion.](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.8) @@ -45,6 +45,13 @@ import Foundation /// | `{list\*}` | `red,green,blue` | /// | `{keys}` | `semi,%3B,dot,.,comma,%2C` | /// | `{keys\*}` | `semi=%3B,dot=.,comma=%2C` | +/// +/// [OpenAPI 3.0.4 - Deep object expansion.](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.4.md#style-examples) +/// +/// | Example Template | Expansion | +/// | ---------------- | ----------------------------------------------------------| +/// | `{?keys\*}` | `?keys%5Bsemi%5D=%3B&keys%5Bdot%5D=.&keys%5Bcomma%5D=%2C` | +/// struct URIDecoder: Sendable { /// The configuration instructing the decoder how to interpret the raw @@ -60,10 +67,6 @@ extension URIDecoder { /// Attempt to decode an object from an URI string. /// - /// Under the hood, `URIDecoder` first parses the string into a - /// `URIParsedNode` using `URIParser`, and then uses - /// `URIValueFromNodeDecoder` to decode the `Decodable` value. - /// /// - Parameters: /// - type: The type to decode. /// - key: The key of the decoded value. Only used with certain styles @@ -71,16 +74,17 @@ extension URIDecoder { /// - data: The URI-encoded string. /// - Returns: The decoded value. /// - Throws: An error if decoding fails, for example, due to incompatible data or key. - func decode(_ type: T.Type = T.self, forKey key: String = "", from data: Substring) throws -> T { - try withDecoder(from: data, forKey: key) { decoder in try decoder.decodeRoot(type) } + func decode( + _ type: T.Type = T.self, + forKey key: String = "", + from data: Substring + ) throws -> T { + let decoder = URIValueFromNodeDecoder(data: data, rootKey: key[...], configuration: configuration) + return try decoder.decodeRoot(type) } /// Attempt to decode an object from an URI string, if present. /// - /// Under the hood, `URIDecoder` first parses the string into a - /// `URIParsedNode` using `URIParser`, and then uses - /// `URIValueFromNodeDecoder` to decode the `Decodable` value. - /// /// - Parameters: /// - type: The type to decode. /// - key: The key of the decoded value. Only used with certain styles @@ -90,22 +94,8 @@ extension URIDecoder { /// - Throws: An error if decoding fails, for example, due to incompatible data or key. func decodeIfPresent(_ type: T.Type = T.self, forKey key: String = "", from data: Substring) throws -> T? - { try withDecoder(from: data, forKey: key) { decoder in try decoder.decodeRootIfPresent(type) } } - - /// Make multiple decode calls on the parsed URI. - /// - /// Use to avoid repeatedly reparsing the raw string. - /// - Parameters: - /// - data: The URI-encoded string. - /// - key: The root key to decode. - /// - calls: The closure that contains 0 or more calls to - /// the `decode` method on `URIDecoderImpl`. - /// - Returns: The result of the closure invocation. - /// - Throws: An error if parsing or decoding fails. - func withDecoder(from data: Substring, forKey key: String, calls: (URIValueFromNodeDecoder) throws -> R) throws - -> R { let decoder = URIValueFromNodeDecoder(data: data, rootKey: key[...], configuration: configuration) - return try calls(decoder) + return try decoder.decodeRootIfPresent(type) } } diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift index 8d7ab6c2..c3d39a5a 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift @@ -14,38 +14,58 @@ import Foundation -/// A type that allows decoding `Decodable` values from `URIParser`'s output. +/// A type that allows decoding `Decodable` values from a URI-encoded string. final class URIValueFromNodeDecoder { - private let data: Substring + /// The key of the root object. + let rootKey: URIParsedKeyComponent - private let configuration: URICoderConfiguration + /// The date transcoder used for decoding the `Date` type. let dateTranscoder: any DateTranscoder + + /// The coder configuration. + private let configuration: URICoderConfiguration + + /// The URIParser used to parse the provided URI-encoded string into + /// an intermediate representation. private let parser: URIParser - struct ParsedState { + + /// The cached parsing state of the decoder. + private struct ParsingCache { + + /// The cached result of parsing the string as a primitive value. var primitive: Result? + + /// The cached result of parsing the string as an array. var array: Result? + + /// The cached result of parsing the string as a dictionary. var dictionary: Result? } - private var state: ParsedState - /// The key of the root value in the node. - let rootKey: URIParsedKeyComponent - /// The stack of nested values within the root node. - private var codingStack: [CodingStackEntry] + /// A cache holding the parsed intermediate representation. + private var cache: ParsingCache + + /// The stack of nested keys within the root node. + /// + /// Represents the currently parsed container. + private var codingStack: [URICoderCodingKey] /// Creates a new decoder. /// - Parameters: /// - data: The data to parse. - /// - rootKey: The key of the root value in the node. + /// - rootKey: The key of the root object. /// - configuration: The configuration of the decoder. - init(data: Substring, rootKey: URIParsedKeyComponent, configuration: URICoderConfiguration) { - self.configuration = configuration + init( + data: Substring, + rootKey: URIParsedKeyComponent, + configuration: URICoderConfiguration + ) { + self.rootKey = rootKey self.dateTranscoder = configuration.dateTranscoder - self.data = data + self.configuration = configuration self.parser = .init(configuration: configuration, data: data) - self.state = .init() - self.rootKey = rootKey + self.cache = .init() self.codingStack = [] } @@ -68,9 +88,9 @@ final class URIValueFromNodeDecoder { return value } - /// Decodes the provided type from the root node. + /// Decodes the provided type from the root node, if it's present. /// - Parameter type: The type to decode from the decoder. - /// - Returns: The decoded value. + /// - Returns: The decoded value, or nil if not found. /// - Throws: When a decoding error occurs. func decodeRootIfPresent(_ type: T.Type = T.self) throws -> T? { switch configuration.style { @@ -101,20 +121,11 @@ extension URIValueFromNodeDecoder { case reachedEndOfUnkeyedContainer } - /// An entry in the coding stack for `URIValueFromNodeDecoder`. - /// - /// This is used to keep track of where we are in the decode. - private struct CodingStackEntry { - - /// The key at which the entry was found. - var key: URICoderCodingKey - } - /// Pushes a new container on top of the current stack, nesting into the /// value at the provided key. /// - Parameter codingKey: The coding key for the value that is then put /// at the top of the stack. - func push(_ codingKey: URICoderCodingKey) { codingStack.append(CodingStackEntry(key: codingKey)) } + func push(_ codingKey: URICoderCodingKey) { codingStack.append(codingKey) } /// Pops the top container from the stack and restores the previously top /// container to be the current top container. @@ -130,9 +141,12 @@ extension URIValueFromNodeDecoder { // MARK: - withParsed methods + /// Use the root as a primitive value. + /// - Parameter work: The closure in which to use the value. + /// - Returns: Any value returned from the closure. private func withParsedRootAsPrimitive(_ work: (URIDecodedPrimitive?) throws -> R) throws -> R { let value: URIDecodedPrimitive? - if let cached = state.primitive { + if let cached = cache.primitive { value = try cached.get() } else { let result: Result @@ -143,14 +157,38 @@ extension URIValueFromNodeDecoder { result = .failure(error) throw error } - state.primitive = result + cache.primitive = result + } + return try work(value) + } + + /// Use the root as an array. + /// - Parameter work: The closure in which to use the value. + /// - Returns: Any value returned from the closure. + private func withParsedRootAsArray(_ work: (URIDecodedArray) throws -> R) throws -> R { + let value: URIDecodedArray + if let cached = cache.array { + value = try cached.get() + } else { + let result: Result + do { + value = try parser.parseRootAsArray(rootKey: rootKey).map(\.value) + result = .success(value) + } catch { + result = .failure(error) + throw error + } + cache.array = result } return try work(value) } + /// Use the root as a dictionary. + /// - Parameter work: The closure in which to use the value. + /// - Returns: Any value returned from the closure. private func withParsedRootAsDictionary(_ work: (URIDecodedDictionary) throws -> R) throws -> R { let value: URIDecodedDictionary - if let cached = state.dictionary { + if let cached = cache.dictionary { value = try cached.get() } else { let result: Result @@ -184,38 +222,30 @@ extension URIValueFromNodeDecoder { result = .failure(error) throw error } - state.dictionary = result - } - return try work(value) - } - - private func withParsedRootAsArray(_ work: (URIDecodedArray) throws -> R) throws -> R { - let value: URIDecodedArray - if let cached = state.array { - value = try cached.get() - } else { - let result: Result - do { - value = try parser.parseRootAsArray(rootKey: rootKey).map(\.value) - result = .success(value) - } catch { - result = .failure(error) - throw error - } - state.array = result + cache.dictionary = result } return try work(value) } // MARK: - decoding utilities + + /// Returns a dictionary value. + /// - Parameters: + /// - key: The key for which to return the value. + /// - dictionary: The dictionary in which to find the value. + /// - Returns: The value in the dictionary, or nil if not found. func primitiveValue(forKey key: String, in dictionary: URIDecodedDictionary) throws -> URIParsedValue? { let values = dictionary[key[...], default: []] if values.isEmpty { return nil } if values.count > 1 { try throwMismatch("Dictionary value contains multiple values.") } return values[0] } + // MARK: - withCurrent methods + /// Use the current top of the stack as a primitive value. + /// - Parameter work: The closure in which to use the value. + /// - Returns: Any value returned from the closure. private func withCurrentPrimitiveElement(_ work: (URIDecodedPrimitive?) throws -> R) throws -> R { if !codingStack.isEmpty { // Nesting is involved. @@ -224,7 +254,7 @@ extension URIValueFromNodeDecoder { // - primitive in a top level dictionary // - primitive in a nested array inside a top level dictionary if codingStack.count == 1 { - let key = codingStack[0].key + let key = codingStack[0] if let intKey = key.intValue { // Top level array. return try withParsedRootAsArray { array in try work(array[intKey]) } @@ -236,8 +266,8 @@ extension URIValueFromNodeDecoder { } } else if codingStack.count == 2 { // Nested array within a top level dictionary. - let dictionaryKey = codingStack[0].key.stringValue[...] - guard let nestedArrayKey = codingStack[1].key.intValue else { + let dictionaryKey = codingStack[0].stringValue[...] + guard let nestedArrayKey = codingStack[1].intValue else { try throwMismatch("Nested coding key is not an integer, hinting at unsupported nesting.") } return try withParsedRootAsDictionary { dictionary in @@ -252,8 +282,11 @@ extension URIValueFromNodeDecoder { } } + /// Use the current top of the stack as an array. + /// - Parameter work: The closure in which to use the value. + /// - Returns: Any value returned from the closure. private func withCurrentArrayElements(_ work: (URIDecodedArray) throws -> R) throws -> R { - if let nestedArrayParentKey = codingStack.first?.key { + if let nestedArrayParentKey = codingStack.first { // Top level is dictionary, first level nesting is array. // Get all the elements that match this rootKey + nested key path. return try withParsedRootAsDictionary { dictionary in @@ -265,6 +298,9 @@ extension URIValueFromNodeDecoder { } } + /// Use the current top of the stack as a dictionary. + /// - Parameter work: The closure in which to use the value. + /// - Returns: Any value returned from the closure. private func withCurrentDictionaryElements(_ work: (URIDecodedDictionary) throws -> R) throws -> R { if !codingStack.isEmpty { try throwMismatch("Nesting a dictionary inside another container is not supported.") @@ -276,19 +312,38 @@ extension URIValueFromNodeDecoder { // MARK: - metadata and data accessors + /// Returns the current top-of-stack as a primitive value. + /// - Returns: The primitive value, or nil if not found. func currentElementAsSingleValue() throws -> URIParsedValue? { try withCurrentPrimitiveElement { $0 } } + /// Returns the count of elements in the current top-of-stack array. + /// - Returns: The number of elements. func countOfCurrentArray() throws -> Int { try withCurrentArrayElements { $0.count } } + /// Returns an element from the current top-of-stack array. + /// - Parameter index: The position in the array to return. + /// - Returns: The primitive value from the array. func nestedElementInCurrentArray(atIndex index: Int) throws -> URIParsedValue { try withCurrentArrayElements { $0[index] } } + + /// Returns an element from the current top-of-stack dictionary. + /// - Parameter key: The key to find a value for. + /// - Returns: The value for the key, or nil if not found. func nestedElementInCurrentDictionary(forKey key: String) throws -> URIParsedValue? { try withCurrentDictionaryElements { dictionary in try primitiveValue(forKey: key, in: dictionary) } } + + /// Returns a Boolean value that represents whether the current top-of-stack dictionary + /// contains a value for the provided key. + /// - Parameter key: The key for which to look for a value. + /// - Returns: `true` if a value was found, `false` otherwise. func containsElementInCurrentDictionary(forKey key: String) throws -> Bool { try withCurrentDictionaryElements { dictionary in dictionary[key[...]] != nil } } + + /// Returns a list of keys found in the current top-of-stack dictionary. + /// - Returns: A list of keys from the dictionary. func elementKeysInCurrentDictionary() throws -> [String] { try withCurrentDictionaryElements { dictionary in dictionary.keys.map(String.init) } } @@ -296,7 +351,7 @@ extension URIValueFromNodeDecoder { extension URIValueFromNodeDecoder: Decoder { - var codingPath: [any CodingKey] { codingStack.map(\.key) } + var codingPath: [any CodingKey] { codingStack } var userInfo: [CodingUserInfoKey: Any] { [:] } diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift index 21028207..06103408 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift @@ -15,7 +15,7 @@ import Foundation /// A type that encodes an `Encodable` value to an URI-encoded string -/// using the rules from RFC 6570, RFC 1866, and OpenAPI 3.0.3, depending on +/// using the rules from RFC 6570, RFC 1866, and OpenAPI 3.0.4, depending on /// the configuration. /// /// [RFC 6570 - Form-style query expansion.](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.8) @@ -45,6 +45,13 @@ import Foundation /// | `{list\*}` | `red,green,blue` | /// | `{keys}` | `semi,%3B,dot,.,comma,%2C` | /// | `{keys\*}` | `semi=%3B,dot=.,comma=%2C` | +/// +/// [OpenAPI 3.0.4 - Deep object expansion.](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.4.md#style-examples) +/// +/// | Example Template | Expansion | +/// | ---------------- | ----------------------------------------------------------| +/// | `{?keys\*}` | `?keys%5Bsemi%5D=%3B&keys%5Bdot%5D=.&keys%5Bcomma%5D=%2C` | +/// struct URIEncoder: Sendable { /// The serializer used to turn `URIEncodedNode` values to a string. diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift index caf84b67..5d892151 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift @@ -19,6 +19,9 @@ struct URIUnkeyedEncodingContainer { /// The associated encoder. let encoder: URIValueToNodeEncoder + + /// Creates a new encoder. + /// - Parameter encoder: The associated encoder. init(encoder: URIValueToNodeEncoder) { self.encoder = encoder try? encoder.currentStackEntry.storage.markAsArray() diff --git a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift index 4c794824..a94be96d 100644 --- a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift +++ b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift @@ -17,15 +17,15 @@ import Foundation /// A type that can parse a primitive, array, and a dictionary from a URI-encoded string. struct URIParser: Sendable { - /// The configuration instructing the parser how to interpret the raw - /// string. + /// The configuration of the parser. private let configuration: URICoderConfiguration - /// The underlying raw string storage. + + /// The string to parse. private let data: Raw /// Creates a new parser. /// - Parameters: - /// - configuration: The configuration instructing the parser how to interpret the raw string. + /// - configuration: The configuration of the parser. /// - data: The string to parse. init(configuration: URICoderConfiguration, data: Substring) { self.configuration = configuration @@ -49,7 +49,10 @@ enum ParsingError: Swift.Error, Hashable { // MARK: - Parser implementations extension URIParser { - + + /// Parses the string as a primitive value. + /// - Parameter rootKey: The key of the root object, used to filter out unrelated values. + /// - Returns: The parsed primitive value, or nil if not found. func parseRootAsPrimitive(rootKey: URIParsedKeyComponent) throws -> URIParsedPair? { var data = data switch (configuration.style, configuration.explode) { @@ -84,6 +87,9 @@ extension URIParser { } } + /// Parses the string as an array. + /// - Parameter rootKey: The key of the root object, used to filter out unrelated values. + /// - Returns: The parsed array. func parseRootAsArray(rootKey: URIParsedKeyComponent) throws -> URIParsedPairArray { var data = data switch (configuration.style, configuration.explode) { @@ -160,6 +166,9 @@ extension URIParser { } } + /// Parses the string as a dictionary. + /// - Parameter rootKey: The key of the root object, used to filter out unrelated values. + /// - Returns: The parsed key/value pairs as an array. func parseRootAsDictionary(rootKey: URIParsedKeyComponent) throws -> URIParsedPairArray { var data = data switch (configuration.style, configuration.explode) { @@ -195,7 +204,6 @@ extension URIParser { case .foundFirst: let unescapedKey = unescapeValue(firstValue) if unescapedKey == rootKey { - let parentKey = URIParsedKey([unescapedKey]) elementScan: while !data.isEmpty { let (innerKeyResult, innerKeyValue) = data.parseUpToEitherCharacterOrEnd( first: arrayElementSeparator, @@ -209,7 +217,7 @@ extension URIParser { ) items.append( .init( - key: parentKey.appending(innerKeyValue), + key: URIParsedKey([unescapedKey, innerKeyValue]), value: unescapeValue(innerValueValue) ) ) From c3d9ec3b972b04ecbc35339dc255989002b207f6 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 13 Nov 2024 14:38:57 +0100 Subject: [PATCH 4/9] Doc fixes --- .../URICoder/Common/URIParsedTypes.swift | 1 - .../URICoder/Decoding/URIDecoder.swift | 6 +----- .../Decoding/URIValueFromNodeDecoder.swift | 19 ++++++++++++++----- .../URICoder/Parsing/URIParser.swift | 4 +++- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URIParsedTypes.swift b/Sources/OpenAPIRuntime/URICoder/Common/URIParsedTypes.swift index 8229f530..9ea19fe8 100644 --- a/Sources/OpenAPIRuntime/URICoder/Common/URIParsedTypes.swift +++ b/Sources/OpenAPIRuntime/URICoder/Common/URIParsedTypes.swift @@ -26,7 +26,6 @@ struct URIParsedKey: Hashable { /// The individual string components. let components: [URIParsedKeyComponent] - /// Creates a new parsed key. /// - Parameter components: The key components. init(_ components: [URIParsedKeyComponent]) { self.components = components } diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift index 8862a098..3be1dce1 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift @@ -74,11 +74,7 @@ extension URIDecoder { /// - data: The URI-encoded string. /// - Returns: The decoded value. /// - Throws: An error if decoding fails, for example, due to incompatible data or key. - func decode( - _ type: T.Type = T.self, - forKey key: String = "", - from data: Substring - ) throws -> T { + func decode(_ type: T.Type = T.self, forKey key: String = "", from data: Substring) throws -> T { let decoder = URIValueFromNodeDecoder(data: data, rootKey: key[...], configuration: configuration) return try decoder.decodeRoot(type) } diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift index c3d39a5a..f61596e2 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift @@ -56,11 +56,7 @@ final class URIValueFromNodeDecoder { /// - data: The data to parse. /// - rootKey: The key of the root object. /// - configuration: The configuration of the decoder. - init( - data: Substring, - rootKey: URIParsedKeyComponent, - configuration: URICoderConfiguration - ) { + init(data: Substring, rootKey: URIParsedKeyComponent, configuration: URICoderConfiguration) { self.rootKey = rootKey self.dateTranscoder = configuration.dateTranscoder self.configuration = configuration @@ -144,6 +140,7 @@ extension URIValueFromNodeDecoder { /// Use the root as a primitive value. /// - Parameter work: The closure in which to use the value. /// - Returns: Any value returned from the closure. + /// - Throws: When parsing the root fails. private func withParsedRootAsPrimitive(_ work: (URIDecodedPrimitive?) throws -> R) throws -> R { let value: URIDecodedPrimitive? if let cached = cache.primitive { @@ -165,6 +162,7 @@ extension URIValueFromNodeDecoder { /// Use the root as an array. /// - Parameter work: The closure in which to use the value. /// - Returns: Any value returned from the closure. + /// - Throws: When parsing the root fails. private func withParsedRootAsArray(_ work: (URIDecodedArray) throws -> R) throws -> R { let value: URIDecodedArray if let cached = cache.array { @@ -186,6 +184,7 @@ extension URIValueFromNodeDecoder { /// Use the root as a dictionary. /// - Parameter work: The closure in which to use the value. /// - Returns: Any value returned from the closure. + /// - Throws: When parsing the root fails. private func withParsedRootAsDictionary(_ work: (URIDecodedDictionary) throws -> R) throws -> R { let value: URIDecodedDictionary if let cached = cache.dictionary { @@ -234,6 +233,7 @@ extension URIValueFromNodeDecoder { /// - key: The key for which to return the value. /// - dictionary: The dictionary in which to find the value. /// - Returns: The value in the dictionary, or nil if not found. + /// - Throws: When multiple values are found for the key. func primitiveValue(forKey key: String, in dictionary: URIDecodedDictionary) throws -> URIParsedValue? { let values = dictionary[key[...], default: []] if values.isEmpty { return nil } @@ -246,6 +246,7 @@ extension URIValueFromNodeDecoder { /// Use the current top of the stack as a primitive value. /// - Parameter work: The closure in which to use the value. /// - Returns: Any value returned from the closure. + /// - Throws: When parsing the root fails. private func withCurrentPrimitiveElement(_ work: (URIDecodedPrimitive?) throws -> R) throws -> R { if !codingStack.isEmpty { // Nesting is involved. @@ -285,6 +286,7 @@ extension URIValueFromNodeDecoder { /// Use the current top of the stack as an array. /// - Parameter work: The closure in which to use the value. /// - Returns: Any value returned from the closure. + /// - Throws: When parsing the root fails. private func withCurrentArrayElements(_ work: (URIDecodedArray) throws -> R) throws -> R { if let nestedArrayParentKey = codingStack.first { // Top level is dictionary, first level nesting is array. @@ -301,6 +303,7 @@ extension URIValueFromNodeDecoder { /// Use the current top of the stack as a dictionary. /// - Parameter work: The closure in which to use the value. /// - Returns: Any value returned from the closure. + /// - Throws: When parsing the root fails or if there is unsupported extra nesting of containers. private func withCurrentDictionaryElements(_ work: (URIDecodedDictionary) throws -> R) throws -> R { if !codingStack.isEmpty { try throwMismatch("Nesting a dictionary inside another container is not supported.") @@ -314,15 +317,18 @@ extension URIValueFromNodeDecoder { /// Returns the current top-of-stack as a primitive value. /// - Returns: The primitive value, or nil if not found. + /// - Throws: When parsing the root fails. func currentElementAsSingleValue() throws -> URIParsedValue? { try withCurrentPrimitiveElement { $0 } } /// Returns the count of elements in the current top-of-stack array. /// - Returns: The number of elements. + /// - Throws: When parsing the root fails. func countOfCurrentArray() throws -> Int { try withCurrentArrayElements { $0.count } } /// Returns an element from the current top-of-stack array. /// - Parameter index: The position in the array to return. /// - Returns: The primitive value from the array. + /// - Throws: When parsing the root fails. func nestedElementInCurrentArray(atIndex index: Int) throws -> URIParsedValue { try withCurrentArrayElements { $0[index] } } @@ -330,6 +336,7 @@ extension URIValueFromNodeDecoder { /// Returns an element from the current top-of-stack dictionary. /// - Parameter key: The key to find a value for. /// - Returns: The value for the key, or nil if not found. + /// - Throws: When parsing the root fails. func nestedElementInCurrentDictionary(forKey key: String) throws -> URIParsedValue? { try withCurrentDictionaryElements { dictionary in try primitiveValue(forKey: key, in: dictionary) } } @@ -338,12 +345,14 @@ extension URIValueFromNodeDecoder { /// contains a value for the provided key. /// - Parameter key: The key for which to look for a value. /// - Returns: `true` if a value was found, `false` otherwise. + /// - Throws: When parsing the root fails. func containsElementInCurrentDictionary(forKey key: String) throws -> Bool { try withCurrentDictionaryElements { dictionary in dictionary[key[...]] != nil } } /// Returns a list of keys found in the current top-of-stack dictionary. /// - Returns: A list of keys from the dictionary. + /// - Throws: When parsing the root fails. func elementKeysInCurrentDictionary() throws -> [String] { try withCurrentDictionaryElements { dictionary in dictionary.keys.map(String.init) } } diff --git a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift index a94be96d..0d7aef58 100644 --- a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift +++ b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift @@ -49,10 +49,10 @@ enum ParsingError: Swift.Error, Hashable { // MARK: - Parser implementations extension URIParser { - /// Parses the string as a primitive value. /// - Parameter rootKey: The key of the root object, used to filter out unrelated values. /// - Returns: The parsed primitive value, or nil if not found. + /// - Throws: When parsing the root fails. func parseRootAsPrimitive(rootKey: URIParsedKeyComponent) throws -> URIParsedPair? { var data = data switch (configuration.style, configuration.explode) { @@ -90,6 +90,7 @@ extension URIParser { /// Parses the string as an array. /// - Parameter rootKey: The key of the root object, used to filter out unrelated values. /// - Returns: The parsed array. + /// - Throws: When parsing the root fails. func parseRootAsArray(rootKey: URIParsedKeyComponent) throws -> URIParsedPairArray { var data = data switch (configuration.style, configuration.explode) { @@ -169,6 +170,7 @@ extension URIParser { /// Parses the string as a dictionary. /// - Parameter rootKey: The key of the root object, used to filter out unrelated values. /// - Returns: The parsed key/value pairs as an array. + /// - Throws: When parsing the root fails. func parseRootAsDictionary(rootKey: URIParsedKeyComponent) throws -> URIParsedPairArray { var data = data switch (configuration.style, configuration.explode) { From 15d3124222995e716dd8637cac130ed8e5085be2 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 18 Nov 2024 13:55:32 +0100 Subject: [PATCH 5/9] Update Sources/OpenAPIRuntime/URICoder/Common/URIParsedTypes.swift Co-authored-by: Si Beaumont --- .../URICoder/Common/URIParsedTypes.swift | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URIParsedTypes.swift b/Sources/OpenAPIRuntime/URICoder/Common/URIParsedTypes.swift index 9ea19fe8..72b0c34e 100644 --- a/Sources/OpenAPIRuntime/URICoder/Common/URIParsedTypes.swift +++ b/Sources/OpenAPIRuntime/URICoder/Common/URIParsedTypes.swift @@ -60,29 +60,7 @@ typealias URIParsedPairArray = [URIParsedPair] // MARK: - Extensions extension URIParsedKey: CustomStringConvertible { - /// A textual representation of this instance. - /// - /// Calling this property directly is discouraged. Instead, convert an - /// instance of any type to a string by using the `String(describing:)` - /// initializer. This initializer works with any type, and uses the custom - /// `description` property for types that conform to - /// `CustomStringConvertible`: - /// - /// struct Point: CustomStringConvertible { - /// let x: Int, y: Int - /// - /// var description: String { - /// return "(\(x), \(y))" - /// } - /// } - /// - /// let p = Point(x: 21, y: 30) - /// let s = String(describing: p) - /// print(s) - /// // Prints "(21, 30)" - /// - /// The conversion of `p` to a string in the assignment to `s` uses the - /// `Point` type's `description` property. + // swift-format-ignore: AllPublicDeclarationsHaveDocumentation var description: String { if components.isEmpty { return "" } return components.joined(separator: "/") From 9cf928062382e30d40f1803c93b74014f95a090d Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 18 Nov 2024 13:55:59 +0100 Subject: [PATCH 6/9] Update Sources/OpenAPIRuntime/URICoder/Common/URIParsedTypes.swift Co-authored-by: Si Beaumont --- Sources/OpenAPIRuntime/URICoder/Common/URIParsedTypes.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URIParsedTypes.swift b/Sources/OpenAPIRuntime/URICoder/Common/URIParsedTypes.swift index 72b0c34e..faab3576 100644 --- a/Sources/OpenAPIRuntime/URICoder/Common/URIParsedTypes.swift +++ b/Sources/OpenAPIRuntime/URICoder/Common/URIParsedTypes.swift @@ -26,6 +26,7 @@ struct URIParsedKey: Hashable { /// The individual string components. let components: [URIParsedKeyComponent] + /// Creates a new parsed key. /// - Parameter components: The key components. init(_ components: [URIParsedKeyComponent]) { self.components = components } From 78da857e2408f715fab1450e3f530bd279df04a9 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 18 Nov 2024 13:58:42 +0100 Subject: [PATCH 7/9] Update Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift Co-authored-by: Si Beaumont --- .../URICoder/Decoding/URIValueFromNodeDecoder+Single.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift index 57e3fcb0..56b0b7f2 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift @@ -36,7 +36,7 @@ extension URISingleValueDecodingContainer { guard let value = try value else { throw DecodingError.valueNotFound( T.self, - DecodingError.Context.init( + DecodingError.Context( codingPath: codingPath, debugDescription: "Value not found.", underlyingError: nil From 331046f940f4a2779d17bb904346dc9225080a47 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 19 Nov 2024 11:08:51 +0100 Subject: [PATCH 8/9] PR feedback --- .../URICoder/Common/URIDecodedTypes.swift | 22 --- .../URICoder/Common/URIEncodedNode.swift | 6 +- .../URICoder/Common/URIParsedTypes.swift | 6 - .../URIValueFromNodeDecoder+Keyed.swift | 10 +- .../URIValueFromNodeDecoder+Single.swift | 5 + .../URIValueFromNodeDecoder+Unkeyed.swift | 15 +- .../Decoding/URIValueFromNodeDecoder.swift | 154 +++++++++--------- .../URICoder/Parsing/URIParser.swift | 25 +-- .../URICoder/Parsing/Test_URIParser.swift | 4 +- 9 files changed, 115 insertions(+), 132 deletions(-) delete mode 100644 Sources/OpenAPIRuntime/URICoder/Common/URIDecodedTypes.swift diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URIDecodedTypes.swift b/Sources/OpenAPIRuntime/URICoder/Common/URIDecodedTypes.swift deleted file mode 100644 index 56b3f23c..00000000 --- a/Sources/OpenAPIRuntime/URICoder/Common/URIDecodedTypes.swift +++ /dev/null @@ -1,22 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftOpenAPIGenerator open source project -// -// Copyright (c) 2024 Apple Inc. and the SwiftOpenAPIGenerator project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -/// A primitive value produced by `URIValueFromNodeDecoder`. -typealias URIDecodedPrimitive = URIParsedValue - -/// An array value produced by `URIValueFromNodeDecoder`. -typealias URIDecodedArray = URIParsedValueArray - -/// A dictionary value produced by `URIValueFromNodeDecoder`. -typealias URIDecodedDictionary = [Substring: URIParsedValueArray] diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift b/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift index d04d0f68..d2f9edbb 100644 --- a/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift +++ b/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift @@ -73,6 +73,10 @@ extension URIEncodedNode { /// The encoder appended to a node that wasn't an array. case appendingToNonArrayContainer + /// The encoder is trying to mark a container as array, but it's already + /// marked as a container of another type. + case markingExistingNonArrayContainerAsArray + /// The encoder inserted a value for key into a node that wasn't /// a dictionary. case insertingChildValueIntoNonContainer @@ -136,7 +140,7 @@ extension URIEncodedNode { // Already an array. break case .unset: self = .array([]) - default: throw InsertionError.appendingToNonArrayContainer + default: throw InsertionError.markingExistingNonArrayContainerAsArray } } diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URIParsedTypes.swift b/Sources/OpenAPIRuntime/URICoder/Common/URIParsedTypes.swift index faab3576..23d54e65 100644 --- a/Sources/OpenAPIRuntime/URICoder/Common/URIParsedTypes.swift +++ b/Sources/OpenAPIRuntime/URICoder/Common/URIParsedTypes.swift @@ -38,9 +38,6 @@ struct URIParsedKey: Hashable { /// A primitive value produced by `URIParser`. typealias URIParsedValue = String.SubSequence -/// An array of primitive values produced by `URIParser`. -typealias URIParsedValueArray = [URIParsedValue] - /// A key-value produced by `URIParser`. struct URIParsedPair: Equatable { @@ -55,9 +52,6 @@ struct URIParsedPair: Equatable { var value: URIParsedValue } -/// An array of key-value pairs produced by `URIParser`. -typealias URIParsedPairArray = [URIParsedPair] - // MARK: - Extensions extension URIParsedKey: CustomStringConvertible { diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift index 9359f16a..3e3990f6 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift @@ -94,15 +94,9 @@ extension URIKeyedDecodingContainer { extension URIKeyedDecodingContainer: KeyedDecodingContainerProtocol { - var allKeys: [Key] { - do { return try decoder.elementKeysInCurrentDictionary().compactMap { .init(stringValue: $0) } } catch { - return [] - } - } + var allKeys: [Key] { decoder.elementKeysInCurrentDictionary().compactMap { .init(stringValue: $0) } } - func contains(_ key: Key) -> Bool { - do { return try decoder.containsElementInCurrentDictionary(forKey: key.stringValue) } catch { return false } - } + func contains(_ key: Key) -> Bool { decoder.containsElementInCurrentDictionary(forKey: key.stringValue) } var codingPath: [any CodingKey] { decoder.codingPath } diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift index 56b0b7f2..2207bd84 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift @@ -24,6 +24,11 @@ struct URISingleValueDecodingContainer { extension URISingleValueDecodingContainer { /// The underlying value as a single value. + /// + /// Can be nil if the underlying URI is valid, just doesn't contain any value. + /// + /// For example, an empty string input into an exploded form decoder (expecting pairs in the form `key=value`) + /// would result in a nil returned value. var value: URIParsedValue? { get throws { try decoder.currentElementAsSingleValue() } } /// Returns the value found in the underlying node converted to diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift index e7c29017..72fe4739 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift @@ -20,11 +20,14 @@ struct URIUnkeyedDecodingContainer { let decoder: URIValueFromNodeDecoder /// The index of the next item to be decoded. - private var index: URIParsedValueArray.Index = 0 + private(set) var currentIndex: Int /// Creates a new unkeyed container ready to decode the first key. /// - Parameter decoder: The underlying decoder. - init(decoder: URIValueFromNodeDecoder) { self.decoder = decoder } + init(decoder: URIValueFromNodeDecoder) { + self.decoder = decoder + self.currentIndex = 0 + } } extension URIUnkeyedDecodingContainer { @@ -36,7 +39,7 @@ extension URIUnkeyedDecodingContainer { /// - Throws: An error if the container ran out of items. private mutating func _decodingNext(in work: () throws -> R) throws -> R { guard !isAtEnd else { throw URIValueFromNodeDecoder.GeneralError.reachedEndOfUnkeyedContainer } - defer { index += 1 } + defer { currentIndex += 1 } return try work() } @@ -45,7 +48,7 @@ extension URIUnkeyedDecodingContainer { /// - Returns: The next value found. /// - Throws: An error if the container ran out of items. private mutating func _decodeNext() throws -> URIParsedValue { - try _decodingNext { [decoder, index] in try decoder.nestedElementInCurrentArray(atIndex: index) } + try _decodingNext { [decoder, currentIndex] in try decoder.nestedElementInCurrentArray(atIndex: currentIndex) } } /// Returns the next value converted to the provided type. @@ -103,9 +106,7 @@ extension URIUnkeyedDecodingContainer: UnkeyedDecodingContainer { var count: Int? { try? decoder.countOfCurrentArray() } - var isAtEnd: Bool { index == count } - - var currentIndex: Int { index } + var isAtEnd: Bool { currentIndex == count } var codingPath: [any CodingKey] { decoder.codingPath } diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift index f61596e2..11ea1a4f 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift @@ -34,13 +34,13 @@ final class URIValueFromNodeDecoder { private struct ParsingCache { /// The cached result of parsing the string as a primitive value. - var primitive: Result? + var primitive: Result? /// The cached result of parsing the string as an array. - var array: Result? + var array: Result<[URIParsedValue], any Error>? /// The cached result of parsing the string as a dictionary. - var dictionary: Result? + var dictionary: Result<[URIParsedKeyComponent: [URIParsedValue]], any Error>? } /// A cache holding the parsed intermediate representation. @@ -95,10 +95,10 @@ final class URIValueFromNodeDecoder { break case .form: // Try to parse as an array, check the number of elements. - if try withParsedRootAsArray({ $0.count == 0 }) { return nil } + if try parsedRootAsArray().count == 0 { return nil } case .deepObject: // Try to parse as a dictionary, check the number of elements. - if try withParsedRootAsDictionary({ $0.count == 0 }) { return nil } + if try parsedRootAsDictionary().count == 0 { return nil } } return try decodeRoot(type) } @@ -137,61 +137,59 @@ extension URIValueFromNodeDecoder { // MARK: - withParsed methods - /// Use the root as a primitive value. - /// - Parameter work: The closure in which to use the value. - /// - Returns: Any value returned from the closure. - /// - Throws: When parsing the root fails. - private func withParsedRootAsPrimitive(_ work: (URIDecodedPrimitive?) throws -> R) throws -> R { - let value: URIDecodedPrimitive? - if let cached = cache.primitive { + /// Parse the root parsed as a specific type, with automatic caching. + /// - Parameters: + /// - valueKeyPath: A key path to the parsing cache for storing the cached value. + /// - parsingClosure: Gets the value from the parser. + /// - Returns: The parsed value. + /// - Throws: If the parsing closure fails. + private func cachedRoot( + as valueKeyPath: WritableKeyPath?>, + parse parsingClosure: (URIParser) throws -> ValueType + ) throws -> ValueType { + let value: ValueType + if let cached = cache[keyPath: valueKeyPath] { value = try cached.get() } else { - let result: Result + let result: Result do { - value = try parser.parseRootAsPrimitive(rootKey: rootKey)?.value + value = try parsingClosure(parser) result = .success(value) } catch { result = .failure(error) throw error } - cache.primitive = result + cache[keyPath: valueKeyPath] = result } - return try work(value) + return value } - /// Use the root as an array. - /// - Parameter work: The closure in which to use the value. - /// - Returns: Any value returned from the closure. + /// Parse the root as a primitive value. + /// + /// Can be nil if the underlying URI is valid, just doesn't contain any value. + /// + /// For example, an empty string input into an exploded form decoder (expecting pairs in the form `key=value`) + /// would result in a nil returned value. + /// - Returns: The parsed value. /// - Throws: When parsing the root fails. - private func withParsedRootAsArray(_ work: (URIDecodedArray) throws -> R) throws -> R { - let value: URIDecodedArray - if let cached = cache.array { - value = try cached.get() - } else { - let result: Result - do { - value = try parser.parseRootAsArray(rootKey: rootKey).map(\.value) - result = .success(value) - } catch { - result = .failure(error) - throw error - } - cache.array = result - } - return try work(value) + private func parsedRootAsPrimitive() throws -> URIParsedValue? { + try cachedRoot(as: \.primitive, parse: { try $0.parseRootAsPrimitive(rootKey: rootKey)?.value }) } - /// Use the root as a dictionary. - /// - Parameter work: The closure in which to use the value. - /// - Returns: Any value returned from the closure. + /// Parse the root as an array. + /// - Returns: The parsed value. /// - Throws: When parsing the root fails. - private func withParsedRootAsDictionary(_ work: (URIDecodedDictionary) throws -> R) throws -> R { - let value: URIDecodedDictionary - if let cached = cache.dictionary { - value = try cached.get() - } else { - let result: Result - do { + private func parsedRootAsArray() throws -> [URIParsedValue] { + try cachedRoot(as: \.array, parse: { try $0.parseRootAsArray(rootKey: rootKey).map(\.value) }) + } + + /// Parse the root as a dictionary. + /// - Returns: The parsed value. + /// - Throws: When parsing the root fails. + private func parsedRootAsDictionary() throws -> [URIParsedKeyComponent: [URIParsedValue]] { + try cachedRoot( + as: \.dictionary, + parse: { parser in func normalizedDictionaryKey(_ key: URIParsedKey) throws -> Substring { func validateComponentCount(_ count: Int) throws { guard key.components.count == count else { @@ -210,20 +208,13 @@ extension URIValueFromNodeDecoder { case (.deepObject, false): try throwMismatch("Decoding deepObject + unexplode is not supported.") } } - let tuples = try parser.parseRootAsDictionary(rootKey: rootKey) let normalizedTuples: [(Substring, [URIParsedValue])] = try tuples.map { pair in try (normalizedDictionaryKey(pair.key), [pair.value]) } - value = Dictionary(normalizedTuples, uniquingKeysWith: +) - result = .success(value) - } catch { - result = .failure(error) - throw error + return Dictionary(normalizedTuples, uniquingKeysWith: +) } - cache.dictionary = result - } - return try work(value) + ) } // MARK: - decoding utilities @@ -234,7 +225,9 @@ extension URIValueFromNodeDecoder { /// - dictionary: The dictionary in which to find the value. /// - Returns: The value in the dictionary, or nil if not found. /// - Throws: When multiple values are found for the key. - func primitiveValue(forKey key: String, in dictionary: URIDecodedDictionary) throws -> URIParsedValue? { + func primitiveValue(forKey key: String, in dictionary: [URIParsedKeyComponent: [URIParsedValue]]) throws + -> URIParsedValue? + { let values = dictionary[key[...], default: []] if values.isEmpty { return nil } if values.count > 1 { try throwMismatch("Dictionary value contains multiple values.") } @@ -244,10 +237,15 @@ extension URIValueFromNodeDecoder { // MARK: - withCurrent methods /// Use the current top of the stack as a primitive value. + /// + /// Can be nil if the underlying URI is valid, just doesn't contain any value. + /// + /// For example, an empty string input into an exploded form decoder (expecting pairs in the form `key=value`) + /// would result in a nil returned value. /// - Parameter work: The closure in which to use the value. /// - Returns: Any value returned from the closure. /// - Throws: When parsing the root fails. - private func withCurrentPrimitiveElement(_ work: (URIDecodedPrimitive?) throws -> R) throws -> R { + private func withCurrentPrimitiveElement(_ work: (URIParsedValue?) throws -> R) throws -> R { if !codingStack.isEmpty { // Nesting is involved. // There are exactly three scenarios we support: @@ -258,12 +256,10 @@ extension URIValueFromNodeDecoder { let key = codingStack[0] if let intKey = key.intValue { // Top level array. - return try withParsedRootAsArray { array in try work(array[intKey]) } + return try work(parsedRootAsArray()[intKey]) } else { // Top level dictionary. - return try withParsedRootAsDictionary { dictionary in - try work(primitiveValue(forKey: key.stringValue, in: dictionary)) - } + return try work(primitiveValue(forKey: key.stringValue, in: parsedRootAsDictionary())) } } else if codingStack.count == 2 { // Nested array within a top level dictionary. @@ -271,15 +267,13 @@ extension URIValueFromNodeDecoder { guard let nestedArrayKey = codingStack[1].intValue else { try throwMismatch("Nested coding key is not an integer, hinting at unsupported nesting.") } - return try withParsedRootAsDictionary { dictionary in - try work(dictionary[dictionaryKey, default: []][nestedArrayKey]) - } + return try work(parsedRootAsDictionary()[dictionaryKey, default: []][nestedArrayKey]) } else { try throwMismatch("Arbitrary nesting of containers is not supported.") } } else { // Top level primitive. - return try withParsedRootAsPrimitive { try work($0) } + return try work(parsedRootAsPrimitive()) } } @@ -287,16 +281,14 @@ extension URIValueFromNodeDecoder { /// - Parameter work: The closure in which to use the value. /// - Returns: Any value returned from the closure. /// - Throws: When parsing the root fails. - private func withCurrentArrayElements(_ work: (URIDecodedArray) throws -> R) throws -> R { + private func withCurrentArrayElements(_ work: ([URIParsedValue]) throws -> R) throws -> R { if let nestedArrayParentKey = codingStack.first { // Top level is dictionary, first level nesting is array. // Get all the elements that match this rootKey + nested key path. - return try withParsedRootAsDictionary { dictionary in - try work(dictionary[nestedArrayParentKey.stringValue[...]] ?? []) - } + return try work(parsedRootAsDictionary()[nestedArrayParentKey.stringValue[...]] ?? []) } else { // Top level array. - return try withParsedRootAsArray { try work($0) } + return try work(parsedRootAsArray()) } } @@ -304,18 +296,25 @@ extension URIValueFromNodeDecoder { /// - Parameter work: The closure in which to use the value. /// - Returns: Any value returned from the closure. /// - Throws: When parsing the root fails or if there is unsupported extra nesting of containers. - private func withCurrentDictionaryElements(_ work: (URIDecodedDictionary) throws -> R) throws -> R { + private func withCurrentDictionaryElements(_ work: ([URIParsedKeyComponent: [URIParsedValue]]) throws -> R) + throws -> R + { if !codingStack.isEmpty { try throwMismatch("Nesting a dictionary inside another container is not supported.") } else { // Top level dictionary. - return try withParsedRootAsDictionary { try work($0) } + return try work(parsedRootAsDictionary()) } } // MARK: - metadata and data accessors /// Returns the current top-of-stack as a primitive value. + /// + /// Can be nil if the underlying URI is valid, just doesn't contain any value. + /// + /// For example, an empty string input into an exploded form decoder (expecting pairs in the form `key=value`) + /// would result in a nil returned value. /// - Returns: The primitive value, or nil if not found. /// - Throws: When parsing the root fails. func currentElementAsSingleValue() throws -> URIParsedValue? { try withCurrentPrimitiveElement { $0 } } @@ -345,16 +344,19 @@ extension URIValueFromNodeDecoder { /// contains a value for the provided key. /// - Parameter key: The key for which to look for a value. /// - Returns: `true` if a value was found, `false` otherwise. - /// - Throws: When parsing the root fails. - func containsElementInCurrentDictionary(forKey key: String) throws -> Bool { - try withCurrentDictionaryElements { dictionary in dictionary[key[...]] != nil } + func containsElementInCurrentDictionary(forKey key: String) -> Bool { + do { return try withCurrentDictionaryElements { dictionary in dictionary[key[...]] != nil } } catch { + return false + } } /// Returns a list of keys found in the current top-of-stack dictionary. /// - Returns: A list of keys from the dictionary. /// - Throws: When parsing the root fails. - func elementKeysInCurrentDictionary() throws -> [String] { - try withCurrentDictionaryElements { dictionary in dictionary.keys.map(String.init) } + func elementKeysInCurrentDictionary() -> [String] { + do { return try withCurrentDictionaryElements { dictionary in dictionary.keys.map(String.init) } } catch { + return [] + } } } diff --git a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift index 0d7aef58..9e1da427 100644 --- a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift +++ b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift @@ -50,6 +50,11 @@ enum ParsingError: Swift.Error, Hashable { extension URIParser { /// Parses the string as a primitive value. + /// + /// Can be nil if the underlying URI is valid, just doesn't contain any value. + /// + /// For example, an empty string input into an exploded form parser (expecting pairs in the form `key=value`) + /// would result in a nil returned value. /// - Parameter rootKey: The key of the root object, used to filter out unrelated values. /// - Returns: The parsed primitive value, or nil if not found. /// - Throws: When parsing the root fails. @@ -91,13 +96,13 @@ extension URIParser { /// - Parameter rootKey: The key of the root object, used to filter out unrelated values. /// - Returns: The parsed array. /// - Throws: When parsing the root fails. - func parseRootAsArray(rootKey: URIParsedKeyComponent) throws -> URIParsedPairArray { + func parseRootAsArray(rootKey: URIParsedKeyComponent) throws -> [URIParsedPair] { var data = data switch (configuration.style, configuration.explode) { case (.form, true): let keyValueSeparator: Character = "=" let pairSeparator: Character = "&" - var items: URIParsedPairArray = [] + var items: [URIParsedPair] = [] while !data.isEmpty { let (firstResult, firstValue) = data.parseUpToEitherCharacterOrEnd( first: keyValueSeparator, @@ -122,7 +127,7 @@ extension URIParser { let keyValueSeparator: Character = "=" let pairSeparator: Character = "&" let arrayElementSeparator: Character = "," - var items: URIParsedPairArray = [] + var items: [URIParsedPair] = [] while !data.isEmpty { let (firstResult, firstValue) = data.parseUpToEitherCharacterOrEnd( first: keyValueSeparator, @@ -154,7 +159,7 @@ extension URIParser { return items case (.simple, _): let pairSeparator: Character = "," - var items: URIParsedPairArray = [] + var items: [URIParsedPair] = [] while !data.isEmpty { let value = data.parseUpToCharacterOrEnd(pairSeparator) items.append(.init(key: .empty, value: unescapeValue(value))) @@ -171,13 +176,13 @@ extension URIParser { /// - Parameter rootKey: The key of the root object, used to filter out unrelated values. /// - Returns: The parsed key/value pairs as an array. /// - Throws: When parsing the root fails. - func parseRootAsDictionary(rootKey: URIParsedKeyComponent) throws -> URIParsedPairArray { + func parseRootAsDictionary(rootKey: URIParsedKeyComponent) throws -> [URIParsedPair] { var data = data switch (configuration.style, configuration.explode) { case (.form, true): let keyValueSeparator: Character = "=" let pairSeparator: Character = "&" - var items: URIParsedPairArray = [] + var items: [URIParsedPair] = [] while !data.isEmpty { let (firstResult, firstValue) = data.parseUpToEitherCharacterOrEnd( first: keyValueSeparator, @@ -196,7 +201,7 @@ extension URIParser { let keyValueSeparator: Character = "=" let pairSeparator: Character = "&" let arrayElementSeparator: Character = "," - var items: URIParsedPairArray = [] + var items: [URIParsedPair] = [] while !data.isEmpty { let (firstResult, firstValue) = data.parseUpToEitherCharacterOrEnd( first: keyValueSeparator, @@ -241,7 +246,7 @@ extension URIParser { case (.simple, true): let keyValueSeparator: Character = "=" let pairSeparator: Character = "," - var items: URIParsedPairArray = [] + var items: [URIParsedPair] = [] while !data.isEmpty { let (firstResult, firstValue) = data.parseUpToEitherCharacterOrEnd( first: keyValueSeparator, @@ -261,7 +266,7 @@ extension URIParser { return items case (.simple, false): let pairSeparator: Character = "," - var items: URIParsedPairArray = [] + var items: [URIParsedPair] = [] while !data.isEmpty { let rawKey = data.parseUpToCharacterOrEnd(pairSeparator) let value: URIParsedValue @@ -275,7 +280,7 @@ extension URIParser { let pairSeparator: Character = "&" let nestedKeyStart: Character = "[" let nestedKeyEnd: Character = "]" - var items: URIParsedPairArray = [] + var items: [URIParsedPair] = [] while !data.isEmpty { let (firstResult, firstValue) = data.parseUpToEitherCharacterOrEnd( first: keyValueSeparator, diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift index dd8c2215..a94805ac 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift @@ -539,8 +539,8 @@ final class Test_URIParser: Test_Runtime { struct Input { var rootKey: URIParsedKeyComponent var primitive: RootInput - var array: RootInput - var dictionary: RootInput + var array: RootInput<[URIParsedPair]> + var dictionary: RootInput<[URIParsedPair]> } struct Variants { var formExplode: Input From 1ea806a7bc48f45e9eeddb586b0aa2cd59a7decf Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 21 Nov 2024 18:16:46 +0100 Subject: [PATCH 9/9] PR feedback --- .../URICoder/Decoding/URIValueFromNodeDecoder.swift | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift index 11ea1a4f..83c77c90 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift @@ -345,18 +345,14 @@ extension URIValueFromNodeDecoder { /// - Parameter key: The key for which to look for a value. /// - Returns: `true` if a value was found, `false` otherwise. func containsElementInCurrentDictionary(forKey key: String) -> Bool { - do { return try withCurrentDictionaryElements { dictionary in dictionary[key[...]] != nil } } catch { - return false - } + (try? withCurrentDictionaryElements({ dictionary in dictionary[key[...]] != nil })) ?? false } /// Returns a list of keys found in the current top-of-stack dictionary. /// - Returns: A list of keys from the dictionary. /// - Throws: When parsing the root fails. func elementKeysInCurrentDictionary() -> [String] { - do { return try withCurrentDictionaryElements { dictionary in dictionary.keys.map(String.init) } } catch { - return [] - } + (try? withCurrentDictionaryElements { dictionary in dictionary.keys.map(String.init) }) ?? [] } }