From 956d9661bb306e5563d81c2507c0b5d9b2ff1d56 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 21 Aug 2023 17:17:37 +0200 Subject: [PATCH 01/17] [Prototype] URIEncoder --- NOTICE.txt | 9 + .../OpenAPIRuntime/Coding/URIEncoder.swift | 64 +++++ Sources/OpenAPIRuntime/Coding/URINode.swift | 99 ++++++++ .../OpenAPIRuntime/Coding/URISerializer.swift | 238 +++++++++++++++++ .../Coding/URITranslator+Keyed.swift | 171 +++++++++++++ .../Coding/URITranslator+Single.swift | 138 ++++++++++ .../Coding/URITranslator+Unkeyed.swift | 169 +++++++++++++ .../OpenAPIRuntime/Coding/URITranslator.swift | 123 +++++++++ .../Coding/Test_URIEncoder.swift | 30 +++ .../Coding/Test_URISerializer.swift | 159 ++++++++++++ .../Coding/Test_URITranslator.swift | 239 ++++++++++++++++++ 11 files changed, 1439 insertions(+) create mode 100644 Sources/OpenAPIRuntime/Coding/URIEncoder.swift create mode 100644 Sources/OpenAPIRuntime/Coding/URINode.swift create mode 100644 Sources/OpenAPIRuntime/Coding/URISerializer.swift create mode 100644 Sources/OpenAPIRuntime/Coding/URITranslator+Keyed.swift create mode 100644 Sources/OpenAPIRuntime/Coding/URITranslator+Single.swift create mode 100644 Sources/OpenAPIRuntime/Coding/URITranslator+Unkeyed.swift create mode 100644 Sources/OpenAPIRuntime/Coding/URITranslator.swift create mode 100644 Tests/OpenAPIRuntimeTests/Coding/Test_URIEncoder.swift create mode 100644 Tests/OpenAPIRuntimeTests/Coding/Test_URISerializer.swift create mode 100644 Tests/OpenAPIRuntimeTests/Coding/Test_URITranslator.swift diff --git a/NOTICE.txt b/NOTICE.txt index d2e20c1a..ae3e5a4f 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -32,3 +32,12 @@ This product contains derivations of various scripts and templates from SwiftNIO * https://www.apache.org/licenses/LICENSE-2.0 * HOMEPAGE: * https://github.com/apple/swift-nio + +--- + +This product contains a coder implementation inspired by swift-http-structured-headers. + + * LICENSE (Apache License 2.0): + * https://www.apache.org/licenses/LICENSE-2.0 + * HOMEPAGE: + * https://github.com/apple/swift-http-structured-headers diff --git a/Sources/OpenAPIRuntime/Coding/URIEncoder.swift b/Sources/OpenAPIRuntime/Coding/URIEncoder.swift new file mode 100644 index 00000000..7f7ffdf4 --- /dev/null +++ b/Sources/OpenAPIRuntime/Coding/URIEncoder.swift @@ -0,0 +1,64 @@ +//===----------------------------------------------------------------------===// +// +// 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 type that encodes an `Encodable` objects to an URL-encoded string +/// using the rules from RFC 6570, RFC 1866, and OpenAPI 3.0.3, depending on +/// the configuration. +/// +/// - [OpenAPI 3.0.3 styles](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#style-examples) +public struct URIEncoder: Sendable { + public init() {} +} + +extension URIEncoder { + + public enum KeyComponent { + case index(Int) + case key(String) + } + + /// Attempt to encode an object into an URI string. + /// + /// - Parameters: + /// - value: The value to encode. + /// - key: The key for which to encode the value. Can be an empty key, + /// in which case you still get a key-value pair, like `=foo`. + /// - Returns: The URI string. + public func encode( + _ value: some Encodable, + forKey key: KeyComponent + ) throws -> String { + + // Under the hood, URIEncoder first encodes the Encodable type + // into a URINode using URITranslator, and then + // URISerializer encodes the URINode into a string based + // on the configured behavior. + + let translator = URITranslator() + var serializer = URISerializer() + let node = try translator.translateValue(value) + + let convertedKey: URISerializer.KeyComponent + switch key { + case .index(let int): + convertedKey = .index(int) + case .key(let string): + convertedKey = .key(string) + } + let encodedString = try serializer.writeNode(node, forKey: convertedKey) + return encodedString + } +} diff --git a/Sources/OpenAPIRuntime/Coding/URINode.swift b/Sources/OpenAPIRuntime/Coding/URINode.swift new file mode 100644 index 00000000..d780cfc0 --- /dev/null +++ b/Sources/OpenAPIRuntime/Coding/URINode.swift @@ -0,0 +1,99 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +enum URINode: Equatable { + + case unset + case primitive(Primitive) + case array([Self]) + case dictionary([String: Self]) + + enum Primitive: Equatable { + case bool(Bool) + case string(String) + case integer(Int) + case double(Double) + } +} + +extension URINode { + + enum InsertionError: Swift.Error { + case settingPrimitiveValueAgain + case settingValueOnAContainer + case appendingToNonArrayContainer + case insertingChildValueIntoNonContainer + case insertingChildValueIntoArrayUsingNonIntValueKey + } + + mutating func set(_ value: Primitive) throws { + switch self { + case .unset: + self = .primitive(value) + case .primitive: + throw InsertionError.settingPrimitiveValueAgain + case .array, .dictionary: + throw InsertionError.settingValueOnAContainer + } + } + + mutating func insert( + _ childValue: Self, + atKey key: Key + ) throws { + switch self { + case .dictionary(var dictionary): + self = .unset + dictionary[key.stringValue] = childValue + self = .dictionary(dictionary) + case .array(var array): + // Check that this is a valid key for an unkeyed container, + // but don't actually extract the index, we only support appending + // here. + guard let intValue = key.intValue else { + throw InsertionError.insertingChildValueIntoArrayUsingNonIntValueKey + } + precondition( + intValue == array.count, + "Unkeyed container inserting at an incorrect index" + ) + self = .unset + array.append(childValue) + self = .array(array) + case .unset: + if let _ = key.intValue { + self = .array([childValue]) + } else { + self = .dictionary([key.stringValue: childValue]) + } + default: + throw InsertionError.insertingChildValueIntoNonContainer + } + } + + mutating func append(_ childValue: Self) throws { + switch self { + case .array(var items): + self = .unset + items.append(childValue) + self = .array(items) + case .unset: + self = .array([childValue]) + default: + throw InsertionError.appendingToNonArrayContainer + } + } +} diff --git a/Sources/OpenAPIRuntime/Coding/URISerializer.swift b/Sources/OpenAPIRuntime/Coding/URISerializer.swift new file mode 100644 index 00000000..cd87d5b4 --- /dev/null +++ b/Sources/OpenAPIRuntime/Coding/URISerializer.swift @@ -0,0 +1,238 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +/// Converts an URINode value into a string. +struct URISerializer: Sendable { + + struct Configuration { + // TODO: Add a few prebuilt options. + struct KeyEncoding { + + // TODO: Hide to avoid adding an enum case being breaking. + enum DictionaryKeyEncoding { + + /// Put the key between square brackets. + /// + /// Example: foo[one]=1&foo[two]=2&foo[three]=3 + case bracketsWithKey + + /// Concatenate components using a period. + /// + /// Example: foo.one=1&foo.two=2&foo.three=3 + case concatenatedByPeriods + } + var dictionary: DictionaryKeyEncoding = .bracketsWithKey + + // TODO: Hide to avoid adding an enum case being breaking. + enum ArrayIndexEncoding { + + /// None, just repeat the container's key. + /// + /// Example: foo=a&foo=b&foo=c. + case none + + /// Repeat empty brackets after the container's key. + /// + /// Example: foo[]=a&foo[]=b&foo[]=c. + case emptyBrackets + + /// Put the index in square brackets after the container's key. + /// + /// Example: foo[0]=a&foo[1]=b&foo[2]=c. + case bracketsWithIndex + } + var array: ArrayIndexEncoding = .bracketsWithIndex + } + var keyEncoding: KeyEncoding = .init() + + // struct ValueEncoding { + // var unescapedCharacterSet: CharacterSet + // + // // TODO: Add a few prebuilt options. + // // static var urlForm: Self { + // // + // // } + // } + // var valueEncoding: ValueEncoding + } + + let configuration: Configuration + + private var data: String + + init(configuration: Configuration = .init()) { + self.configuration = configuration + self.data = "" + } +} + +extension URISerializer { + + mutating func writeNode( + _ value: URINode, + forKey keyComponent: KeyComponent + ) throws -> String { + defer { + data.removeAll(keepingCapacity: true) + } + try serializeNode(value, forKey: [keyComponent]) + return data + } +} + +extension URISerializer { + + enum KeyComponent { + case index(Int) + case key(String) + } + + enum SerializationError: Swift.Error { + case topLevelKeyMustBeString + } + + typealias Key = [KeyComponent] + + private func computeSafeKey(_ unsafeKey: String) -> String { + // TODO: Escape the key here + unsafeKey + } + + private func computeSafeValue(_ unsafeValue: String) -> String { + // TODO: Escape the value here + unsafeValue + } + + private func stringifiedKeyComponent(_ keyComponent: KeyComponent) -> String { + // TODO: This will differ based on configuration. + switch keyComponent { + case .index(let int): + switch configuration.keyEncoding.array { + case .none: + return "" + case .emptyBrackets: + return "[]" + case .bracketsWithIndex: + return "[\(int)]" + } + case .key(let string): + let safeKey = computeSafeKey(string) + switch configuration.keyEncoding.dictionary { + case .bracketsWithKey: + return "[\(safeKey)]" + case .concatenatedByPeriods: + return ".\(safeKey)" + } + } + } + + private func stringifiedKey(_ key: Key) throws -> String { + // The root key is handled separately. + guard !key.isEmpty else { + return "" + } + let topLevelKey = key[0] + guard case .key(let string) = topLevelKey else { + throw SerializationError.topLevelKeyMustBeString + } + let safeTopLevelKey = computeSafeKey(string) + return + ([safeTopLevelKey] + + key + .dropFirst() + .map(stringifiedKeyComponent)) + .joined() + } + + private mutating func serializeNode(_ value: URINode, forKey key: Key) throws { + switch value { + case .unset: + // TODO: Is there a distinction here between `a=` and `a`, in other + // words between an empty string value and a nil value? + data.append(try stringifiedKey(key)) + data.append("=") + case .primitive(let primitive): + try serializePrimitiveValue(primitive, forKey: key) + case .array(let array): + try serializeArray(array, forKey: key) + case .dictionary(let dictionary): + try serializeDictionary(dictionary, forKey: key) + } + } + + private mutating func serializePrimitiveValue( + _ value: URINode.Primitive, + forKey key: Key + ) throws { + let stringValue: String + switch value { + case .bool(let bool): + stringValue = bool.description + case .string(let string): + stringValue = computeSafeValue(string) + case .integer(let int): + stringValue = int.description + case .double(let double): + stringValue = double.description + } + data.append(try stringifiedKey(key)) + data.append("=") + data.append(stringValue) + } + + private mutating func serializeArray( + _ array: [URINode], + forKey key: Key + ) throws { + try serializeTuples( + array.enumerated() + .map { index, element in + (key + [.index(index)], element) + } + ) + } + + private mutating func serializeDictionary( + _ dictionary: [String: URINode], + forKey key: Key + ) throws { + try serializeTuples( + dictionary + .sorted { a, b in + a.key.localizedCaseInsensitiveCompare(b.key) + == .orderedAscending + } + .map { elementKey, element in + (key + [.key(elementKey)], element) + } + ) + } + + private mutating func serializeTuples( + _ items: [(Key, URINode)] + ) throws { + guard !items.isEmpty else { + return + } + for (key, element) in items.dropLast() { + try serializeNode(element, forKey: key) + data.append("&") + } + if let (key, element) = items.last { + try serializeNode(element, forKey: key) + } + } +} diff --git a/Sources/OpenAPIRuntime/Coding/URITranslator+Keyed.swift b/Sources/OpenAPIRuntime/Coding/URITranslator+Keyed.swift new file mode 100644 index 00000000..04a46a88 --- /dev/null +++ b/Sources/OpenAPIRuntime/Coding/URITranslator+Keyed.swift @@ -0,0 +1,171 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +struct URIKeyedEncodingContainer { + let translator: URITranslator +} + +extension URIKeyedEncodingContainer { + private func _insertValue(_ node: URINode, atKey key: Key) throws { + try translator.currentStackEntry.storage.insert(node, atKey: key) + } + + private func _insertValue(_ node: URINode.Primitive, atKey key: Key) throws { + try _insertValue(.primitive(node), atKey: key) + } + + private func _insertBinaryFloatingPoint( + _ value: some BinaryFloatingPoint, + atKey key: Key + ) throws { + try _insertValue(.double(Double(value)), atKey: key) + } + + private func _insertFixedWidthInteger( + _ value: some FixedWidthInteger, + atKey key: Key + ) throws { + guard let validatedValue = Int(exactly: value) else { + throw URITranslator.GeneralError.integerOutOfRange + } + try _insertValue(.integer(validatedValue), atKey: key) + } +} + +extension URIKeyedEncodingContainer: KeyedEncodingContainerProtocol { + + var codingPath: [any CodingKey] { + translator.codingPath + } + + mutating func encodeNil(forKey key: Key) throws { + // Setting a nil value is equivalent to not encoding the value at all. + } + + mutating func encode(_ value: Bool, forKey key: Key) throws { + try _insertValue(.bool(value), atKey: key) + } + + mutating func encode(_ value: String, forKey key: Key) throws { + try _insertValue(.string(value), atKey: key) + } + + mutating func encode(_ value: Double, forKey key: Key) throws { + try _insertBinaryFloatingPoint(value, atKey: key) + } + + mutating func encode(_ value: Float, forKey key: Key) throws { + try _insertBinaryFloatingPoint(value, atKey: key) + } + + mutating func encode(_ value: Int, forKey key: Key) throws { + try _insertFixedWidthInteger(value, atKey: key) + } + + mutating func encode(_ value: Int8, forKey key: Key) throws { + try _insertFixedWidthInteger(value, atKey: key) + } + + mutating func encode(_ value: Int16, forKey key: Key) throws { + try _insertFixedWidthInteger(value, atKey: key) + } + + mutating func encode(_ value: Int32, forKey key: Key) throws { + try _insertFixedWidthInteger(value, atKey: key) + } + + mutating func encode(_ value: Int64, forKey key: Key) throws { + try _insertFixedWidthInteger(value, atKey: key) + } + + mutating func encode(_ value: UInt, forKey key: Key) throws { + try _insertFixedWidthInteger(value, atKey: key) + } + + mutating func encode(_ value: UInt8, forKey key: Key) throws { + try _insertFixedWidthInteger(value, atKey: key) + } + + mutating func encode(_ value: UInt16, forKey key: Key) throws { + try _insertFixedWidthInteger(value, atKey: key) + } + + mutating func encode(_ value: UInt32, forKey key: Key) throws { + try _insertFixedWidthInteger(value, atKey: key) + } + + mutating func encode(_ value: UInt64, forKey key: Key) throws { + try _insertFixedWidthInteger(value, atKey: key) + } + + mutating func encode(_ value: T, forKey key: Key) throws where T: Encodable { + switch value { + case let value as UInt8: + try encode(value, forKey: key) + case let value as Int8: + try encode(value, forKey: key) + case let value as UInt16: + try encode(value, forKey: key) + case let value as Int16: + try encode(value, forKey: key) + case let value as UInt32: + try encode(value, forKey: key) + case let value as Int32: + try encode(value, forKey: key) + case let value as UInt64: + try encode(value, forKey: key) + case let value as Int64: + try encode(value, forKey: key) + case let value as Int: + try encode(value, forKey: key) + case let value as UInt: + try encode(value, forKey: key) + case let value as Float: + try encode(value, forKey: key) + case let value as Double: + try encode(value, forKey: key) + case let value as String: + try encode(value, forKey: key) + case let value as Bool: + try encode(value, forKey: key) + default: + translator.push(key: .init(key), newStorage: .unset) + try value.encode(to: translator) + try translator.pop() + } + } + + mutating func nestedContainer( + keyedBy keyType: NestedKey.Type, + forKey key: Key + ) -> KeyedEncodingContainer where NestedKey: CodingKey { + translator.container(keyedBy: NestedKey.self) + } + + mutating func nestedUnkeyedContainer( + forKey key: Key + ) -> any UnkeyedEncodingContainer { + translator.unkeyedContainer() + } + + mutating func superEncoder() -> any Encoder { + translator + } + + mutating func superEncoder(forKey key: Key) -> any Encoder { + translator + } +} diff --git a/Sources/OpenAPIRuntime/Coding/URITranslator+Single.swift b/Sources/OpenAPIRuntime/Coding/URITranslator+Single.swift new file mode 100644 index 00000000..b1eb2d5c --- /dev/null +++ b/Sources/OpenAPIRuntime/Coding/URITranslator+Single.swift @@ -0,0 +1,138 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +struct URISingleValueEncodingContainer: SingleValueEncodingContainer { + let translator: URITranslator +} + +extension URISingleValueEncodingContainer { + private func _setValue(_ node: URINode.Primitive) throws { + try translator.currentStackEntry.storage.set(node) + } + + private func _setBinaryFloatingPoint(_ value: some BinaryFloatingPoint) throws { + try _setValue(.double(Double(value))) + } + + private func _setFixedWidthInteger(_ value: some FixedWidthInteger) throws { + guard let validatedValue = Int(exactly: value) else { + throw URITranslator.GeneralError.integerOutOfRange + } + try _setValue(.integer(validatedValue)) + } +} + +extension URISingleValueEncodingContainer { + + var codingPath: [any CodingKey] { + translator.codingPath + } + + func encodeNil() throws { + throw URITranslator.GeneralError.nilNotSupported + } + + func encode(_ value: Bool) throws { + try _setValue(.bool(value)) + } + + func encode(_ value: String) throws { + try _setValue(.string(value)) + } + + func encode(_ value: Double) throws { + try _setBinaryFloatingPoint(value) + } + + func encode(_ value: Float) throws { + try _setBinaryFloatingPoint(value) + } + + func encode(_ value: Int) throws { + try _setFixedWidthInteger(value) + } + + func encode(_ value: Int8) throws { + try _setFixedWidthInteger(value) + } + + func encode(_ value: Int16) throws { + try _setFixedWidthInteger(value) + } + + func encode(_ value: Int32) throws { + try _setFixedWidthInteger(value) + } + + func encode(_ value: Int64) throws { + try _setFixedWidthInteger(value) + } + + func encode(_ value: UInt) throws { + try _setFixedWidthInteger(value) + } + + func encode(_ value: UInt8) throws { + try _setFixedWidthInteger(value) + } + + func encode(_ value: UInt16) throws { + try _setFixedWidthInteger(value) + } + + func encode(_ value: UInt32) throws { + try _setFixedWidthInteger(value) + } + + func encode(_ value: UInt64) throws { + try _setFixedWidthInteger(value) + } + + func encode(_ value: T) throws where T: Encodable { + switch value { + case let value as UInt8: + try encode(value) + case let value as Int8: + try encode(value) + case let value as UInt16: + try encode(value) + case let value as Int16: + try encode(value) + case let value as UInt32: + try encode(value) + case let value as Int32: + try encode(value) + case let value as UInt64: + try encode(value) + case let value as Int64: + try encode(value) + case let value as Int: + try encode(value) + case let value as UInt: + try encode(value) + case let value as Float: + try encode(value) + case let value as Double: + try encode(value) + case let value as String: + try encode(value) + case let value as Bool: + try encode(value) + default: + throw URITranslator.GeneralError.nestedValueInSingleValueContainer + } + } +} diff --git a/Sources/OpenAPIRuntime/Coding/URITranslator+Unkeyed.swift b/Sources/OpenAPIRuntime/Coding/URITranslator+Unkeyed.swift new file mode 100644 index 00000000..922a7878 --- /dev/null +++ b/Sources/OpenAPIRuntime/Coding/URITranslator+Unkeyed.swift @@ -0,0 +1,169 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +struct URIUnkeyedEncodingContainer { + let translator: URITranslator +} + +extension URIUnkeyedEncodingContainer { + private func _appendValue(_ node: URINode) throws { + try translator.currentStackEntry.storage.append(node) + } + + private func _appendValue(_ node: URINode.Primitive) throws { + try _appendValue(.primitive(node)) + } + + private func _appendBinaryFloatingPoint(_ value: some BinaryFloatingPoint) throws { + try _appendValue(.double(Double(value))) + } + + private func _appendFixedWidthInteger(_ value: some FixedWidthInteger) throws { + guard let validatedValue = Int(exactly: value) else { + throw URITranslator.GeneralError.integerOutOfRange + } + try _appendValue(.integer(validatedValue)) + } +} + +extension URIUnkeyedEncodingContainer: UnkeyedEncodingContainer { + + var codingPath: [any CodingKey] { + translator.codingPath + } + + var count: Int { + switch translator.currentStackEntry.storage { + case .array(let array): + return array.count + case .unset: + return 0 + default: + fatalError("Cannot have an unkeyed container at \(translator.currentStackEntry).") + } + } + + func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer { + translator.unkeyedContainer() + } + + func nestedContainer( + keyedBy keyType: NestedKey.Type + ) -> KeyedEncodingContainer where NestedKey: CodingKey { + translator.container(keyedBy: NestedKey.self) + } + + func superEncoder() -> any Encoder { + translator + } + + func encodeNil() throws { + throw URITranslator.GeneralError.nilNotSupported + } + + func encode(_ value: Bool) throws { + try _appendValue(.bool(value)) + } + + func encode(_ value: String) throws { + try _appendValue(.string(value)) + } + + func encode(_ value: Double) throws { + try _appendBinaryFloatingPoint(value) + } + + func encode(_ value: Float) throws { + try _appendBinaryFloatingPoint(value) + } + + func encode(_ value: Int) throws { + try _appendFixedWidthInteger(value) + } + + func encode(_ value: Int8) throws { + try _appendFixedWidthInteger(value) + } + + func encode(_ value: Int16) throws { + try _appendFixedWidthInteger(value) + } + + func encode(_ value: Int32) throws { + try _appendFixedWidthInteger(value) + } + + func encode(_ value: Int64) throws { + try _appendFixedWidthInteger(value) + } + + func encode(_ value: UInt) throws { + try _appendFixedWidthInteger(value) + } + + func encode(_ value: UInt8) throws { + try _appendFixedWidthInteger(value) + } + + func encode(_ value: UInt16) throws { + try _appendFixedWidthInteger(value) + } + + func encode(_ value: UInt32) throws { + try _appendFixedWidthInteger(value) + } + + func encode(_ value: UInt64) throws { + try _appendFixedWidthInteger(value) + } + + func encode(_ value: T) throws where T: Encodable { + switch value { + case let value as UInt8: + try encode(value) + case let value as Int8: + try encode(value) + case let value as UInt16: + try encode(value) + case let value as Int16: + try encode(value) + case let value as UInt32: + try encode(value) + case let value as Int32: + try encode(value) + case let value as UInt64: + try encode(value) + case let value as Int64: + try encode(value) + case let value as Int: + try encode(value) + case let value as UInt: + try encode(value) + case let value as Float: + try encode(value) + case let value as Double: + try encode(value) + case let value as String: + try encode(value) + case let value as Bool: + try encode(value) + default: + translator.push(key: .init(intValue: count), newStorage: .unset) + try value.encode(to: translator) + try translator.pop() + } + } +} diff --git a/Sources/OpenAPIRuntime/Coding/URITranslator.swift b/Sources/OpenAPIRuntime/Coding/URITranslator.swift new file mode 100644 index 00000000..2895cdb1 --- /dev/null +++ b/Sources/OpenAPIRuntime/Coding/URITranslator.swift @@ -0,0 +1,123 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +/// Converts an Encodable type into a URINode. +final class URITranslator { + + /// The coding key. + struct _CodingKey: CodingKey { + var stringValue: String + var intValue: Int? + + init(stringValue: String) { + self.stringValue = stringValue + self.intValue = nil + } + + init(intValue: Int) { + self.stringValue = "\(intValue)" + self.intValue = intValue + } + + init(_ key: some CodingKey) { + self.stringValue = key.stringValue + self.intValue = key.intValue + } + } + + /// An entry in the coding stack for \_URIEncoder. + /// + /// This is used to keep track of where we are in the encode. + struct CodingStackEntry { + var key: _CodingKey + var storage: URINode + } + + enum GeneralError: Swift.Error { + case nilNotSupported + case dataNotSupported + case invalidEncoderCallForValue + case integerOutOfRange + case nestedValueInSingleValueContainer + } + + var _codingPath: [CodingStackEntry] + var currentStackEntry: CodingStackEntry + init() { + self._codingPath = [] + self.currentStackEntry = CodingStackEntry( + key: .init(stringValue: ""), + storage: .unset + ) + } + + func translateValue(_ value: some Encodable) throws -> URINode { + try value.encode(to: self) + let encodedValue = currentStackEntry.storage + _codingPath = [] + currentStackEntry = CodingStackEntry( + key: .init(stringValue: ""), + storage: .unset + ) + return encodedValue + } +} + +extension URITranslator: Encoder { + var codingPath: [any CodingKey] { + // The coding path meaningful to the types conforming to Codable. + // 1. Omit the root coding path. + // 2. Add the current stack entry's coding path. + (_codingPath + .dropFirst() + .map(\.key) + + [currentStackEntry.key]) + .map { $0 as any CodingKey } + } + + var userInfo: [CodingUserInfoKey: Any] { + [:] + } + + func push(key: _CodingKey, newStorage: URINode) { + _codingPath.append(currentStackEntry) + currentStackEntry = .init(key: key, storage: newStorage) + } + + func pop() throws { + // This is called when we've completed the storage in the current container. + // We can pop the value at the base of the stack, then "insert" the current one + // into it, and save the new value as the new current. + let current = currentStackEntry + var newCurrent = _codingPath.removeLast() + try newCurrent.storage.insert(current.storage, atKey: current.key) + currentStackEntry = newCurrent + } + + func container( + keyedBy type: Key.Type + ) -> KeyedEncodingContainer where Key: CodingKey { + KeyedEncodingContainer(URIKeyedEncodingContainer(translator: self)) + } + + func unkeyedContainer() -> any UnkeyedEncodingContainer { + URIUnkeyedEncodingContainer(translator: self) + } + + func singleValueContainer() -> any SingleValueEncodingContainer { + URISingleValueEncodingContainer(translator: self) + } +} diff --git a/Tests/OpenAPIRuntimeTests/Coding/Test_URIEncoder.swift b/Tests/OpenAPIRuntimeTests/Coding/Test_URIEncoder.swift new file mode 100644 index 00000000..44d5ece6 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Coding/Test_URIEncoder.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// 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 XCTest +@testable import OpenAPIRuntime + +final class Test_URIEncoder: Test_Runtime { + + func testEncoding() throws { + struct Foo: Encodable { + var bar: String + } + let encoder = URIEncoder() + let encodedString = try encoder.encode( + Foo(bar: "hello world"), + forKey: .key("root") + ) + XCTAssertEqual(encodedString, "root[bar]=hello world") + } +} diff --git a/Tests/OpenAPIRuntimeTests/Coding/Test_URISerializer.swift b/Tests/OpenAPIRuntimeTests/Coding/Test_URISerializer.swift new file mode 100644 index 00000000..a7243667 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Coding/Test_URISerializer.swift @@ -0,0 +1,159 @@ +//===----------------------------------------------------------------------===// +// +// 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 XCTest +@testable import OpenAPIRuntime + +final class Test_URISerializer: Test_Runtime { + + func testTranslating() throws { + struct Case { + var value: URINode + var expectedString: String + var file: StaticString = #file + var line: UInt = #line + } + func makeCase(_ value: URINode, _ expectedString: String, file: StaticString = #file, line: UInt = #line) + -> Case + { + .init(value: value, expectedString: expectedString, file: file, line: line) + } + + let cases: [Case] = [ + makeCase(.primitive(.string("")), "root="), + makeCase(.primitive(.string("Hello World")), "root=Hello World"), + makeCase(.primitive(.integer(1234)), "root=1234"), + makeCase(.primitive(.double(12.34)), "root=12.34"), + makeCase(.primitive(.bool(true)), "root=true"), + makeCase( + .array([ + .primitive(.string("a")), + .primitive(.string("b")), + .primitive(.string("c")), + ]), + "root[0]=a&root[1]=b&root[2]=c" + ), + makeCase( + .array([ + .array([ + .primitive(.string("a")) + ]), + .array([ + .primitive(.string("b")), + .primitive(.string("c")), + ]), + ]), + "root[0][0]=a&root[1][0]=b&root[1][1]=c" + ), + makeCase( + .dictionary([ + "foo": .primitive(.string("bar")) + ]), + "root[foo]=bar" + ), + makeCase( + .dictionary([ + "simple": .dictionary([ + "foo": .primitive(.string("bar")) + ]) + ]), + "root[simple][foo]=bar" + ), + makeCase( + .array([ + .dictionary([ + "foo": .primitive(.string("bar")) + ]), + .dictionary([ + "foo": .primitive(.string("baz")) + ]), + ]), + "root[0][foo]=bar&root[1][foo]=baz" + ), + makeCase( + .array([ + .array([ + .dictionary([ + "foo": .primitive(.string("bar")) + ]) + ]), + .array([ + .dictionary([ + "foo": .primitive(.string("baz")) + ]) + ]), + ]), + "root[0][0][foo]=bar&root[1][0][foo]=baz" + ), + makeCase( + .dictionary([ + "one": .primitive(.integer(1)), + "two": .primitive(.integer(2)), + ]), + "root[one]=1&root[two]=2" + ), + makeCase( + .dictionary([ + "A": .dictionary([ + "one": .primitive(.integer(1)), + "two": .primitive(.integer(2)), + ]), + "B": .dictionary([ + "three": .primitive(.integer(3)), + "four": .primitive(.integer(4)), + ]), + ]), + "root[A][one]=1&root[A][two]=2&root[B][four]=4&root[B][three]=3" + ), + makeCase( + .dictionary([ + "barkey": .dictionary([ + "foo": .primitive(.string("bar")) + ]), + "bazkey": .dictionary([ + "foo": .primitive(.string("baz")) + ]), + ]), + "root[barkey][foo]=bar&root[bazkey][foo]=baz" + ), + makeCase( + .dictionary([ + "outBar": .dictionary([ + "inBar": .dictionary([ + "foo": .primitive(.string("bar")) + ]) + ]), + "outBaz": .dictionary([ + "inBaz": .dictionary([ + "foo": .primitive(.string("baz")) + ]) + ]), + ]), + "root[outBar][inBar][foo]=bar&root[outBaz][inBaz][foo]=baz" + ), + ] + var serializer = URISerializer() + for testCase in cases { + let encodedString = try serializer.writeNode( + testCase.value, + forKey: .key("root") + ) + XCTAssertEqual( + encodedString, + testCase.expectedString, + file: testCase.file, + line: testCase.line + ) + } + } +} diff --git a/Tests/OpenAPIRuntimeTests/Coding/Test_URITranslator.swift b/Tests/OpenAPIRuntimeTests/Coding/Test_URITranslator.swift new file mode 100644 index 00000000..82f7daf4 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Coding/Test_URITranslator.swift @@ -0,0 +1,239 @@ +//===----------------------------------------------------------------------===// +// +// 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 XCTest +@testable import OpenAPIRuntime + +final class Test_URITranslator: Test_Runtime { + + func testTranslating() throws { + struct Case { + var value: any Encodable + var expectedNode: URINode + var file: StaticString = #file + var line: UInt = #line + } + func makeCase( + _ value: any Encodable, + _ expectedNode: URINode, + file: StaticString = #file, + line: UInt = #line + ) + -> Case + { + .init(value: value, expectedNode: expectedNode, file: file, line: line) + } + + struct SimpleStruct: Encodable { + var foo: String + var bar: Int? + } + + struct NestedStruct: Encodable { + var simple: SimpleStruct + } + + let cases: [Case] = [ + + // An empty string. + makeCase( + "", + .primitive(.string("")) + ), + + // A string with a space. + makeCase( + "Hello World", + .primitive(.string("Hello World")) + ), + + // An integer. + makeCase( + 1234, + .primitive(.integer(1234)) + ), + + // A float. + makeCase( + 12.34, + .primitive(.double(12.34)) + ), + + // A bool. + makeCase( + true, + .primitive(.bool(true)) + ), + + // A simple array. + makeCase( + ["a", "b", "c"], + .array([ + .primitive(.string("a")), + .primitive(.string("b")), + .primitive(.string("c")), + ]) + ), + + // A nested array. + makeCase( + [["a"], ["b", "c"]], + .array([ + .array([ + .primitive(.string("a")) + ]), + .array([ + .primitive(.string("b")), + .primitive(.string("c")), + ]), + ]) + ), + + // A struct. + makeCase( + SimpleStruct(foo: "bar"), + .dictionary([ + "foo": .primitive(.string("bar")) + ]) + ), + + // A nested struct. + makeCase( + NestedStruct(simple: SimpleStruct(foo: "bar")), + .dictionary([ + "simple": .dictionary([ + "foo": .primitive(.string("bar")) + ]) + ]) + ), + + // An array of structs. + makeCase( + [ + SimpleStruct(foo: "bar"), + SimpleStruct(foo: "baz"), + ], + .array([ + .dictionary([ + "foo": .primitive(.string("bar")) + ]), + .dictionary([ + "foo": .primitive(.string("baz")) + ]), + ]) + ), + + // An array of arrays of structs. + makeCase( + [ + [ + SimpleStruct(foo: "bar") + ], + [ + SimpleStruct(foo: "baz") + ], + ], + .array([ + .array([ + .dictionary([ + "foo": .primitive(.string("bar")) + ]) + ]), + .array([ + .dictionary([ + "foo": .primitive(.string("baz")) + ]) + ]), + ]) + ), + + // A simple dictionary. + makeCase( + ["one": 1, "two": 2], + .dictionary([ + "one": .primitive(.integer(1)), + "two": .primitive(.integer(2)), + ]) + ), + + // A nested dictionary. + makeCase( + [ + "A": ["one": 1, "two": 2], + "B": ["three": 3, "four": 4], + ], + .dictionary([ + "A": .dictionary([ + "one": .primitive(.integer(1)), + "two": .primitive(.integer(2)), + ]), + "B": .dictionary([ + "three": .primitive(.integer(3)), + "four": .primitive(.integer(4)), + ]), + ]) + ), + + // A dictionary of structs. + makeCase( + [ + "barkey": SimpleStruct(foo: "bar"), + "bazkey": SimpleStruct(foo: "baz"), + ], + .dictionary([ + "barkey": .dictionary([ + "foo": .primitive(.string("bar")) + ]), + "bazkey": .dictionary([ + "foo": .primitive(.string("baz")) + ]), + ]) + ), + + // An dictionary of dictionaries of structs. + makeCase( + [ + "outBar": + [ + "inBar": SimpleStruct(foo: "bar") + ], + "outBaz": [ + "inBaz": SimpleStruct(foo: "baz") + ], + ], + .dictionary([ + "outBar": .dictionary([ + "inBar": .dictionary([ + "foo": .primitive(.string("bar")) + ]) + ]), + "outBaz": .dictionary([ + "inBaz": .dictionary([ + "foo": .primitive(.string("baz")) + ]) + ]), + ]) + ), + ] + let translator = URITranslator() + for testCase in cases { + let translatedNode = try translator.translateValue(testCase.value) + XCTAssertEqual( + translatedNode, + testCase.expectedNode, + file: testCase.file, + line: testCase.line + ) + } + } +} From dbbe26124ed455e46da9156aaff78b6ceebb876d Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 22 Aug 2023 13:38:13 +0200 Subject: [PATCH 02/17] Refactored the URIEncoder a bunch, only support top level values for now, no nested containers, as per the RFCs --- .../OpenAPIRuntime/Coding/URISerializer.swift | 238 -------------- .../Encoding/URIEncodableNode.swift} | 4 +- .../Encoding}/URIEncoder.swift | 45 ++- .../URIValueToNodeEncoder+Keyed.swift} | 8 +- .../URIValueToNodeEncoder+Single.swift} | 10 +- .../URIValueToNodeEncoder+Unkeyed.swift} | 10 +- .../Encoding/URIValueToNodeEncoder.swift} | 24 +- .../URICoder/Parsing/URIParsedNode.swift | 25 ++ .../URICoder/Parsing/URIParser.swift | 221 +++++++++++++ .../Serialization/URISerializer.swift | 309 ++++++++++++++++++ .../Coding/Test_URISerializer.swift | 159 --------- .../Test_URIEncoder.swift | 7 +- .../Test_URIFormStyleSerializer.swift | 145 ++++++++ .../URICoder/Test_URIParser.swift | 160 +++++++++ .../Test_URIValueToNodeEncoder.swift} | 8 +- 15 files changed, 917 insertions(+), 456 deletions(-) delete mode 100644 Sources/OpenAPIRuntime/Coding/URISerializer.swift rename Sources/OpenAPIRuntime/{Coding/URINode.swift => URICoder/Encoding/URIEncodableNode.swift} (97%) rename Sources/OpenAPIRuntime/{Coding => URICoder/Encoding}/URIEncoder.swift (63%) rename Sources/OpenAPIRuntime/{Coding/URITranslator+Keyed.swift => URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift} (94%) rename Sources/OpenAPIRuntime/{Coding/URITranslator+Single.swift => URICoder/Encoding/URIValueToNodeEncoder+Single.swift} (91%) rename Sources/OpenAPIRuntime/{Coding/URITranslator+Unkeyed.swift => URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift} (93%) rename Sources/OpenAPIRuntime/{Coding/URITranslator.swift => URICoder/Encoding/URIValueToNodeEncoder.swift} (86%) create mode 100644 Sources/OpenAPIRuntime/URICoder/Parsing/URIParsedNode.swift create mode 100644 Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift create mode 100644 Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift delete mode 100644 Tests/OpenAPIRuntimeTests/Coding/Test_URISerializer.swift rename Tests/OpenAPIRuntimeTests/{Coding => URICoder}/Test_URIEncoder.swift (79%) create mode 100644 Tests/OpenAPIRuntimeTests/URICoder/Test_URIFormStyleSerializer.swift create mode 100644 Tests/OpenAPIRuntimeTests/URICoder/Test_URIParser.swift rename Tests/OpenAPIRuntimeTests/{Coding/Test_URITranslator.swift => URICoder/Test_URIValueToNodeEncoder.swift} (97%) diff --git a/Sources/OpenAPIRuntime/Coding/URISerializer.swift b/Sources/OpenAPIRuntime/Coding/URISerializer.swift deleted file mode 100644 index cd87d5b4..00000000 --- a/Sources/OpenAPIRuntime/Coding/URISerializer.swift +++ /dev/null @@ -1,238 +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 - -/// Converts an URINode value into a string. -struct URISerializer: Sendable { - - struct Configuration { - // TODO: Add a few prebuilt options. - struct KeyEncoding { - - // TODO: Hide to avoid adding an enum case being breaking. - enum DictionaryKeyEncoding { - - /// Put the key between square brackets. - /// - /// Example: foo[one]=1&foo[two]=2&foo[three]=3 - case bracketsWithKey - - /// Concatenate components using a period. - /// - /// Example: foo.one=1&foo.two=2&foo.three=3 - case concatenatedByPeriods - } - var dictionary: DictionaryKeyEncoding = .bracketsWithKey - - // TODO: Hide to avoid adding an enum case being breaking. - enum ArrayIndexEncoding { - - /// None, just repeat the container's key. - /// - /// Example: foo=a&foo=b&foo=c. - case none - - /// Repeat empty brackets after the container's key. - /// - /// Example: foo[]=a&foo[]=b&foo[]=c. - case emptyBrackets - - /// Put the index in square brackets after the container's key. - /// - /// Example: foo[0]=a&foo[1]=b&foo[2]=c. - case bracketsWithIndex - } - var array: ArrayIndexEncoding = .bracketsWithIndex - } - var keyEncoding: KeyEncoding = .init() - - // struct ValueEncoding { - // var unescapedCharacterSet: CharacterSet - // - // // TODO: Add a few prebuilt options. - // // static var urlForm: Self { - // // - // // } - // } - // var valueEncoding: ValueEncoding - } - - let configuration: Configuration - - private var data: String - - init(configuration: Configuration = .init()) { - self.configuration = configuration - self.data = "" - } -} - -extension URISerializer { - - mutating func writeNode( - _ value: URINode, - forKey keyComponent: KeyComponent - ) throws -> String { - defer { - data.removeAll(keepingCapacity: true) - } - try serializeNode(value, forKey: [keyComponent]) - return data - } -} - -extension URISerializer { - - enum KeyComponent { - case index(Int) - case key(String) - } - - enum SerializationError: Swift.Error { - case topLevelKeyMustBeString - } - - typealias Key = [KeyComponent] - - private func computeSafeKey(_ unsafeKey: String) -> String { - // TODO: Escape the key here - unsafeKey - } - - private func computeSafeValue(_ unsafeValue: String) -> String { - // TODO: Escape the value here - unsafeValue - } - - private func stringifiedKeyComponent(_ keyComponent: KeyComponent) -> String { - // TODO: This will differ based on configuration. - switch keyComponent { - case .index(let int): - switch configuration.keyEncoding.array { - case .none: - return "" - case .emptyBrackets: - return "[]" - case .bracketsWithIndex: - return "[\(int)]" - } - case .key(let string): - let safeKey = computeSafeKey(string) - switch configuration.keyEncoding.dictionary { - case .bracketsWithKey: - return "[\(safeKey)]" - case .concatenatedByPeriods: - return ".\(safeKey)" - } - } - } - - private func stringifiedKey(_ key: Key) throws -> String { - // The root key is handled separately. - guard !key.isEmpty else { - return "" - } - let topLevelKey = key[0] - guard case .key(let string) = topLevelKey else { - throw SerializationError.topLevelKeyMustBeString - } - let safeTopLevelKey = computeSafeKey(string) - return - ([safeTopLevelKey] - + key - .dropFirst() - .map(stringifiedKeyComponent)) - .joined() - } - - private mutating func serializeNode(_ value: URINode, forKey key: Key) throws { - switch value { - case .unset: - // TODO: Is there a distinction here between `a=` and `a`, in other - // words between an empty string value and a nil value? - data.append(try stringifiedKey(key)) - data.append("=") - case .primitive(let primitive): - try serializePrimitiveValue(primitive, forKey: key) - case .array(let array): - try serializeArray(array, forKey: key) - case .dictionary(let dictionary): - try serializeDictionary(dictionary, forKey: key) - } - } - - private mutating func serializePrimitiveValue( - _ value: URINode.Primitive, - forKey key: Key - ) throws { - let stringValue: String - switch value { - case .bool(let bool): - stringValue = bool.description - case .string(let string): - stringValue = computeSafeValue(string) - case .integer(let int): - stringValue = int.description - case .double(let double): - stringValue = double.description - } - data.append(try stringifiedKey(key)) - data.append("=") - data.append(stringValue) - } - - private mutating func serializeArray( - _ array: [URINode], - forKey key: Key - ) throws { - try serializeTuples( - array.enumerated() - .map { index, element in - (key + [.index(index)], element) - } - ) - } - - private mutating func serializeDictionary( - _ dictionary: [String: URINode], - forKey key: Key - ) throws { - try serializeTuples( - dictionary - .sorted { a, b in - a.key.localizedCaseInsensitiveCompare(b.key) - == .orderedAscending - } - .map { elementKey, element in - (key + [.key(elementKey)], element) - } - ) - } - - private mutating func serializeTuples( - _ items: [(Key, URINode)] - ) throws { - guard !items.isEmpty else { - return - } - for (key, element) in items.dropLast() { - try serializeNode(element, forKey: key) - data.append("&") - } - if let (key, element) = items.last { - try serializeNode(element, forKey: key) - } - } -} diff --git a/Sources/OpenAPIRuntime/Coding/URINode.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncodableNode.swift similarity index 97% rename from Sources/OpenAPIRuntime/Coding/URINode.swift rename to Sources/OpenAPIRuntime/URICoder/Encoding/URIEncodableNode.swift index d780cfc0..8d9ff1b0 100644 --- a/Sources/OpenAPIRuntime/Coding/URINode.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncodableNode.swift @@ -14,7 +14,7 @@ import Foundation -enum URINode: Equatable { +enum URIEncodableNode: Equatable { case unset case primitive(Primitive) @@ -29,7 +29,7 @@ enum URINode: Equatable { } } -extension URINode { +extension URIEncodableNode { enum InsertionError: Swift.Error { case settingPrimitiveValueAgain diff --git a/Sources/OpenAPIRuntime/Coding/URIEncoder.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift similarity index 63% rename from Sources/OpenAPIRuntime/Coding/URIEncoder.swift rename to Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift index 7f7ffdf4..d528e05f 100644 --- a/Sources/OpenAPIRuntime/Coding/URIEncoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift @@ -19,19 +19,28 @@ import Foundation /// the configuration. /// /// - [OpenAPI 3.0.3 styles](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#style-examples) -public struct URIEncoder: Sendable { - public init() {} +struct URIEncoder: Sendable { + + private let serializer: URISerializer + + init(serializer: URISerializer) { + self.serializer = serializer + } + + init(serializerConfiguration: URISerializer.Configuration) { + self.init(serializer: .init(configuration: serializerConfiguration)) + } } extension URIEncoder { - public enum KeyComponent { - case index(Int) - case key(String) - } - /// Attempt to encode an object into an URI string. /// + /// Under the hood, URIEncoder first encodes the Encodable type + /// into a URIEncodableNode using URIValueToNodeEncoder, and then + /// URISerializer encodes the URIEncodableNode into a string based + /// on the configured behavior. + /// /// - Parameters: /// - value: The value to encode. /// - key: The key for which to encode the value. Can be an empty key, @@ -39,26 +48,12 @@ extension URIEncoder { /// - Returns: The URI string. public func encode( _ value: some Encodable, - forKey key: KeyComponent + forKey key: String ) throws -> String { - - // Under the hood, URIEncoder first encodes the Encodable type - // into a URINode using URITranslator, and then - // URISerializer encodes the URINode into a string based - // on the configured behavior. - - let translator = URITranslator() - var serializer = URISerializer() + let translator = URIValueToNodeEncoder() let node = try translator.translateValue(value) - - let convertedKey: URISerializer.KeyComponent - switch key { - case .index(let int): - convertedKey = .index(int) - case .key(let string): - convertedKey = .key(string) - } - let encodedString = try serializer.writeNode(node, forKey: convertedKey) + var serializer = serializer + let encodedString = try serializer.serializeNode(node, forKey: key) return encodedString } } diff --git a/Sources/OpenAPIRuntime/Coding/URITranslator+Keyed.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift similarity index 94% rename from Sources/OpenAPIRuntime/Coding/URITranslator+Keyed.swift rename to Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift index 04a46a88..f22da36e 100644 --- a/Sources/OpenAPIRuntime/Coding/URITranslator+Keyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift @@ -15,15 +15,15 @@ import Foundation struct URIKeyedEncodingContainer { - let translator: URITranslator + let translator: URIValueToNodeEncoder } extension URIKeyedEncodingContainer { - private func _insertValue(_ node: URINode, atKey key: Key) throws { + private func _insertValue(_ node: URIEncodableNode, atKey key: Key) throws { try translator.currentStackEntry.storage.insert(node, atKey: key) } - private func _insertValue(_ node: URINode.Primitive, atKey key: Key) throws { + private func _insertValue(_ node: URIEncodableNode.Primitive, atKey key: Key) throws { try _insertValue(.primitive(node), atKey: key) } @@ -39,7 +39,7 @@ extension URIKeyedEncodingContainer { atKey key: Key ) throws { guard let validatedValue = Int(exactly: value) else { - throw URITranslator.GeneralError.integerOutOfRange + throw URIValueToNodeEncoder.GeneralError.integerOutOfRange } try _insertValue(.integer(validatedValue), atKey: key) } diff --git a/Sources/OpenAPIRuntime/Coding/URITranslator+Single.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift similarity index 91% rename from Sources/OpenAPIRuntime/Coding/URITranslator+Single.swift rename to Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift index b1eb2d5c..3596d080 100644 --- a/Sources/OpenAPIRuntime/Coding/URITranslator+Single.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift @@ -15,11 +15,11 @@ import Foundation struct URISingleValueEncodingContainer: SingleValueEncodingContainer { - let translator: URITranslator + let translator: URIValueToNodeEncoder } extension URISingleValueEncodingContainer { - private func _setValue(_ node: URINode.Primitive) throws { + private func _setValue(_ node: URIEncodableNode.Primitive) throws { try translator.currentStackEntry.storage.set(node) } @@ -29,7 +29,7 @@ extension URISingleValueEncodingContainer { private func _setFixedWidthInteger(_ value: some FixedWidthInteger) throws { guard let validatedValue = Int(exactly: value) else { - throw URITranslator.GeneralError.integerOutOfRange + throw URIValueToNodeEncoder.GeneralError.integerOutOfRange } try _setValue(.integer(validatedValue)) } @@ -42,7 +42,7 @@ extension URISingleValueEncodingContainer { } func encodeNil() throws { - throw URITranslator.GeneralError.nilNotSupported + throw URIValueToNodeEncoder.GeneralError.nilNotSupported } func encode(_ value: Bool) throws { @@ -132,7 +132,7 @@ extension URISingleValueEncodingContainer { case let value as Bool: try encode(value) default: - throw URITranslator.GeneralError.nestedValueInSingleValueContainer + throw URIValueToNodeEncoder.GeneralError.nestedValueInSingleValueContainer } } } diff --git a/Sources/OpenAPIRuntime/Coding/URITranslator+Unkeyed.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift similarity index 93% rename from Sources/OpenAPIRuntime/Coding/URITranslator+Unkeyed.swift rename to Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift index 922a7878..395ecc6e 100644 --- a/Sources/OpenAPIRuntime/Coding/URITranslator+Unkeyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift @@ -15,15 +15,15 @@ import Foundation struct URIUnkeyedEncodingContainer { - let translator: URITranslator + let translator: URIValueToNodeEncoder } extension URIUnkeyedEncodingContainer { - private func _appendValue(_ node: URINode) throws { + private func _appendValue(_ node: URIEncodableNode) throws { try translator.currentStackEntry.storage.append(node) } - private func _appendValue(_ node: URINode.Primitive) throws { + private func _appendValue(_ node: URIEncodableNode.Primitive) throws { try _appendValue(.primitive(node)) } @@ -33,7 +33,7 @@ extension URIUnkeyedEncodingContainer { private func _appendFixedWidthInteger(_ value: some FixedWidthInteger) throws { guard let validatedValue = Int(exactly: value) else { - throw URITranslator.GeneralError.integerOutOfRange + throw URIValueToNodeEncoder.GeneralError.integerOutOfRange } try _appendValue(.integer(validatedValue)) } @@ -71,7 +71,7 @@ extension URIUnkeyedEncodingContainer: UnkeyedEncodingContainer { } func encodeNil() throws { - throw URITranslator.GeneralError.nilNotSupported + throw URIValueToNodeEncoder.GeneralError.nilNotSupported } func encode(_ value: Bool) throws { diff --git a/Sources/OpenAPIRuntime/Coding/URITranslator.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift similarity index 86% rename from Sources/OpenAPIRuntime/Coding/URITranslator.swift rename to Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift index 2895cdb1..0beb36bc 100644 --- a/Sources/OpenAPIRuntime/Coding/URITranslator.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift @@ -14,8 +14,8 @@ import Foundation -/// Converts an Encodable type into a URINode. -final class URITranslator { +/// Converts an Encodable type into a URIEncodableNode. +final class URIValueToNodeEncoder { /// The coding key. struct _CodingKey: CodingKey { @@ -43,7 +43,7 @@ final class URITranslator { /// This is used to keep track of where we are in the encode. struct CodingStackEntry { var key: _CodingKey - var storage: URINode + var storage: URIEncodableNode } enum GeneralError: Swift.Error { @@ -64,19 +64,21 @@ final class URITranslator { ) } - func translateValue(_ value: some Encodable) throws -> URINode { + func translateValue(_ value: some Encodable) throws -> URIEncodableNode { + defer { + _codingPath = [] + currentStackEntry = CodingStackEntry( + key: .init(stringValue: ""), + storage: .unset + ) + } try value.encode(to: self) let encodedValue = currentStackEntry.storage - _codingPath = [] - currentStackEntry = CodingStackEntry( - key: .init(stringValue: ""), - storage: .unset - ) return encodedValue } } -extension URITranslator: Encoder { +extension URIValueToNodeEncoder: Encoder { var codingPath: [any CodingKey] { // The coding path meaningful to the types conforming to Codable. // 1. Omit the root coding path. @@ -92,7 +94,7 @@ extension URITranslator: Encoder { [:] } - func push(key: _CodingKey, newStorage: URINode) { + func push(key: _CodingKey, newStorage: URIEncodableNode) { _codingPath.append(currentStackEntry) currentStackEntry = .init(key: key, storage: newStorage) } diff --git a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParsedNode.swift b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParsedNode.swift new file mode 100644 index 00000000..ced77008 --- /dev/null +++ b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParsedNode.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +enum URIParsedNode: Equatable { + case unset + case primitive(String) + case array([Self]) + case dictionary([String: Self]) + + typealias Root = [String: Self] +} + diff --git a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift new file mode 100644 index 00000000..dd063ecd --- /dev/null +++ b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift @@ -0,0 +1,221 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +/// Converts an URINode value into a string. +struct URIParser: Sendable { + + private typealias Raw = String.SubSequence + private var data: Raw + + init(data: String) { + self.data = data[...] + } +} + +extension URIParser { + +// mutating func parseRoot() throws -> URIParsedNode.Root { +// } +} + +fileprivate enum ParsingError: Swift.Error { + case unexpectadlyFoundEnd + case malformedBracketedString +} + +extension String.SubSequence { + + mutating func parseUntilOrEnd(character: Character) -> Self { + let startIndex = startIndex + var index = startIndex + while index != endIndex && self[index] != character { + formIndex(after: &index) + } + let parsed = self[startIndex.. Self { + let parsed = parseUntilOrEnd(character: character) + guard !isEmpty else { + throw ParsingError.unexpectadlyFoundEnd + } + return parsed + } + + mutating func parseUpTo(character: Character) throws -> Self { + let startIndex = startIndex + var index = startIndex + while index != endIndex && self[index] != character { + formIndex(after: &index) + } + if index == endIndex { + throw ParsingError.unexpectadlyFoundEnd + } + let parsed = self[startIndex...index] + formIndex(after: &index) + self = self[index...] + return parsed + } + + mutating func stripSquareBrackets() throws { + guard let first, let last, first == "[" && last == "]" else { + throw ParsingError.malformedBracketedString + } + let newStart = index(after: startIndex) + let newEnd = index(before: endIndex) + self = self[newStart.. String { + // TODO: Unescape the value here + escapedValue + } + + private func parsedKey(_ rawKey: Raw) throws -> Key { + // TODO: This will differ based on configuration. + + var rawKey = rawKey + + // First subsequence until "[" is found is the first component. + let rootKey = rawKey.parseUntilOrEnd(character: "[") + + var childComponents: [KeyComponent] = [] + while !rawKey.isEmpty { + var parsedKey = try rawKey.parseUpTo(character: "]") + try parsedKey.stripSquareBrackets() + childComponents.append(.init(rawValue: parsedKey)) + } + return [.key(rootKey)] + childComponents + } + +// private func stringifiedKey(_ key: Key) throws -> String { +// // The root key is handled separately. +// guard !key.isEmpty else { +// return "" +// } +// let topLevelKey = key[0] +// guard case .key(let string) = topLevelKey else { +// throw SerializationError.topLevelKeyMustBeString +// } +// let safeTopLevelKey = computeSafeKey(string) +// return +// ([safeTopLevelKey] +// + key +// .dropFirst() +// .map(stringifiedKeyComponent)) +// .joined() +// } +// +// private mutating func serializeNode(_ value: URIEncodableNode, forKey key: Key) throws { +// switch value { +// case .unset: +// // TODO: Is there a distinction here between `a=` and `a`, in other +// // words between an empty string value and a nil value? +// data.append(try stringifiedKey(key)) +// data.append("=") +// case .primitive(let primitive): +// try serializePrimitiveValue(primitive, forKey: key) +// case .array(let array): +// try serializeArray(array, forKey: key) +// case .dictionary(let dictionary): +// try serializeDictionary(dictionary, forKey: key) +// } +// } +// +// private mutating func serializePrimitiveValue( +// _ value: URIEncodableNode.Primitive, +// forKey key: Key +// ) throws { +// let stringValue: String +// switch value { +// case .bool(let bool): +// stringValue = bool.description +// case .string(let string): +// stringValue = computeSafeValue(string) +// case .integer(let int): +// stringValue = int.description +// case .double(let double): +// stringValue = double.description +// } +// data.append(try stringifiedKey(key)) +// data.append("=") +// data.append(stringValue) +// } +// +// private mutating func serializeArray( +// _ array: [URIEncodableNode], +// forKey key: Key +// ) throws { +// try serializeTuples( +// array.enumerated() +// .map { index, element in +// (key + [.index(index)], element) +// } +// ) +// } +// +// private mutating func serializeDictionary( +// _ dictionary: [String: URIEncodableNode], +// forKey key: Key +// ) throws { +// try serializeTuples( +// dictionary +// .sorted { a, b in +// a.key.localizedCaseInsensitiveCompare(b.key) +// == .orderedAscending +// } +// .map { elementKey, element in +// (key + [.key(elementKey[...])], element) +// } +// ) +// } +// +// private mutating func serializeTuples( +// _ items: [(Key, URIEncodableNode)] +// ) throws { +// guard !items.isEmpty else { +// return +// } +// for (key, element) in items.dropLast() { +// try serializeNode(element, forKey: key) +// data.append("&") +// } +// if let (key, element) = items.last { +// try serializeNode(element, forKey: key) +// } +// } +} diff --git a/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift b/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift new file mode 100644 index 00000000..45c50780 --- /dev/null +++ b/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift @@ -0,0 +1,309 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +/// Implements form-style query expansion from RFC 6570. +/// +/// [RFC 6570 - Form-style query expansion.](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.8) +/// +/// | Example Template | Expansion | +/// | ---------------- | ----------------------------------| +/// | `{?who}` | `?who=fred` | +/// | `{?half}` | `?half=50%25` | +/// | `{?x,y}` | `?x=1024&y=768` | +/// | `{?x,y,empty}` | `?x=1024&y=768&empty=` | +/// | `{?x,y,undef}` | `?x=1024&y=768` | +/// | `{?list}` | `?list=red,green,blue` | +/// | `{?list\*}` | `?list=red&list=green&list=blue` | +/// | `{?keys}` | `?keys=semi,%3B,dot,.,comma,%2C` | +/// | `{?keys\*}` | `?semi=%3B&dot=.&comma=%2C` | +struct URISerializer { + + struct Configuration { + + // TODO: Wrap in a struct. + enum Style { + case simple + case form + } + + var style: Style + var explode: Bool + var spaceEscapingCharacter: String + + private init(style: Style, explode: Bool, spaceEscapingCharacter: String) { + self.style = style + self.explode = explode + self.spaceEscapingCharacter = spaceEscapingCharacter + } + + static let formExplode: Self = .init( + style: .form, + explode: true, + spaceEscapingCharacter: "%20" + ) + + static let formUnexplode: Self = .init( + style: .form, + explode: false, + spaceEscapingCharacter: "%20" + ) + + static let simpleExplode: Self = .init( + style: .simple, + explode: true, + spaceEscapingCharacter: "%20" + ) + + static let simpleUnexplode: Self = .init( + style: .simple, + explode: false, + spaceEscapingCharacter: "%20" + ) + + static let formDataExplode: Self = .init( + style: .form, + explode: true, + spaceEscapingCharacter: "+" + ) + + static let formDataUnexplode: Self = .init( + style: .form, + explode: false, + spaceEscapingCharacter: "+" + ) + } + + private let configuration: Configuration + private var data: String + + init(configuration: Configuration) { + self.configuration = configuration + self.data = "" + } +} + +extension CharacterSet { + fileprivate static let unreservedSymbols: CharacterSet = .init(charactersIn: "-._~") + fileprivate static let unreserved: CharacterSet = .alphanumerics.union(unreservedSymbols) + fileprivate static let space: CharacterSet = .init(charactersIn: " ") + fileprivate static let unreservedAndSpace: CharacterSet = .unreserved.union(space) +} + +extension URISerializer { + + private func computeSafeString(_ unsafeString: String) -> String { + // The space character needs to be encoded based on the config, + // so first allow it to be unescaped, and then we'll do a second + // pass and only encode the space based on the config. + let partiallyEncoded = unsafeString.addingPercentEncoding( + withAllowedCharacters: .unreservedAndSpace + ) ?? "" + let fullyEncoded = partiallyEncoded.replacingOccurrences( + of: " ", + with: configuration.spaceEscapingCharacter + ) + return fullyEncoded + } +} + +extension URISerializer { + + enum SerializationError: Swift.Error { + case nestedContainersNotSupported + } + + mutating func serializeNode( + _ value: URIEncodableNode, + forKey key: String + ) throws -> String { + defer { + data.removeAll(keepingCapacity: true) + } + try serializeAnyNode(value, forKey: key) + return data + } + + private func stringifiedKey(_ key: String) throws -> String { + // The root key is handled separately. + guard !key.isEmpty else { + return "" + } + let safeTopLevelKey = computeSafeString(key) + return safeTopLevelKey + } + + private mutating func serializeAnyNode(_ value: URIEncodableNode, forKey key: String) throws { + func unwrapPrimitiveValue(_ node: URIEncodableNode) throws -> URIEncodableNode.Primitive { + guard case let .primitive(primitive) = node else { + throw SerializationError.nestedContainersNotSupported + } + return primitive + } + switch value { + case .unset: + // Nothing to serialize. + break + case .primitive(let primitive): + try serializePrimitiveKeyValuePair( + primitive, + forKey: key, + + // TODO: Seems strange to assume this, is the API wrong? + separator: "=" + ) + case .array(let array): + try serializeArray( + array.map(unwrapPrimitiveValue), + forKey: key + ) + case .dictionary(let dictionary): + try serializeDictionary( + dictionary.mapValues(unwrapPrimitiveValue), + forKey: key + ) + } + } + + private mutating func serializePrimitiveValue( + _ value: URIEncodableNode.Primitive + ) throws { + let stringValue: String + switch value { + case .bool(let bool): + stringValue = bool.description + case .string(let string): + stringValue = computeSafeString(string) + case .integer(let int): + stringValue = int.description + case .double(let double): + stringValue = double.description + } + data.append(stringValue) + } + + private mutating func serializePrimitiveKeyValuePair( + _ value: URIEncodableNode.Primitive, + forKey key: String, + separator: String + ) throws { + data.append(try stringifiedKey(key)) + data.append(separator) + try serializePrimitiveValue(value) + } + + private mutating func serializeArray( + _ array: [URIEncodableNode.Primitive], + forKey key: String + ) throws { + guard !array.isEmpty else { + return + } + let style = configuration.style + let explode = configuration.explode + + let keyAndValueSeparator: String? + let pairSeparator: String + switch (style, explode) { + case (.form, true): + keyAndValueSeparator = "=" + pairSeparator = "&" + case (.form, false): + keyAndValueSeparator = nil + pairSeparator = "," + case (.simple, _): + keyAndValueSeparator = nil + pairSeparator = "," + } + func serializeNext(_ element: URIEncodableNode.Primitive) throws { + if let keyAndValueSeparator { + try serializePrimitiveKeyValuePair( + element, + forKey: key, + separator: keyAndValueSeparator + ) + } else { + try serializePrimitiveValue(element) + } + } + if keyAndValueSeparator == nil { + data.append(try stringifiedKey(key)) + data.append("=") + } + for element in array.dropLast() { + try serializeNext(element) + data.append(pairSeparator) + } + if let element = array.last { + try serializeNext(element) + } + } + + private mutating func serializeDictionary( + _ dictionary: [String: URIEncodableNode.Primitive], + forKey key: String + ) throws { + guard !dictionary.isEmpty else { + return + } + let sortedDictionary = dictionary + .sorted { a, b in + a.key.localizedCaseInsensitiveCompare(b.key) + == .orderedAscending + } + + let style = configuration.style + let explode = configuration.explode + + let keyAndValueSeparator: String? + let pairSeparator: String + switch (style, explode) { + case (.form, true): + keyAndValueSeparator = "=" + pairSeparator = "&" + case (.form, false): + keyAndValueSeparator = "," + pairSeparator = "," + case (.simple, true): + keyAndValueSeparator = "=" + pairSeparator = "," + case (.simple, false): + keyAndValueSeparator = "," + pairSeparator = "," + } + func serializeNext(_ element: URIEncodableNode.Primitive, forKey elementKey: String) throws { + if let keyAndValueSeparator { + try serializePrimitiveKeyValuePair( + element, + forKey: elementKey, + separator: keyAndValueSeparator + ) + } else { + try serializePrimitiveValue(element) + } + } + if !explode { + data.append(try stringifiedKey(key)) + data.append("=") + } + for (elementKey, element) in sortedDictionary.dropLast() { + try serializeNext(element, forKey: elementKey) + data.append(pairSeparator) + } + if let (elementKey, element) = sortedDictionary.last { + try serializeNext(element, forKey: elementKey) + } + } +} diff --git a/Tests/OpenAPIRuntimeTests/Coding/Test_URISerializer.swift b/Tests/OpenAPIRuntimeTests/Coding/Test_URISerializer.swift deleted file mode 100644 index a7243667..00000000 --- a/Tests/OpenAPIRuntimeTests/Coding/Test_URISerializer.swift +++ /dev/null @@ -1,159 +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 XCTest -@testable import OpenAPIRuntime - -final class Test_URISerializer: Test_Runtime { - - func testTranslating() throws { - struct Case { - var value: URINode - var expectedString: String - var file: StaticString = #file - var line: UInt = #line - } - func makeCase(_ value: URINode, _ expectedString: String, file: StaticString = #file, line: UInt = #line) - -> Case - { - .init(value: value, expectedString: expectedString, file: file, line: line) - } - - let cases: [Case] = [ - makeCase(.primitive(.string("")), "root="), - makeCase(.primitive(.string("Hello World")), "root=Hello World"), - makeCase(.primitive(.integer(1234)), "root=1234"), - makeCase(.primitive(.double(12.34)), "root=12.34"), - makeCase(.primitive(.bool(true)), "root=true"), - makeCase( - .array([ - .primitive(.string("a")), - .primitive(.string("b")), - .primitive(.string("c")), - ]), - "root[0]=a&root[1]=b&root[2]=c" - ), - makeCase( - .array([ - .array([ - .primitive(.string("a")) - ]), - .array([ - .primitive(.string("b")), - .primitive(.string("c")), - ]), - ]), - "root[0][0]=a&root[1][0]=b&root[1][1]=c" - ), - makeCase( - .dictionary([ - "foo": .primitive(.string("bar")) - ]), - "root[foo]=bar" - ), - makeCase( - .dictionary([ - "simple": .dictionary([ - "foo": .primitive(.string("bar")) - ]) - ]), - "root[simple][foo]=bar" - ), - makeCase( - .array([ - .dictionary([ - "foo": .primitive(.string("bar")) - ]), - .dictionary([ - "foo": .primitive(.string("baz")) - ]), - ]), - "root[0][foo]=bar&root[1][foo]=baz" - ), - makeCase( - .array([ - .array([ - .dictionary([ - "foo": .primitive(.string("bar")) - ]) - ]), - .array([ - .dictionary([ - "foo": .primitive(.string("baz")) - ]) - ]), - ]), - "root[0][0][foo]=bar&root[1][0][foo]=baz" - ), - makeCase( - .dictionary([ - "one": .primitive(.integer(1)), - "two": .primitive(.integer(2)), - ]), - "root[one]=1&root[two]=2" - ), - makeCase( - .dictionary([ - "A": .dictionary([ - "one": .primitive(.integer(1)), - "two": .primitive(.integer(2)), - ]), - "B": .dictionary([ - "three": .primitive(.integer(3)), - "four": .primitive(.integer(4)), - ]), - ]), - "root[A][one]=1&root[A][two]=2&root[B][four]=4&root[B][three]=3" - ), - makeCase( - .dictionary([ - "barkey": .dictionary([ - "foo": .primitive(.string("bar")) - ]), - "bazkey": .dictionary([ - "foo": .primitive(.string("baz")) - ]), - ]), - "root[barkey][foo]=bar&root[bazkey][foo]=baz" - ), - makeCase( - .dictionary([ - "outBar": .dictionary([ - "inBar": .dictionary([ - "foo": .primitive(.string("bar")) - ]) - ]), - "outBaz": .dictionary([ - "inBaz": .dictionary([ - "foo": .primitive(.string("baz")) - ]) - ]), - ]), - "root[outBar][inBar][foo]=bar&root[outBaz][inBaz][foo]=baz" - ), - ] - var serializer = URISerializer() - for testCase in cases { - let encodedString = try serializer.writeNode( - testCase.value, - forKey: .key("root") - ) - XCTAssertEqual( - encodedString, - testCase.expectedString, - file: testCase.file, - line: testCase.line - ) - } - } -} diff --git a/Tests/OpenAPIRuntimeTests/Coding/Test_URIEncoder.swift b/Tests/OpenAPIRuntimeTests/URICoder/Test_URIEncoder.swift similarity index 79% rename from Tests/OpenAPIRuntimeTests/Coding/Test_URIEncoder.swift rename to Tests/OpenAPIRuntimeTests/URICoder/Test_URIEncoder.swift index 44d5ece6..4250db26 100644 --- a/Tests/OpenAPIRuntimeTests/Coding/Test_URIEncoder.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Test_URIEncoder.swift @@ -20,11 +20,12 @@ final class Test_URIEncoder: Test_Runtime { struct Foo: Encodable { var bar: String } - let encoder = URIEncoder() + let serializer = URISerializer(configuration: .formDataExplode) + let encoder = URIEncoder(serializer: serializer) let encodedString = try encoder.encode( Foo(bar: "hello world"), - forKey: .key("root") + forKey: "root" ) - XCTAssertEqual(encodedString, "root[bar]=hello world") + XCTAssertEqual(encodedString, "bar=hello+world") } } diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Test_URIFormStyleSerializer.swift b/Tests/OpenAPIRuntimeTests/URICoder/Test_URIFormStyleSerializer.swift new file mode 100644 index 00000000..33de58a4 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/URICoder/Test_URIFormStyleSerializer.swift @@ -0,0 +1,145 @@ +//===----------------------------------------------------------------------===// +// +// 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 XCTest +@testable import OpenAPIRuntime + +final class Test_URISerializer: Test_Runtime { + + let testedVariants: [URISerializer.Configuration] = [ + .formExplode, + .formUnexplode, + .simpleExplode, + .simpleUnexplode, + .formDataExplode, + .formDataUnexplode, + ] + + func testSerializing() throws { + let cases: [Case] = [ + makeCase( + .primitive(.string("")), + "root=" + ), + makeCase( + .primitive(.string("fred")), + "root=fred" + ), + makeCase( + .primitive(.integer(1234)), + "root=1234" + ), + makeCase( + .primitive(.double(12.34)), + "root=12.34" + ), + makeCase( + .primitive(.bool(true)), + "root=true" + ), + makeCase( + .primitive(.string("Hello World")), + [ + (.formExplode, "root=Hello%20World"), + (.simpleExplode, "root=Hello%20World"), + (.formDataExplode, "root=Hello+World"), + ] + ), + makeCase( + .primitive(.string("50%")), + "root=50%25" + ), + makeCase( + .array([ + .primitive(.string("red")), + .primitive(.string("green")), + .primitive(.string("blue")), + ]), + [ + (.formExplode, "root=red&root=green&root=blue"), + (.formUnexplode, "root=red,green,blue"), + (.simpleExplode, "root=red,green,blue"), + (.simpleUnexplode, "root=red,green,blue"), + (.formDataExplode, "root=red&root=green&root=blue"), + (.formDataUnexplode, "root=red,green,blue"), + ] + ), + makeCase( + .dictionary([ + "semi": .primitive(.string(";")), + "dot": .primitive(.string(".")), + "comma": .primitive(.string(",")) + ]), + [ + (.formExplode, "comma=%2C&dot=.&semi=%3B"), + (.formUnexplode, "root=comma,%2C,dot,.,semi,%3B"), + (.simpleExplode, "comma=%2C,dot=.,semi=%3B"), + (.simpleUnexplode, "root=comma,%2C,dot,.,semi,%3B"), + (.formDataExplode, "comma=%2C&dot=.&semi=%3B"), + (.formDataUnexplode, "root=comma,%2C,dot,.,semi,%3B"), + ] + ), + ] + for testCase in cases { + for (config, expectedString) in testCase.variants { + var serializer = URISerializer(configuration: config) + let encodedString = try serializer.serializeNode( + testCase.value, + forKey: "root" + ) + XCTAssertEqual( + encodedString, + expectedString, + "Failed for config: \(config)", + file: testCase.file, + line: testCase.line + ) + } + } + } +} + +extension Test_URISerializer { + struct Case { + var value: URIEncodableNode + var variants: [(URISerializer.Configuration, String)] + var file: StaticString = #file + var line: UInt = #line + } + func makeCase( + _ value: URIEncodableNode, + _ expectedString: String, + file: StaticString = #file, + line: UInt = #line + ) -> Case { + .init( + value: value, + variants: testedVariants.map { config in (config, expectedString) }, + file: file, + line: line + ) + } + func makeCase( + _ value: URIEncodableNode, + _ variants: [(URISerializer.Configuration, String)], + file: StaticString = #file, + line: UInt = #line + ) -> Case { + .init( + value: value, + variants: variants, + file: file, + line: line + ) + } +} diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Test_URIParser.swift b/Tests/OpenAPIRuntimeTests/URICoder/Test_URIParser.swift new file mode 100644 index 00000000..0d9d2c4f --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/URICoder/Test_URIParser.swift @@ -0,0 +1,160 @@ +//===----------------------------------------------------------------------===// +// +// 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 XCTest +@testable import OpenAPIRuntime + +final class Test_URIParser: Test_Runtime { + + + func testParsing() throws { + struct Case { + var value: String + var expectedNode: URIParsedNode + var file: StaticString = #file + var line: UInt = #line + } + func makeCase(_ value: String, _ expectedNode: URIParsedNode, file: StaticString = #file, line: UInt = #line) + -> Case + { + .init(value: value, expectedNode: expectedNode, file: file, line: line) + } + + let cases: [Case] = [ + makeCase("root=", .primitive("")), +// makeCase(.primitive(.string("Hello World")), "root=Hello World"), +// makeCase(.primitive(.integer(1234)), "root=1234"), +// makeCase(.primitive(.double(12.34)), "root=12.34"), +// makeCase(.primitive(.bool(true)), "root=true"), +// makeCase( +// .array([ +// .primitive(.string("a")), +// .primitive(.string("b")), +// .primitive(.string("c")), +// ]), +// "root[0]=a&root[1]=b&root[2]=c" +// ), +// makeCase( +// .array([ +// .array([ +// .primitive(.string("a")) +// ]), +// .array([ +// .primitive(.string("b")), +// .primitive(.string("c")), +// ]), +// ]), +// "root[0][0]=a&root[1][0]=b&root[1][1]=c" +// ), +// makeCase( +// .dictionary([ +// "foo": .primitive(.string("bar")) +// ]), +// "root[foo]=bar" +// ), +// makeCase( +// .dictionary([ +// "simple": .dictionary([ +// "foo": .primitive(.string("bar")) +// ]) +// ]), +// "root[simple][foo]=bar" +// ), +// makeCase( +// .array([ +// .dictionary([ +// "foo": .primitive(.string("bar")) +// ]), +// .dictionary([ +// "foo": .primitive(.string("baz")) +// ]), +// ]), +// "root[0][foo]=bar&root[1][foo]=baz" +// ), +// makeCase( +// .array([ +// .array([ +// .dictionary([ +// "foo": .primitive(.string("bar")) +// ]) +// ]), +// .array([ +// .dictionary([ +// "foo": .primitive(.string("baz")) +// ]) +// ]), +// ]), +// "root[0][0][foo]=bar&root[1][0][foo]=baz" +// ), +// makeCase( +// .dictionary([ +// "one": .primitive(.integer(1)), +// "two": .primitive(.integer(2)), +// ]), +// "root[one]=1&root[two]=2" +// ), +// makeCase( +// .dictionary([ +// "A": .dictionary([ +// "one": .primitive(.integer(1)), +// "two": .primitive(.integer(2)), +// ]), +// "B": .dictionary([ +// "three": .primitive(.integer(3)), +// "four": .primitive(.integer(4)), +// ]), +// ]), +// "root[A][one]=1&root[A][two]=2&root[B][four]=4&root[B][three]=3" +// ), +// makeCase( +// .dictionary([ +// "barkey": .dictionary([ +// "foo": .primitive(.string("bar")) +// ]), +// "bazkey": .dictionary([ +// "foo": .primitive(.string("baz")) +// ]), +// ]), +// "root[barkey][foo]=bar&root[bazkey][foo]=baz" +// ), +// makeCase( +// .dictionary([ +// "outBar": .dictionary([ +// "inBar": .dictionary([ +// "foo": .primitive(.string("bar")) +// ]) +// ]), +// "outBaz": .dictionary([ +// "inBaz": .dictionary([ +// "foo": .primitive(.string("baz")) +// ]) +// ]), +// ]), +// "root[outBar][inBar][foo]=bar&root[outBaz][inBaz][foo]=baz" +// ), + ] +// var serializer = URISerializer() +// for testCase in cases { +// let encodedString = try serializer.writeNode( +// testCase.value, +// forKey: .key("root") +// ) +// XCTAssertEqual( +// encodedString, +// testCase.expectedString, +// file: testCase.file, +// line: testCase.line +// ) +// } + } +} diff --git a/Tests/OpenAPIRuntimeTests/Coding/Test_URITranslator.swift b/Tests/OpenAPIRuntimeTests/URICoder/Test_URIValueToNodeEncoder.swift similarity index 97% rename from Tests/OpenAPIRuntimeTests/Coding/Test_URITranslator.swift rename to Tests/OpenAPIRuntimeTests/URICoder/Test_URIValueToNodeEncoder.swift index 82f7daf4..812ebead 100644 --- a/Tests/OpenAPIRuntimeTests/Coding/Test_URITranslator.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Test_URIValueToNodeEncoder.swift @@ -14,18 +14,18 @@ import XCTest @testable import OpenAPIRuntime -final class Test_URITranslator: Test_Runtime { +final class Test_URIValueToNodeEncoder: Test_Runtime { func testTranslating() throws { struct Case { var value: any Encodable - var expectedNode: URINode + var expectedNode: URIEncodableNode var file: StaticString = #file var line: UInt = #line } func makeCase( _ value: any Encodable, - _ expectedNode: URINode, + _ expectedNode: URIEncodableNode, file: StaticString = #file, line: UInt = #line ) @@ -225,7 +225,7 @@ final class Test_URITranslator: Test_Runtime { ]) ), ] - let translator = URITranslator() + let translator = URIValueToNodeEncoder() for testCase in cases { let translatedNode = try translator.translateValue(testCase.value) XCTAssertEqual( From 2c9912838cd06ecec5c6da741decc74f9aeb4c80 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 23 Aug 2023 10:14:04 +0200 Subject: [PATCH 03/17] Encoder, serializer, parser work, what remains is the decoder --- ...codableNode.swift => URIEncodedNode.swift} | 4 +- .../URICoder/Encoding/URIEncoder.swift | 2 +- .../URIValueToNodeEncoder+Keyed.swift | 4 +- .../URIValueToNodeEncoder+Single.swift | 2 +- .../URIValueToNodeEncoder+Unkeyed.swift | 4 +- .../Encoding/URIValueToNodeEncoder.swift | 6 +- .../URICoder/Parsing/URIParsedNode.swift | 10 +- .../URICoder/Parsing/URIParser.swift | 438 +++++++++++------- .../Serialization/URISerializer.swift | 163 +++---- .../URICoder/URICoderConfiguration.swift | 72 +++ .../{ => Encoding}/Test_URIEncoder.swift | 0 .../Test_URIValueToNodeEncoder.swift | 4 +- .../URICoder/Parsing/Test_URIParser.swift | 219 +++++++++ .../Serialization/Test_URISerializer.swift | 220 +++++++++ .../Test_URIFormStyleSerializer.swift | 145 ------ .../URICoder/Test_URIParser.swift | 160 ------- 16 files changed, 861 insertions(+), 592 deletions(-) rename Sources/OpenAPIRuntime/URICoder/Encoding/{URIEncodableNode.swift => URIEncodedNode.swift} (97%) create mode 100644 Sources/OpenAPIRuntime/URICoder/URICoderConfiguration.swift rename Tests/OpenAPIRuntimeTests/URICoder/{ => Encoding}/Test_URIEncoder.swift (100%) rename Tests/OpenAPIRuntimeTests/URICoder/{ => Encoding}/Test_URIValueToNodeEncoder.swift (98%) create mode 100644 Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift create mode 100644 Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift delete mode 100644 Tests/OpenAPIRuntimeTests/URICoder/Test_URIFormStyleSerializer.swift delete mode 100644 Tests/OpenAPIRuntimeTests/URICoder/Test_URIParser.swift diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncodableNode.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncodedNode.swift similarity index 97% rename from Sources/OpenAPIRuntime/URICoder/Encoding/URIEncodableNode.swift rename to Sources/OpenAPIRuntime/URICoder/Encoding/URIEncodedNode.swift index 8d9ff1b0..b3a5b954 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncodableNode.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncodedNode.swift @@ -14,7 +14,7 @@ import Foundation -enum URIEncodableNode: Equatable { +enum URIEncodedNode: Equatable { case unset case primitive(Primitive) @@ -29,7 +29,7 @@ enum URIEncodableNode: Equatable { } } -extension URIEncodableNode { +extension URIEncodedNode { enum InsertionError: Swift.Error { case settingPrimitiveValueAgain diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift index d528e05f..5ec99a09 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift @@ -27,7 +27,7 @@ struct URIEncoder: Sendable { self.serializer = serializer } - init(serializerConfiguration: URISerializer.Configuration) { + init(serializerConfiguration: URISerializationConfiguration) { self.init(serializer: .init(configuration: serializerConfiguration)) } } diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift index f22da36e..da9afb5c 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift @@ -19,11 +19,11 @@ struct URIKeyedEncodingContainer { } extension URIKeyedEncodingContainer { - private func _insertValue(_ node: URIEncodableNode, atKey key: Key) throws { + private func _insertValue(_ node: URIEncodedNode, atKey key: Key) throws { try translator.currentStackEntry.storage.insert(node, atKey: key) } - private func _insertValue(_ node: URIEncodableNode.Primitive, atKey key: Key) throws { + private func _insertValue(_ node: URIEncodedNode.Primitive, atKey key: Key) throws { try _insertValue(.primitive(node), atKey: key) } diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift index 3596d080..87b0439c 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift @@ -19,7 +19,7 @@ struct URISingleValueEncodingContainer: SingleValueEncodingContainer { } extension URISingleValueEncodingContainer { - private func _setValue(_ node: URIEncodableNode.Primitive) throws { + private func _setValue(_ node: URIEncodedNode.Primitive) throws { try translator.currentStackEntry.storage.set(node) } diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift index 395ecc6e..1407374d 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift @@ -19,11 +19,11 @@ struct URIUnkeyedEncodingContainer { } extension URIUnkeyedEncodingContainer { - private func _appendValue(_ node: URIEncodableNode) throws { + private func _appendValue(_ node: URIEncodedNode) throws { try translator.currentStackEntry.storage.append(node) } - private func _appendValue(_ node: URIEncodableNode.Primitive) throws { + private func _appendValue(_ node: URIEncodedNode.Primitive) throws { try _appendValue(.primitive(node)) } diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift index 0beb36bc..c6979448 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift @@ -43,7 +43,7 @@ final class URIValueToNodeEncoder { /// This is used to keep track of where we are in the encode. struct CodingStackEntry { var key: _CodingKey - var storage: URIEncodableNode + var storage: URIEncodedNode } enum GeneralError: Swift.Error { @@ -64,7 +64,7 @@ final class URIValueToNodeEncoder { ) } - func translateValue(_ value: some Encodable) throws -> URIEncodableNode { + func translateValue(_ value: some Encodable) throws -> URIEncodedNode { defer { _codingPath = [] currentStackEntry = CodingStackEntry( @@ -94,7 +94,7 @@ extension URIValueToNodeEncoder: Encoder { [:] } - func push(key: _CodingKey, newStorage: URIEncodableNode) { + func push(key: _CodingKey, newStorage: URIEncodedNode) { _codingPath.append(currentStackEntry) currentStackEntry = .init(key: key, storage: newStorage) } diff --git a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParsedNode.swift b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParsedNode.swift index ced77008..f15bf77a 100644 --- a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParsedNode.swift +++ b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParsedNode.swift @@ -14,12 +14,4 @@ import Foundation -enum URIParsedNode: Equatable { - case unset - case primitive(String) - case array([Self]) - case dictionary([String: Self]) - - typealias Root = [String: Self] -} - +typealias URIParsedNode = [String.SubSequence: [String.SubSequence]] diff --git a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift index dd063ecd..b385eb54 100644 --- a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift +++ b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift @@ -14,208 +14,308 @@ import Foundation -/// Converts an URINode value into a string. +/// Parses data from a subset of variable expansions from RFC 6570. +/// +/// [RFC 6570 - Form-style query expansion.](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.8) +/// +/// | Example Template | Expansion | +/// | ---------------- | ----------------------------------| +/// | `{?who}` | `?who=fred` | +/// | `{?half}` | `?half=50%25` | +/// | `{?x,y}` | `?x=1024&y=768` | +/// | `{?x,y,empty}` | `?x=1024&y=768&empty=` | +/// | `{?x,y,undef}` | `?x=1024&y=768` | +/// | `{?list}` | `?list=red,green,blue` | +/// | `{?list\*}` | `?list=red&list=green&list=blue` | +/// | `{?keys}` | `?keys=semi,%3B,dot,.,comma,%2C` | +/// | `{?keys\*}` | `?semi=%3B&dot=.&comma=%2C` | +/// +/// [RFC 6570 - Simple string expansion.](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.2) +/// +/// | Example Template | Expansion | +/// | ---------------- | ----------------------------------| +/// | `{hello}` | `Hello%20World%21` | +/// | `{half}` | `50%25` | +/// | `{x,y}` | `1024,768` | +/// | `{x,empty}` | `1024,` | +/// | `{x,undef}` | `1024` | +/// | `{list}` | `red,green,blue` | +/// | `{list\*}` | `red,green,blue` | +/// | `{keys}` | `semi,%3B,dot,.,comma,%2C` | +/// | `{keys\*}` | `semi=%3B,dot=.,comma=%2C` | struct URIParser: Sendable { + private let configuration: URISerializationConfiguration private typealias Raw = String.SubSequence private var data: Raw - init(data: String) { + init(configuration: URISerializationConfiguration, data: String) { + self.configuration = configuration self.data = data[...] } } -extension URIParser { - -// mutating func parseRoot() throws -> URIParsedNode.Root { -// } -} - fileprivate enum ParsingError: Swift.Error { - case unexpectadlyFoundEnd - case malformedBracketedString + case malformedKeyValuePair(String.SubSequence) } -extension String.SubSequence { - - mutating func parseUntilOrEnd(character: Character) -> Self { - let startIndex = startIndex - var index = startIndex - while index != endIndex && self[index] != character { - formIndex(after: &index) +// MARK: - Parser implementations + +extension URIParser { + 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 data.isEmpty { + return ["": [""]] + } + + 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() } - let parsed = self[startIndex.. Self { - let parsed = parseUntilOrEnd(character: character) - guard !isEmpty else { - throw ParsingError.unexpectadlyFoundEnd + + mutating func parseExplodedFormRoot() throws -> URIParsedNode { + try parseGenericRoot { data, appendPair in + let keyValueSeparator: Character = "=" + let pairSeparator: Character = "&" + + 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() + } + appendPair(key, [value]) + } } - return parsed } - mutating func parseUpTo(character: Character) throws -> Self { - let startIndex = startIndex - var index = startIndex - while index != endIndex && self[index] != character { - formIndex(after: &index) + mutating func parseUnexplodedFormRoot() throws -> URIParsedNode { + try parseGenericRoot { data, appendPair in + let keyValueSeparator: Character = "=" + let pairSeparator: Character = "&" + let valueSeparator: Character = "," + + 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 + } + } + 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) + } } - if index == endIndex { - throw ParsingError.unexpectadlyFoundEnd + } + + mutating func parseExplodedSimpleRoot() throws -> URIParsedNode { + try parseGenericRoot { data, appendPair in + let keyValueSeparator: Character = "=" + let pairSeparator: Character = "," + + 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 value. + key = .init() + value = firstValue + } + appendPair(key, [value]) + } } - let parsed = self[startIndex...index] - formIndex(after: &index) - self = self[index...] - return parsed } - - mutating func stripSquareBrackets() throws { - guard let first, let last, first == "[" && last == "]" else { - throw ParsingError.malformedBracketedString + + 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 + let pairSeparator: Character = "," + while !data.isEmpty { + let value = data.parseUpToCharacterOrEnd( + pairSeparator + ) + appendPair(.init(), [value]) + } } - let newStart = index(after: startIndex) - let newEnd = index(before: endIndex) - self = self[newStart.. 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 } + + private func unescapeValue(_ escapedValue: Raw) -> Raw { + Self.unescapeValue( + escapedValue, + spaceEscapingCharacter: configuration.spaceEscapingCharacter + ) + } + + private static func unescapeValue( + _ escapedValue: Raw, + spaceEscapingCharacter: String + ) -> Raw { + // The inverse of URISerializer.computeSafeString. + let partiallyDecoded = escapedValue.replacingOccurrences( + of: spaceEscapingCharacter, + with: " " + ) + return (partiallyDecoded.removingPercentEncoding ?? "")[...] + } +} - private typealias Key = [KeyComponent] +// MARK: - Substring utilities - private func unescapeValue(_ escapedValue: String) -> String { - // TODO: Unescape the value here - escapedValue +extension String.SubSequence { + + enum ParseUpToEitherCharacterResult { + case foundFirst + case foundSecondOrEnd } - private func parsedKey(_ rawKey: Raw) throws -> Key { - // TODO: This will differ based on configuration. - - var rawKey = rawKey - - // First subsequence until "[" is found is the first component. - let rootKey = rawKey.parseUntilOrEnd(character: "[") + mutating func parseUpToEitherCharacterOrEnd( + first: Character, + second: Character + ) -> (ParseUpToEitherCharacterResult, Self) { + let startIndex = startIndex + guard startIndex != endIndex else { + return (.foundSecondOrEnd, .init()) + } + var currentIndex = startIndex - var childComponents: [KeyComponent] = [] - while !rawKey.isEmpty { - var parsedKey = try rawKey.parseUpTo(character: "]") - try parsedKey.stripSquareBrackets() - childComponents.append(.init(rawValue: parsedKey)) + func finalize( + _ result: ParseUpToEitherCharacterResult + ) -> (ParseUpToEitherCharacterResult, Self) { + let parsed = self[startIndex.. String { -// // The root key is handled separately. -// guard !key.isEmpty else { -// return "" -// } -// let topLevelKey = key[0] -// guard case .key(let string) = topLevelKey else { -// throw SerializationError.topLevelKeyMustBeString -// } -// let safeTopLevelKey = computeSafeKey(string) -// return -// ([safeTopLevelKey] -// + key -// .dropFirst() -// .map(stringifiedKeyComponent)) -// .joined() -// } -// -// private mutating func serializeNode(_ value: URIEncodableNode, forKey key: Key) throws { -// switch value { -// case .unset: -// // TODO: Is there a distinction here between `a=` and `a`, in other -// // words between an empty string value and a nil value? -// data.append(try stringifiedKey(key)) -// data.append("=") -// case .primitive(let primitive): -// try serializePrimitiveValue(primitive, forKey: key) -// case .array(let array): -// try serializeArray(array, forKey: key) -// case .dictionary(let dictionary): -// try serializeDictionary(dictionary, forKey: key) -// } -// } -// -// private mutating func serializePrimitiveValue( -// _ value: URIEncodableNode.Primitive, -// forKey key: Key -// ) throws { -// let stringValue: String -// switch value { -// case .bool(let bool): -// stringValue = bool.description -// case .string(let string): -// stringValue = computeSafeValue(string) -// case .integer(let int): -// stringValue = int.description -// case .double(let double): -// stringValue = double.description -// } -// data.append(try stringifiedKey(key)) -// data.append("=") -// data.append(stringValue) -// } -// -// private mutating func serializeArray( -// _ array: [URIEncodableNode], -// forKey key: Key -// ) throws { -// try serializeTuples( -// array.enumerated() -// .map { index, element in -// (key + [.index(index)], element) -// } -// ) -// } -// -// private mutating func serializeDictionary( -// _ dictionary: [String: URIEncodableNode], -// forKey key: Key -// ) throws { -// try serializeTuples( -// dictionary -// .sorted { a, b in -// a.key.localizedCaseInsensitiveCompare(b.key) -// == .orderedAscending -// } -// .map { elementKey, element in -// (key + [.key(elementKey[...])], element) -// } -// ) -// } -// -// private mutating func serializeTuples( -// _ items: [(Key, URIEncodableNode)] -// ) throws { -// guard !items.isEmpty else { -// return -// } -// for (key, element) in items.dropLast() { -// try serializeNode(element, forKey: key) -// data.append("&") -// } -// if let (key, element) = items.last { -// try serializeNode(element, forKey: key) -// } -// } + mutating func parseUpToCharacterOrEnd( + _ character: Character + ) -> Self { + let startIndex = startIndex + guard startIndex != endIndex else { + return .init() + } + var currentIndex = startIndex + + func finalize() -> Self { + let parsed = self[startIndex.. String { defer { data.removeAll(keepingCapacity: true) } - try serializeAnyNode(value, forKey: key) + try serializeTopLevelNode(value, forKey: key) return data } @@ -145,8 +104,11 @@ extension URISerializer { return safeTopLevelKey } - private mutating func serializeAnyNode(_ value: URIEncodableNode, forKey key: String) throws { - func unwrapPrimitiveValue(_ node: URIEncodableNode) throws -> URIEncodableNode.Primitive { + private mutating func serializeTopLevelNode( + _ value: URIEncodedNode, + forKey key: String + ) throws { + func unwrapPrimitiveValue(_ node: URIEncodedNode) throws -> URIEncodedNode.Primitive { guard case let .primitive(primitive) = node else { throw SerializationError.nestedContainersNotSupported } @@ -157,12 +119,17 @@ extension URISerializer { // Nothing to serialize. break case .primitive(let primitive): + let keyAndValueSeparator: String? + switch configuration.style { + case .form: + keyAndValueSeparator = "=" + case .simple: + keyAndValueSeparator = nil + } try serializePrimitiveKeyValuePair( primitive, forKey: key, - - // TODO: Seems strange to assume this, is the API wrong? - separator: "=" + separator: keyAndValueSeparator ) case .array(let array): try serializeArray( @@ -178,7 +145,7 @@ extension URISerializer { } private mutating func serializePrimitiveValue( - _ value: URIEncodableNode.Primitive + _ value: URIEncodedNode.Primitive ) throws { let stringValue: String switch value { @@ -195,28 +162,27 @@ extension URISerializer { } private mutating func serializePrimitiveKeyValuePair( - _ value: URIEncodableNode.Primitive, + _ value: URIEncodedNode.Primitive, forKey key: String, - separator: String + separator: String? ) throws { - data.append(try stringifiedKey(key)) - data.append(separator) + if let separator { + data.append(try stringifiedKey(key)) + data.append(separator) + } try serializePrimitiveValue(value) } private mutating func serializeArray( - _ array: [URIEncodableNode.Primitive], + _ array: [URIEncodedNode.Primitive], forKey key: String ) throws { guard !array.isEmpty else { return } - let style = configuration.style - let explode = configuration.explode - let keyAndValueSeparator: String? let pairSeparator: String - switch (style, explode) { + switch (configuration.style, configuration.explode) { case (.form, true): keyAndValueSeparator = "=" pairSeparator = "&" @@ -227,7 +193,7 @@ extension URISerializer { keyAndValueSeparator = nil pairSeparator = "," } - func serializeNext(_ element: URIEncodableNode.Primitive) throws { + func serializeNext(_ element: URIEncodedNode.Primitive) throws { if let keyAndValueSeparator { try serializePrimitiveKeyValuePair( element, @@ -238,9 +204,9 @@ extension URISerializer { try serializePrimitiveValue(element) } } - if keyAndValueSeparator == nil { + if let containerKeyAndValue = configuration.containerKeyAndValueSeparator { data.append(try stringifiedKey(key)) - data.append("=") + data.append(containerKeyAndValue) } for element in array.dropLast() { try serializeNext(element) @@ -252,7 +218,7 @@ extension URISerializer { } private mutating func serializeDictionary( - _ dictionary: [String: URIEncodableNode.Primitive], + _ dictionary: [String: URIEncodedNode.Primitive], forKey key: String ) throws { guard !dictionary.isEmpty else { @@ -264,12 +230,9 @@ extension URISerializer { == .orderedAscending } - let style = configuration.style - let explode = configuration.explode - - let keyAndValueSeparator: String? + let keyAndValueSeparator: String let pairSeparator: String - switch (style, explode) { + switch (configuration.style, configuration.explode) { case (.form, true): keyAndValueSeparator = "=" pairSeparator = "&" @@ -283,20 +246,17 @@ extension URISerializer { keyAndValueSeparator = "," pairSeparator = "," } - func serializeNext(_ element: URIEncodableNode.Primitive, forKey elementKey: String) throws { - if let keyAndValueSeparator { - try serializePrimitiveKeyValuePair( - element, - forKey: elementKey, - separator: keyAndValueSeparator - ) - } else { - try serializePrimitiveValue(element) - } + + func serializeNext(_ element: URIEncodedNode.Primitive, forKey elementKey: String) throws { + try serializePrimitiveKeyValuePair( + element, + forKey: elementKey, + separator: keyAndValueSeparator + ) } - if !explode { + if let containerKeyAndValue = configuration.containerKeyAndValueSeparator { data.append(try stringifiedKey(key)) - data.append("=") + data.append(containerKeyAndValue) } for (elementKey, element) in sortedDictionary.dropLast() { try serializeNext(element, forKey: elementKey) @@ -307,3 +267,14 @@ extension URISerializer { } } } + +extension URISerializationConfiguration { + fileprivate var containerKeyAndValueSeparator: String? { + switch (style, explode) { + case (.form, false): + return "=" + default: + return nil + } + } +} diff --git a/Sources/OpenAPIRuntime/URICoder/URICoderConfiguration.swift b/Sources/OpenAPIRuntime/URICoder/URICoderConfiguration.swift new file mode 100644 index 00000000..d5376fc4 --- /dev/null +++ b/Sources/OpenAPIRuntime/URICoder/URICoderConfiguration.swift @@ -0,0 +1,72 @@ +//===----------------------------------------------------------------------===// +// +// 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 bag of configuration values used by the URI parser and serializer. +struct URISerializationConfiguration { + + // TODO: Wrap in a struct, as this will grow. + // https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#style-values + enum Style { + case simple + case form + } + + var style: Style + var explode: Bool + var spaceEscapingCharacter: String + + private init(style: Style, explode: Bool, spaceEscapingCharacter: String) { + self.style = style + self.explode = explode + self.spaceEscapingCharacter = spaceEscapingCharacter + } + + static let formExplode: Self = .init( + style: .form, + explode: true, + spaceEscapingCharacter: "%20" + ) + + static let formUnexplode: Self = .init( + style: .form, + explode: false, + spaceEscapingCharacter: "%20" + ) + + static let simpleExplode: Self = .init( + style: .simple, + explode: true, + spaceEscapingCharacter: "%20" + ) + + static let simpleUnexplode: Self = .init( + style: .simple, + explode: false, + spaceEscapingCharacter: "%20" + ) + + static let formDataExplode: Self = .init( + style: .form, + explode: true, + spaceEscapingCharacter: "+" + ) + + static let formDataUnexplode: Self = .init( + style: .form, + explode: false, + spaceEscapingCharacter: "+" + ) +} diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Test_URIEncoder.swift b/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIEncoder.swift similarity index 100% rename from Tests/OpenAPIRuntimeTests/URICoder/Test_URIEncoder.swift rename to Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIEncoder.swift diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Test_URIValueToNodeEncoder.swift b/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIValueToNodeEncoder.swift similarity index 98% rename from Tests/OpenAPIRuntimeTests/URICoder/Test_URIValueToNodeEncoder.swift rename to Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIValueToNodeEncoder.swift index 812ebead..2fc82dab 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Test_URIValueToNodeEncoder.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIValueToNodeEncoder.swift @@ -19,13 +19,13 @@ final class Test_URIValueToNodeEncoder: Test_Runtime { func testTranslating() throws { struct Case { var value: any Encodable - var expectedNode: URIEncodableNode + var expectedNode: URIEncodedNode var file: StaticString = #file var line: UInt = #line } func makeCase( _ value: any Encodable, - _ expectedNode: URIEncodableNode, + _ expectedNode: URIEncodedNode, file: StaticString = #file, line: UInt = #line ) diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift new file mode 100644 index 00000000..fe4f9977 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift @@ -0,0 +1,219 @@ +//===----------------------------------------------------------------------===// +// +// 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 XCTest +@testable import OpenAPIRuntime + +final class Test_URIParser: Test_Runtime { + + let testedVariants: [URISerializationConfiguration] = [ + .formExplode, + .formUnexplode, + .simpleExplode, + .simpleUnexplode, + .formDataExplode, + .formDataUnexplode, + ] + + func testParsing() throws { + let cases: [Case] = [ + makeCase( + .init( + formExplode: "empty=", + formUnexplode: "empty=", + simpleExplode: .custom("", value: ["": [""]]), + simpleUnexplode: .custom("", value: ["": [""]]), + formDataExplode: "empty=", + formDataUnexplode: "empty=" + ), + value: [ + "empty": [""] + ] + ), + makeCase( + .init( + formExplode: "who=fred", + formUnexplode: "who=fred", + simpleExplode: .custom("fred", value: ["": ["fred"]]), + simpleUnexplode: .custom("fred", value: ["": ["fred"]]), + formDataExplode: "who=fred", + formDataUnexplode: "who=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" + ), + 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" + ), + value: [ + "list": ["red", "green", "blue"] + ] + ), + makeCase( + .init( + formExplode: "comma=%2C&dot=.&semi=%3B", + formUnexplode: .custom( + "keys=comma,%2C,dot,.,semi,%3B", + value: ["keys": ["comma", ",", "dot", ".", "semi", ";"]] + ), + simpleExplode: "comma=%2C,dot=.,semi=%3B", + simpleUnexplode: .custom( + "comma,%2C,dot,.,semi,%3B", + value: ["": ["comma", ",", "dot", ".", "semi", ";"]] + ), + formDataExplode: "comma=%2C&dot=.&semi=%3B", + formDataUnexplode: .custom( + "keys=comma,%2C,dot,.,semi,%3B", + value: ["keys": ["comma", ",", "dot", ".", "semi", ";"]] + ) + ), + value: [ + "semi": [";"], + "dot": ["."], + "comma": [","], + ] + ), + ] + for testCase in cases { + func testVariant( + _ variant: Case.Variant, + _ input: Case.Variants.Input + ) throws { + var parser = URIParser( + configuration: variant.config, + data: input.string + ) + let parsedNode = try parser.parseRoot() + XCTAssertEqual( + parsedNode, + input.valueOverride ?? testCase.value, + "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) + } + } +} + +extension Test_URIParser { + struct Case { + struct Variant { + var name: String + var config: URISerializationConfiguration + + static let formExplode: Self = .init( + name: "formExplode", + config: .formExplode + ) + static let formUnexplode: Self = .init( + name: "formUnexplode", + config: .formUnexplode + ) + static let simpleExplode: Self = .init( + name: "simpleExplode", + config: .simpleExplode + ) + static let simpleUnexplode: Self = .init( + name: "simpleUnexplode", + config: .simpleUnexplode + ) + static let formDataExplode: Self = .init( + name: "formDataExplode", + config: .formDataExplode + ) + static let formDataUnexplode: Self = .init( + name: "formDataUnexplode", + config: .formDataUnexplode + ) + } + struct Variants { + + struct Input: ExpressibleByStringLiteral { + var string: String + var valueOverride: URIParsedNode? + + init(string: String, valueOverride: URIParsedNode? = nil) { + self.string = string + self.valueOverride = valueOverride + } + + static func custom(_ string: String, value: URIParsedNode) -> Self { + .init(string: string, valueOverride: value) + } + + init(stringLiteral value: String) { + self.string = value + self.valueOverride = nil + } + } + + var formExplode: Input + var formUnexplode: Input + var simpleExplode: Input + var simpleUnexplode: Input + var formDataExplode: Input + var formDataUnexplode: 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 + ) + } +} diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift b/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift new file mode 100644 index 00000000..8d573d6b --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift @@ -0,0 +1,220 @@ +//===----------------------------------------------------------------------===// +// +// 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 XCTest +@testable import OpenAPIRuntime + +final class Test_URISerializer: Test_Runtime { + + let testedVariants: [URISerializationConfiguration] = [ + .formExplode, + .formUnexplode, + .simpleExplode, + .simpleUnexplode, + .formDataExplode, + .formDataUnexplode, + ] + + func testSerializing() throws { + let cases: [Case] = [ + makeCase( + value: .primitive(.string("")), + key: "empty", + .init( + formExplode: "empty=", + formUnexplode: "empty=", + simpleExplode: "", + simpleUnexplode: "", + formDataExplode: "empty=", + formDataUnexplode: "empty=" + ) + ), + makeCase( + value: .primitive(.string("fred")), + key: "who", + .init( + formExplode: "who=fred", + formUnexplode: "who=fred", + simpleExplode: "fred", + simpleUnexplode: "fred", + formDataExplode: "who=fred", + formDataUnexplode: "who=fred" + ) + ), + makeCase( + value: .primitive(.integer(1234)), + key: "x", + .init( + formExplode: "x=1234", + formUnexplode: "x=1234", + simpleExplode: "1234", + simpleUnexplode: "1234", + formDataExplode: "x=1234", + formDataUnexplode: "x=1234" + ) + ), + makeCase( + value: .primitive(.double(12.34)), + key: "x", + .init( + formExplode: "x=12.34", + formUnexplode: "x=12.34", + simpleExplode: "12.34", + simpleUnexplode: "12.34", + formDataExplode: "x=12.34", + formDataUnexplode: "x=12.34" + ) + ), + makeCase( + value: .primitive(.bool(true)), + key: "enabled", + .init( + formExplode: "enabled=true", + formUnexplode: "enabled=true", + simpleExplode: "true", + simpleUnexplode: "true", + formDataExplode: "enabled=true", + formDataUnexplode: "enabled=true" + ) + ), + makeCase( + value: .primitive(.string("Hello World")), + key: "hello", + .init( + formExplode: "hello=Hello%20World", + formUnexplode: "hello=Hello%20World", + simpleExplode: "Hello%20World", + simpleUnexplode: "Hello%20World", + formDataExplode: "hello=Hello+World", + formDataUnexplode: "hello=Hello+World" + ) + ), + makeCase( + value: .array([ + .primitive(.string("red")), + .primitive(.string("green")), + .primitive(.string("blue")), + ]), + key: "list", + .init( + formExplode: "list=red&list=green&list=blue", + formUnexplode: "list=red,green,blue", + simpleExplode: "red,green,blue", + simpleUnexplode: "red,green,blue", + formDataExplode: "list=red&list=green&list=blue", + formDataUnexplode: "list=red,green,blue" + ) + ), + makeCase( + value: .dictionary([ + "semi": .primitive(.string(";")), + "dot": .primitive(.string(".")), + "comma": .primitive(.string(",")) + ]), + key: "keys", + .init( + formExplode: "comma=%2C&dot=.&semi=%3B", + formUnexplode: "keys=comma,%2C,dot,.,semi,%3B", + simpleExplode: "comma=%2C,dot=.,semi=%3B", + simpleUnexplode: "comma,%2C,dot,.,semi,%3B", + formDataExplode: "comma=%2C&dot=.&semi=%3B", + formDataUnexplode: "keys=comma,%2C,dot,.,semi,%3B" + ) + ), + ] + for testCase in cases { + func testVariant(_ variant: Case.Variant, _ expectedString: String) throws { + var serializer = URISerializer(configuration: variant.config) + let encodedString = try serializer.serializeNode( + testCase.value, + forKey: testCase.key + ) + XCTAssertEqual( + encodedString, + expectedString, + "Failed for config: \(variant.name)", + file: testCase.file, + line: testCase.line + ) + } + try testVariant(.formExplode, testCase.variants.formExplode) + try testVariant(.formUnexplode, testCase.variants.formUnexplode) + try testVariant(.simpleExplode, testCase.variants.simpleExplode) + try testVariant(.simpleUnexplode, testCase.variants.simpleUnexplode) + try testVariant(.formDataExplode,testCase.variants.formDataExplode) + try testVariant(.formDataUnexplode, testCase.variants.formDataUnexplode) + } + } +} + +extension Test_URISerializer { + struct Case { + struct Variant { + var name: String + var config: URISerializationConfiguration + + static let formExplode: Self = .init( + name: "formExplode", + config: .formExplode + ) + static let formUnexplode: Self = .init( + name: "formUnexplode", + config: .formUnexplode + ) + static let simpleExplode: Self = .init( + name: "simpleExplode", + config: .simpleExplode + ) + static let simpleUnexplode: Self = .init( + name: "simpleUnexplode", + config: .simpleUnexplode + ) + static let formDataExplode: Self = .init( + name: "formDataExplode", + config: .formDataExplode + ) + static let formDataUnexplode: Self = .init( + name: "formDataUnexplode", + config: .formDataUnexplode + ) + } + struct Variants { + var formExplode: String + var formUnexplode: String + var simpleExplode: String + var simpleUnexplode: String + var formDataExplode: String + var formDataUnexplode: String + } + var value: URIEncodedNode + var key: String + var variants: Variants + var file: StaticString = #file + var line: UInt = #line + } + func makeCase( + value: URIEncodedNode, + key: String, + _ variants: Case.Variants, + file: StaticString = #file, + line: UInt = #line + ) -> Case { + .init( + value: value, + key: key, + variants: variants, + file: file, + line: line + ) + } +} diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Test_URIFormStyleSerializer.swift b/Tests/OpenAPIRuntimeTests/URICoder/Test_URIFormStyleSerializer.swift deleted file mode 100644 index 33de58a4..00000000 --- a/Tests/OpenAPIRuntimeTests/URICoder/Test_URIFormStyleSerializer.swift +++ /dev/null @@ -1,145 +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 XCTest -@testable import OpenAPIRuntime - -final class Test_URISerializer: Test_Runtime { - - let testedVariants: [URISerializer.Configuration] = [ - .formExplode, - .formUnexplode, - .simpleExplode, - .simpleUnexplode, - .formDataExplode, - .formDataUnexplode, - ] - - func testSerializing() throws { - let cases: [Case] = [ - makeCase( - .primitive(.string("")), - "root=" - ), - makeCase( - .primitive(.string("fred")), - "root=fred" - ), - makeCase( - .primitive(.integer(1234)), - "root=1234" - ), - makeCase( - .primitive(.double(12.34)), - "root=12.34" - ), - makeCase( - .primitive(.bool(true)), - "root=true" - ), - makeCase( - .primitive(.string("Hello World")), - [ - (.formExplode, "root=Hello%20World"), - (.simpleExplode, "root=Hello%20World"), - (.formDataExplode, "root=Hello+World"), - ] - ), - makeCase( - .primitive(.string("50%")), - "root=50%25" - ), - makeCase( - .array([ - .primitive(.string("red")), - .primitive(.string("green")), - .primitive(.string("blue")), - ]), - [ - (.formExplode, "root=red&root=green&root=blue"), - (.formUnexplode, "root=red,green,blue"), - (.simpleExplode, "root=red,green,blue"), - (.simpleUnexplode, "root=red,green,blue"), - (.formDataExplode, "root=red&root=green&root=blue"), - (.formDataUnexplode, "root=red,green,blue"), - ] - ), - makeCase( - .dictionary([ - "semi": .primitive(.string(";")), - "dot": .primitive(.string(".")), - "comma": .primitive(.string(",")) - ]), - [ - (.formExplode, "comma=%2C&dot=.&semi=%3B"), - (.formUnexplode, "root=comma,%2C,dot,.,semi,%3B"), - (.simpleExplode, "comma=%2C,dot=.,semi=%3B"), - (.simpleUnexplode, "root=comma,%2C,dot,.,semi,%3B"), - (.formDataExplode, "comma=%2C&dot=.&semi=%3B"), - (.formDataUnexplode, "root=comma,%2C,dot,.,semi,%3B"), - ] - ), - ] - for testCase in cases { - for (config, expectedString) in testCase.variants { - var serializer = URISerializer(configuration: config) - let encodedString = try serializer.serializeNode( - testCase.value, - forKey: "root" - ) - XCTAssertEqual( - encodedString, - expectedString, - "Failed for config: \(config)", - file: testCase.file, - line: testCase.line - ) - } - } - } -} - -extension Test_URISerializer { - struct Case { - var value: URIEncodableNode - var variants: [(URISerializer.Configuration, String)] - var file: StaticString = #file - var line: UInt = #line - } - func makeCase( - _ value: URIEncodableNode, - _ expectedString: String, - file: StaticString = #file, - line: UInt = #line - ) -> Case { - .init( - value: value, - variants: testedVariants.map { config in (config, expectedString) }, - file: file, - line: line - ) - } - func makeCase( - _ value: URIEncodableNode, - _ variants: [(URISerializer.Configuration, String)], - file: StaticString = #file, - line: UInt = #line - ) -> Case { - .init( - value: value, - variants: variants, - file: file, - line: line - ) - } -} diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Test_URIParser.swift b/Tests/OpenAPIRuntimeTests/URICoder/Test_URIParser.swift deleted file mode 100644 index 0d9d2c4f..00000000 --- a/Tests/OpenAPIRuntimeTests/URICoder/Test_URIParser.swift +++ /dev/null @@ -1,160 +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 XCTest -@testable import OpenAPIRuntime - -final class Test_URIParser: Test_Runtime { - - - func testParsing() throws { - struct Case { - var value: String - var expectedNode: URIParsedNode - var file: StaticString = #file - var line: UInt = #line - } - func makeCase(_ value: String, _ expectedNode: URIParsedNode, file: StaticString = #file, line: UInt = #line) - -> Case - { - .init(value: value, expectedNode: expectedNode, file: file, line: line) - } - - let cases: [Case] = [ - makeCase("root=", .primitive("")), -// makeCase(.primitive(.string("Hello World")), "root=Hello World"), -// makeCase(.primitive(.integer(1234)), "root=1234"), -// makeCase(.primitive(.double(12.34)), "root=12.34"), -// makeCase(.primitive(.bool(true)), "root=true"), -// makeCase( -// .array([ -// .primitive(.string("a")), -// .primitive(.string("b")), -// .primitive(.string("c")), -// ]), -// "root[0]=a&root[1]=b&root[2]=c" -// ), -// makeCase( -// .array([ -// .array([ -// .primitive(.string("a")) -// ]), -// .array([ -// .primitive(.string("b")), -// .primitive(.string("c")), -// ]), -// ]), -// "root[0][0]=a&root[1][0]=b&root[1][1]=c" -// ), -// makeCase( -// .dictionary([ -// "foo": .primitive(.string("bar")) -// ]), -// "root[foo]=bar" -// ), -// makeCase( -// .dictionary([ -// "simple": .dictionary([ -// "foo": .primitive(.string("bar")) -// ]) -// ]), -// "root[simple][foo]=bar" -// ), -// makeCase( -// .array([ -// .dictionary([ -// "foo": .primitive(.string("bar")) -// ]), -// .dictionary([ -// "foo": .primitive(.string("baz")) -// ]), -// ]), -// "root[0][foo]=bar&root[1][foo]=baz" -// ), -// makeCase( -// .array([ -// .array([ -// .dictionary([ -// "foo": .primitive(.string("bar")) -// ]) -// ]), -// .array([ -// .dictionary([ -// "foo": .primitive(.string("baz")) -// ]) -// ]), -// ]), -// "root[0][0][foo]=bar&root[1][0][foo]=baz" -// ), -// makeCase( -// .dictionary([ -// "one": .primitive(.integer(1)), -// "two": .primitive(.integer(2)), -// ]), -// "root[one]=1&root[two]=2" -// ), -// makeCase( -// .dictionary([ -// "A": .dictionary([ -// "one": .primitive(.integer(1)), -// "two": .primitive(.integer(2)), -// ]), -// "B": .dictionary([ -// "three": .primitive(.integer(3)), -// "four": .primitive(.integer(4)), -// ]), -// ]), -// "root[A][one]=1&root[A][two]=2&root[B][four]=4&root[B][three]=3" -// ), -// makeCase( -// .dictionary([ -// "barkey": .dictionary([ -// "foo": .primitive(.string("bar")) -// ]), -// "bazkey": .dictionary([ -// "foo": .primitive(.string("baz")) -// ]), -// ]), -// "root[barkey][foo]=bar&root[bazkey][foo]=baz" -// ), -// makeCase( -// .dictionary([ -// "outBar": .dictionary([ -// "inBar": .dictionary([ -// "foo": .primitive(.string("bar")) -// ]) -// ]), -// "outBaz": .dictionary([ -// "inBaz": .dictionary([ -// "foo": .primitive(.string("baz")) -// ]) -// ]), -// ]), -// "root[outBar][inBar][foo]=bar&root[outBaz][inBaz][foo]=baz" -// ), - ] -// var serializer = URISerializer() -// for testCase in cases { -// let encodedString = try serializer.writeNode( -// testCase.value, -// forKey: .key("root") -// ) -// XCTAssertEqual( -// encodedString, -// testCase.expectedString, -// file: testCase.file, -// line: testCase.line -// ) -// } - } -} From 2f710ab251dc4be5cabef646e06bc4e25fad8c79 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 23 Aug 2023 10:32:41 +0200 Subject: [PATCH 04/17] Renaming, prepared the ground for implementing the decoder --- .../{ => Common}/URICoderConfiguration.swift | 2 +- .../{Encoding => Common}/URIEncodedNode.swift | 0 .../{Parsing => Common}/URIParsedNode.swift | 0 .../URICoder/Decoding/URIDecoder.swift | 50 +++++++++++++++++++ .../Decoding/URIValueFromNodeDecoder.swift | 28 +++++++++++ .../URICoder/Encoding/URIEncoder.swift | 14 +++--- .../URIValueToNodeEncoder+Keyed.swift | 20 ++++---- .../URIValueToNodeEncoder+Single.swift | 6 +-- .../URIValueToNodeEncoder+Unkeyed.swift | 22 ++++---- .../Encoding/URIValueToNodeEncoder.swift | 8 +-- .../URICoder/Parsing/URIParser.swift | 18 +++---- .../Serialization/URISerializer.swift | 8 +-- .../Encoding/Test_URIValueToNodeEncoder.swift | 2 +- .../URICoder/Parsing/Test_URIParser.swift | 4 +- .../Serialization/Test_URISerializer.swift | 4 +- 15 files changed, 131 insertions(+), 55 deletions(-) rename Sources/OpenAPIRuntime/URICoder/{ => Common}/URICoderConfiguration.swift (98%) rename Sources/OpenAPIRuntime/URICoder/{Encoding => Common}/URIEncodedNode.swift (100%) rename Sources/OpenAPIRuntime/URICoder/{Parsing => Common}/URIParsedNode.swift (100%) create mode 100644 Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift create mode 100644 Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift diff --git a/Sources/OpenAPIRuntime/URICoder/URICoderConfiguration.swift b/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift similarity index 98% rename from Sources/OpenAPIRuntime/URICoder/URICoderConfiguration.swift rename to Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift index d5376fc4..98fa59eb 100644 --- a/Sources/OpenAPIRuntime/URICoder/URICoderConfiguration.swift +++ b/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift @@ -15,7 +15,7 @@ import Foundation /// A bag of configuration values used by the URI parser and serializer. -struct URISerializationConfiguration { +struct URICoderConfiguration { // TODO: Wrap in a struct, as this will grow. // https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#style-values diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncodedNode.swift b/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift similarity index 100% rename from Sources/OpenAPIRuntime/URICoder/Encoding/URIEncodedNode.swift rename to Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift diff --git a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParsedNode.swift b/Sources/OpenAPIRuntime/URICoder/Common/URIParsedNode.swift similarity index 100% rename from Sources/OpenAPIRuntime/URICoder/Parsing/URIParsedNode.swift rename to Sources/OpenAPIRuntime/URICoder/Common/URIParsedNode.swift diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift new file mode 100644 index 00000000..fb94448b --- /dev/null +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift @@ -0,0 +1,50 @@ +//===----------------------------------------------------------------------===// +// +// 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 type that decodes a `Decodable` objects from an URI-encoded string +/// using the rules from RFC 6570, RFC 1866, and OpenAPI 3.0.3, depending on +/// the configuration. +struct URIDecoder: Sendable { + + private let configuration: URICoderConfiguration + + init(configuration: URICoderConfiguration) { + self.configuration = configuration + } +} + +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. + /// - data: The URI-encoded string. + /// - Returns: The decoded value. + func decode( + _ type: T.Type = T.self, + from data: String + ) throws -> T { + var parser = URIParser(configuration: configuration, data: data) + let parsedNode = try parser.parseRoot() + let decoder = URIValueFromNodeDecoder(node: parsedNode) + return try decoder.decodeRoot() + } +} diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift new file mode 100644 index 00000000..dc0ddbe0 --- /dev/null +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +final class URIValueFromNodeDecoder { + + private var node: URIParsedNode + + init(node: URIParsedNode) { + self.node = node + } + + func decodeRoot(_ type: T.Type = T.self) throws -> T { + fatalError() + } +} diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift index 5ec99a09..374eb3b3 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift @@ -14,11 +14,9 @@ import Foundation -/// A type that encodes an `Encodable` objects to an URL-encoded string +/// A type that encodes an `Encodable` objects to an URI-encoded string /// using the rules from RFC 6570, RFC 1866, and OpenAPI 3.0.3, depending on /// the configuration. -/// -/// - [OpenAPI 3.0.3 styles](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#style-examples) struct URIEncoder: Sendable { private let serializer: URISerializer @@ -27,8 +25,8 @@ struct URIEncoder: Sendable { self.serializer = serializer } - init(serializerConfiguration: URISerializationConfiguration) { - self.init(serializer: .init(configuration: serializerConfiguration)) + init(configuration: URICoderConfiguration) { + self.init(serializer: .init(configuration: configuration)) } } @@ -46,12 +44,12 @@ extension URIEncoder { /// - key: The key for which to encode the value. Can be an empty key, /// in which case you still get a key-value pair, like `=foo`. /// - Returns: The URI string. - public func encode( + func encode( _ value: some Encodable, forKey key: String ) throws -> String { - let translator = URIValueToNodeEncoder() - let node = try translator.translateValue(value) + let encoder = URIValueToNodeEncoder() + let node = try encoder.encodeValue(value) var serializer = serializer let encodedString = try serializer.serializeNode(node, forKey: key) return encodedString diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift index da9afb5c..4f27fa74 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift @@ -15,12 +15,12 @@ import Foundation struct URIKeyedEncodingContainer { - let translator: URIValueToNodeEncoder + let encoder: URIValueToNodeEncoder } extension URIKeyedEncodingContainer { private func _insertValue(_ node: URIEncodedNode, atKey key: Key) throws { - try translator.currentStackEntry.storage.insert(node, atKey: key) + try encoder.currentStackEntry.storage.insert(node, atKey: key) } private func _insertValue(_ node: URIEncodedNode.Primitive, atKey key: Key) throws { @@ -48,7 +48,7 @@ extension URIKeyedEncodingContainer { extension URIKeyedEncodingContainer: KeyedEncodingContainerProtocol { var codingPath: [any CodingKey] { - translator.codingPath + encoder.codingPath } mutating func encodeNil(forKey key: Key) throws { @@ -142,9 +142,9 @@ extension URIKeyedEncodingContainer: KeyedEncodingContainerProtocol { case let value as Bool: try encode(value, forKey: key) default: - translator.push(key: .init(key), newStorage: .unset) - try value.encode(to: translator) - try translator.pop() + encoder.push(key: .init(key), newStorage: .unset) + try value.encode(to: encoder) + try encoder.pop() } } @@ -152,20 +152,20 @@ extension URIKeyedEncodingContainer: KeyedEncodingContainerProtocol { keyedBy keyType: NestedKey.Type, forKey key: Key ) -> KeyedEncodingContainer where NestedKey: CodingKey { - translator.container(keyedBy: NestedKey.self) + encoder.container(keyedBy: NestedKey.self) } mutating func nestedUnkeyedContainer( forKey key: Key ) -> any UnkeyedEncodingContainer { - translator.unkeyedContainer() + encoder.unkeyedContainer() } mutating func superEncoder() -> any Encoder { - translator + encoder } mutating func superEncoder(forKey key: Key) -> any Encoder { - translator + encoder } } diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift index 87b0439c..c5bcee82 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift @@ -15,12 +15,12 @@ import Foundation struct URISingleValueEncodingContainer: SingleValueEncodingContainer { - let translator: URIValueToNodeEncoder + let encoder: URIValueToNodeEncoder } extension URISingleValueEncodingContainer { private func _setValue(_ node: URIEncodedNode.Primitive) throws { - try translator.currentStackEntry.storage.set(node) + try encoder.currentStackEntry.storage.set(node) } private func _setBinaryFloatingPoint(_ value: some BinaryFloatingPoint) throws { @@ -38,7 +38,7 @@ extension URISingleValueEncodingContainer { extension URISingleValueEncodingContainer { var codingPath: [any CodingKey] { - translator.codingPath + encoder.codingPath } func encodeNil() throws { diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift index 1407374d..9a86d543 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift @@ -15,12 +15,12 @@ import Foundation struct URIUnkeyedEncodingContainer { - let translator: URIValueToNodeEncoder + let encoder: URIValueToNodeEncoder } extension URIUnkeyedEncodingContainer { private func _appendValue(_ node: URIEncodedNode) throws { - try translator.currentStackEntry.storage.append(node) + try encoder.currentStackEntry.storage.append(node) } private func _appendValue(_ node: URIEncodedNode.Primitive) throws { @@ -42,32 +42,32 @@ extension URIUnkeyedEncodingContainer { extension URIUnkeyedEncodingContainer: UnkeyedEncodingContainer { var codingPath: [any CodingKey] { - translator.codingPath + encoder.codingPath } var count: Int { - switch translator.currentStackEntry.storage { + switch encoder.currentStackEntry.storage { case .array(let array): return array.count case .unset: return 0 default: - fatalError("Cannot have an unkeyed container at \(translator.currentStackEntry).") + fatalError("Cannot have an unkeyed container at \(encoder.currentStackEntry).") } } func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer { - translator.unkeyedContainer() + encoder.unkeyedContainer() } func nestedContainer( keyedBy keyType: NestedKey.Type ) -> KeyedEncodingContainer where NestedKey: CodingKey { - translator.container(keyedBy: NestedKey.self) + encoder.container(keyedBy: NestedKey.self) } func superEncoder() -> any Encoder { - translator + encoder } func encodeNil() throws { @@ -161,9 +161,9 @@ extension URIUnkeyedEncodingContainer: UnkeyedEncodingContainer { case let value as Bool: try encode(value) default: - translator.push(key: .init(intValue: count), newStorage: .unset) - try value.encode(to: translator) - try translator.pop() + encoder.push(key: .init(intValue: count), newStorage: .unset) + try value.encode(to: encoder) + try encoder.pop() } } } diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift index c6979448..0bbed8a4 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift @@ -64,7 +64,7 @@ final class URIValueToNodeEncoder { ) } - func translateValue(_ value: some Encodable) throws -> URIEncodedNode { + func encodeValue(_ value: some Encodable) throws -> URIEncodedNode { defer { _codingPath = [] currentStackEntry = CodingStackEntry( @@ -112,14 +112,14 @@ extension URIValueToNodeEncoder: Encoder { func container( keyedBy type: Key.Type ) -> KeyedEncodingContainer where Key: CodingKey { - KeyedEncodingContainer(URIKeyedEncodingContainer(translator: self)) + KeyedEncodingContainer(URIKeyedEncodingContainer(encoder: self)) } func unkeyedContainer() -> any UnkeyedEncodingContainer { - URIUnkeyedEncodingContainer(translator: self) + URIUnkeyedEncodingContainer(encoder: self) } func singleValueContainer() -> any SingleValueEncodingContainer { - URISingleValueEncodingContainer(translator: self) + URISingleValueEncodingContainer(encoder: self) } } diff --git a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift index b385eb54..e33b0f27 100644 --- a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift +++ b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift @@ -45,11 +45,11 @@ import Foundation /// | `{keys\*}` | `semi=%3B,dot=.,comma=%2C` | struct URIParser: Sendable { - private let configuration: URISerializationConfiguration + private let configuration: URICoderConfiguration private typealias Raw = String.SubSequence private var data: Raw - init(configuration: URISerializationConfiguration, data: String) { + init(configuration: URICoderConfiguration, data: String) { self.configuration = configuration self.data = data[...] } @@ -82,7 +82,7 @@ extension URIParser { } } - mutating func parseExplodedFormRoot() throws -> URIParsedNode { + private mutating func parseExplodedFormRoot() throws -> URIParsedNode { try parseGenericRoot { data, appendPair in let keyValueSeparator: Character = "=" let pairSeparator: Character = "&" @@ -110,7 +110,7 @@ extension URIParser { } } - mutating func parseUnexplodedFormRoot() throws -> URIParsedNode { + private mutating func parseUnexplodedFormRoot() throws -> URIParsedNode { try parseGenericRoot { data, appendPair in let keyValueSeparator: Character = "=" let pairSeparator: Character = "&" @@ -159,7 +159,7 @@ extension URIParser { } } - mutating func parseExplodedSimpleRoot() throws -> URIParsedNode { + private mutating func parseExplodedSimpleRoot() throws -> URIParsedNode { try parseGenericRoot { data, appendPair in let keyValueSeparator: Character = "=" let pairSeparator: Character = "," @@ -187,7 +187,7 @@ extension URIParser { } } - mutating func parseUnexplodedSimpleRoot() throws -> URIParsedNode { + 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 @@ -249,12 +249,12 @@ extension URIParser { extension String.SubSequence { - enum ParseUpToEitherCharacterResult { + fileprivate enum ParseUpToEitherCharacterResult { case foundFirst case foundSecondOrEnd } - mutating func parseUpToEitherCharacterOrEnd( + fileprivate mutating func parseUpToEitherCharacterOrEnd( first: Character, second: Character ) -> (ParseUpToEitherCharacterResult, Self) { @@ -289,7 +289,7 @@ extension String.SubSequence { return finalize(.foundSecondOrEnd) } - mutating func parseUpToCharacterOrEnd( + fileprivate mutating func parseUpToCharacterOrEnd( _ character: Character ) -> Self { let startIndex = startIndex diff --git a/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift b/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift index fdcb540b..7f63394d 100644 --- a/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift +++ b/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift @@ -45,10 +45,10 @@ import Foundation /// | `{keys\*}` | `semi=%3B,dot=.,comma=%2C` | struct URISerializer { - private let configuration: URISerializationConfiguration + private let configuration: URICoderConfiguration private var data: String - init(configuration: URISerializationConfiguration) { + init(configuration: URICoderConfiguration) { self.configuration = configuration self.data = "" } @@ -80,7 +80,7 @@ extension URISerializer { extension URISerializer { - enum SerializationError: Swift.Error { + private enum SerializationError: Swift.Error { case nestedContainersNotSupported } @@ -268,7 +268,7 @@ extension URISerializer { } } -extension URISerializationConfiguration { +extension URICoderConfiguration { fileprivate var containerKeyAndValueSeparator: String? { switch (style, explode) { case (.form, false): diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIValueToNodeEncoder.swift b/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIValueToNodeEncoder.swift index 2fc82dab..f334925d 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIValueToNodeEncoder.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIValueToNodeEncoder.swift @@ -227,7 +227,7 @@ final class Test_URIValueToNodeEncoder: Test_Runtime { ] let translator = URIValueToNodeEncoder() for testCase in cases { - let translatedNode = try translator.translateValue(testCase.value) + let translatedNode = try translator.encodeValue(testCase.value) XCTAssertEqual( translatedNode, testCase.expectedNode, diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift index fe4f9977..47b11a17 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift @@ -16,7 +16,7 @@ import XCTest final class Test_URIParser: Test_Runtime { - let testedVariants: [URISerializationConfiguration] = [ + let testedVariants: [URICoderConfiguration] = [ .formExplode, .formUnexplode, .simpleExplode, @@ -143,7 +143,7 @@ extension Test_URIParser { struct Case { struct Variant { var name: String - var config: URISerializationConfiguration + var config: URICoderConfiguration static let formExplode: Self = .init( name: "formExplode", diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift b/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift index 8d573d6b..b578d51c 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift @@ -16,7 +16,7 @@ import XCTest final class Test_URISerializer: Test_Runtime { - let testedVariants: [URISerializationConfiguration] = [ + let testedVariants: [URICoderConfiguration] = [ .formExplode, .formUnexplode, .simpleExplode, @@ -161,7 +161,7 @@ extension Test_URISerializer { struct Case { struct Variant { var name: String - var config: URISerializationConfiguration + var config: URICoderConfiguration static let formExplode: Self = .init( name: "formExplode", From 569c576fa3564fe42ff4287b725305e700fcb8ad Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 23 Aug 2023 12:38:06 +0200 Subject: [PATCH 05/17] WIP on the Decoder --- .../URICoder/Common/URICodeCodingKey.swift | 36 +++ .../URICoder/Common/URIParsedNode.swift | 5 +- .../URIValueFromNodeDecoder+Single.swift | 169 ++++++++++++ .../Decoding/URIValueFromNodeDecoder.swift | 85 ++++++- .../Encoding/URIValueToNodeEncoder.swift | 25 +- .../URICoder/Decoder/Test_URIDecoder.swift | 27 ++ .../Test_URIValueFromNodeDecoder.swift | 217 ++++++++++++++++ .../Encoding/Test_URIValueToNodeEncoder.swift | 8 +- .../URICoder/Test_URICodingRoundtrip.swift | 240 ++++++++++++++++++ 9 files changed, 783 insertions(+), 29 deletions(-) create mode 100644 Sources/OpenAPIRuntime/URICoder/Common/URICodeCodingKey.swift create mode 100644 Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift create mode 100644 Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift create mode 100644 Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift create mode 100644 Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URICodeCodingKey.swift b/Sources/OpenAPIRuntime/URICoder/Common/URICodeCodingKey.swift new file mode 100644 index 00000000..cc2f51de --- /dev/null +++ b/Sources/OpenAPIRuntime/URICoder/Common/URICodeCodingKey.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// 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 coding key. +struct URICoderCodingKey: CodingKey { + var stringValue: String + var intValue: Int? + + init(stringValue: String) { + self.stringValue = stringValue + self.intValue = nil + } + + init(intValue: Int) { + self.stringValue = "\(intValue)" + self.intValue = intValue + } + + init(_ key: some CodingKey) { + self.stringValue = key.stringValue + self.intValue = key.intValue + } +} diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URIParsedNode.swift b/Sources/OpenAPIRuntime/URICoder/Common/URIParsedNode.swift index f15bf77a..ee6b7246 100644 --- a/Sources/OpenAPIRuntime/URICoder/Common/URIParsedNode.swift +++ b/Sources/OpenAPIRuntime/URICoder/Common/URIParsedNode.swift @@ -14,4 +14,7 @@ import Foundation -typealias URIParsedNode = [String.SubSequence: [String.SubSequence]] +typealias URIParsedKey = String.SubSequence +typealias URIParsedValue = String.SubSequence +typealias URIParsedValueArray = [URIParsedValue] +typealias URIParsedNode = [URIParsedKey: URIParsedValueArray] diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift new file mode 100644 index 00000000..5884bca1 --- /dev/null +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift @@ -0,0 +1,169 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +struct URISingleValueDecodingContainer { + let _codingPath: [any CodingKey] + let value: URIParsedValue +} + +extension URISingleValueDecodingContainer { + private func _decodeBinaryFloatingPoint( + _: T.Type = T.self + ) throws -> T { + guard let double = Double(value) else { + throw DecodingError.typeMismatch( + T.self, + .init( + codingPath: codingPath, + debugDescription: "Failed to convert to Double." + ) + ) + } + return T(double) + } + + private func _decodeFixedWidthInteger( + _: T.Type = T.self + ) throws -> T { + guard let parsedValue = T(value) else { + throw DecodingError.typeMismatch( + T.self, + .init( + codingPath: codingPath, + debugDescription: "Failed to convert to the requested type." + ) + ) + } + return parsedValue + } + + private func _decodeLosslessStringConvertible( + _: T.Type = T.self + ) throws -> T { + guard let parsedValue = T(String(value)) else { + throw DecodingError.typeMismatch( + T.self, + .init( + codingPath: codingPath, + debugDescription: "Failed to convert to the requested type." + ) + ) + } + return parsedValue + } +} + +extension URISingleValueDecodingContainer: SingleValueDecodingContainer { + + var codingPath: [any CodingKey] { + _codingPath + } + + func decodeNil() -> Bool { + false + } + + func decode(_ type: Bool.Type) throws -> Bool { + try _decodeLosslessStringConvertible() + } + + func decode(_ type: String.Type) throws -> String { + String(value) + } + + func decode(_ type: Double.Type) throws -> Double { + try _decodeBinaryFloatingPoint() + } + + func decode(_ type: Float.Type) throws -> Float { + try _decodeBinaryFloatingPoint() + } + + func decode(_ type: Int.Type) throws -> Int { + try _decodeFixedWidthInteger() + } + + func decode(_ type: Int8.Type) throws -> Int8 { + try _decodeFixedWidthInteger() + } + + func decode(_ type: Int16.Type) throws -> Int16 { + try _decodeFixedWidthInteger() + } + + func decode(_ type: Int32.Type) throws -> Int32 { + try _decodeFixedWidthInteger() + } + + func decode(_ type: Int64.Type) throws -> Int64 { + try _decodeFixedWidthInteger() + } + + func decode(_ type: UInt.Type) throws -> UInt { + try _decodeFixedWidthInteger() + } + + func decode(_ type: UInt8.Type) throws -> UInt8 { + try _decodeFixedWidthInteger() + } + + func decode(_ type: UInt16.Type) throws -> UInt16 { + try _decodeFixedWidthInteger() + } + + func decode(_ type: UInt32.Type) throws -> UInt32 { + try _decodeFixedWidthInteger() + } + + func decode(_ type: UInt64.Type) throws -> UInt64 { + try _decodeFixedWidthInteger() + } + + func decode(_ type: T.Type) throws -> T where T : Decodable { + switch type { + case is Bool.Type: + return try decode(Bool.self) as! T + case is String.Type: + return try decode(String.self) as! T + case is Double.Type: + return try decode(Double.self) as! T + case is Float.Type: + return try decode(Float.self) as! T + case is Int.Type: + return try decode(Int.self) as! T + case is Int8.Type: + return try decode(Int8.self) as! T + case is Int16.Type: + return try decode(Int16.self) as! T + case is Int32.Type: + return try decode(Int32.self) as! T + case is Int64.Type: + return try decode(Int64.self) as! T + case is UInt.Type: + return try decode(UInt.self) as! T + case is UInt8.Type: + return try decode(UInt8.self) as! T + 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 + default: + throw URIValueFromNodeDecoder.GeneralError.unsupportedType(T.self) + } + } +} diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift index dc0ddbe0..339a502f 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift @@ -16,13 +16,96 @@ import Foundation final class URIValueFromNodeDecoder { - private var node: URIParsedNode + private let node: URIParsedNode + private var codingStack: [CodingStackEntry] init(node: URIParsedNode) { self.node = node + self.codingStack = [] } func decodeRoot(_ type: T.Type = T.self) throws -> T { + precondition(codingStack.isEmpty) + defer { + precondition(codingStack.isEmpty) + } + return try T.init(from: self) + } +} + +extension URIValueFromNodeDecoder { + enum GeneralError: Swift.Error { + case unsupportedType(Any.Type) + } + + /// An entry in the coding stack for URIValueFromNodeDecoder. + /// + /// This is used to keep track of where we are in the decode. + private struct CodingStackEntry { + var key: URICoderCodingKey + var element: URIParsedNode + } + + /// The element at the current head of the coding stack. + private var currentElement: URIParsedNode? { + self.codingStack.last.map { $0.element } + } +} + +extension URIValueFromNodeDecoder: Decoder { + + var codingPath: [any CodingKey] { + // TODO: Fill in + [] + } + + var userInfo: [CodingUserInfoKey : Any] { + [:] + } + + func container( + keyedBy type: Key.Type + ) throws -> KeyedDecodingContainer where Key : CodingKey { fatalError() } + + func unkeyedContainer() throws -> any UnkeyedDecodingContainer { + fatalError() + } + + func singleValueContainer() throws -> any SingleValueDecodingContainer { + + func throwMismatch(_ message: String) throws -> Never { + throw DecodingError.typeMismatch( + String.self, + .init( + codingPath: codingPath, + debugDescription: message + ) + ) + } + + // 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. + + guard !node.isEmpty else { + try throwMismatch("Cannot parse a single value from an empty node.") + } + guard node.count == 1 else { + try throwMismatch("Cannot parse a single value from a node with multiple key-value pairs.") + } + let values = node.first!.value + guard !values.isEmpty else { + try throwMismatch("Cannot parse a single value from a node with an empty value array.") + } + guard values.count == 1 else { + try throwMismatch("Cannot parse a single value from a node with multiple values.") + } + let value = values[0] + return URISingleValueDecodingContainer( + _codingPath: codingPath, + value: value + ) + } } diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift index 0bbed8a4..db43036a 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift @@ -17,32 +17,11 @@ import Foundation /// Converts an Encodable type into a URIEncodableNode. final class URIValueToNodeEncoder { - /// The coding key. - struct _CodingKey: CodingKey { - var stringValue: String - var intValue: Int? - - init(stringValue: String) { - self.stringValue = stringValue - self.intValue = nil - } - - init(intValue: Int) { - self.stringValue = "\(intValue)" - self.intValue = intValue - } - - init(_ key: some CodingKey) { - self.stringValue = key.stringValue - self.intValue = key.intValue - } - } - /// An entry in the coding stack for \_URIEncoder. /// /// This is used to keep track of where we are in the encode. struct CodingStackEntry { - var key: _CodingKey + var key: URICoderCodingKey var storage: URIEncodedNode } @@ -94,7 +73,7 @@ extension URIValueToNodeEncoder: Encoder { [:] } - func push(key: _CodingKey, newStorage: URIEncodedNode) { + func push(key: URICoderCodingKey, newStorage: URIEncodedNode) { _codingPath.append(currentStackEntry) currentStackEntry = .init(key: key, storage: newStorage) } diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift new file mode 100644 index 00000000..5c1c9807 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------===// +// +// 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 XCTest +@testable import OpenAPIRuntime + +final class Test_URIDecoder: Test_Runtime { + + func testDecoding() throws { + struct Foo: Decodable, Equatable { + var bar: String + } + let decoder = URIDecoder(configuration: .formDataExplode) + let decodedValue = try decoder.decode(Foo.self, from: "bar=hello+world") + XCTAssertEqual(decodedValue, Foo(bar: "hello world")) + } +} diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift new file mode 100644 index 00000000..c41ef8b2 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift @@ -0,0 +1,217 @@ +//===----------------------------------------------------------------------===// +// +// 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 XCTest +@testable import OpenAPIRuntime + +final class Test_URIValueFromNodeDecoder: Test_Runtime { + + func testDecoding() throws { + struct SimpleStruct: Encodable { + var foo: String + var bar: Int? + } + + // An empty string. + try test( + ["": [""]], + "" + ) + + // A string with a space. + try test( + ["": ["Hello World"]], + "Hello World" + ) + + // An integer. + try test( + ["": ["1234"]], + 1234 + ) + + // A float. + try test( + ["": ["12.34"]], + 12.34 + ) + + // A bool. + try test( + ["": ["true"]], + true + ) + + // A simple array. + try test( + ["": ["a", "b", "c"]], + ["a", "b", "c"] + ) + +// // A nested array. +// makeCase( +// [["a"], ["b", "c"]], +// .array([ +// .array([ +// .primitive(.string("a")) +// ]), +// .array([ +// .primitive(.string("b")), +// .primitive(.string("c")), +// ]), +// ]) +// ), +// +// // A struct. +// makeCase( +// SimpleStruct(foo: "bar"), +// .dictionary([ +// "foo": .primitive(.string("bar")) +// ]) +// ), +// +// // A nested struct. +// makeCase( +// NestedStruct(simple: SimpleStruct(foo: "bar")), +// .dictionary([ +// "simple": .dictionary([ +// "foo": .primitive(.string("bar")) +// ]) +// ]) +// ), +// +// // An array of structs. +// makeCase( +// [ +// SimpleStruct(foo: "bar"), +// SimpleStruct(foo: "baz"), +// ], +// .array([ +// .dictionary([ +// "foo": .primitive(.string("bar")) +// ]), +// .dictionary([ +// "foo": .primitive(.string("baz")) +// ]), +// ]) +// ), +// +// // An array of arrays of structs. +// makeCase( +// [ +// [ +// SimpleStruct(foo: "bar") +// ], +// [ +// SimpleStruct(foo: "baz") +// ], +// ], +// .array([ +// .array([ +// .dictionary([ +// "foo": .primitive(.string("bar")) +// ]) +// ]), +// .array([ +// .dictionary([ +// "foo": .primitive(.string("baz")) +// ]) +// ]), +// ]) +// ), +// +// // A simple dictionary. +// makeCase( +// ["one": 1, "two": 2], +// .dictionary([ +// "one": .primitive(.integer(1)), +// "two": .primitive(.integer(2)), +// ]) +// ), +// +// // A nested dictionary. +// makeCase( +// [ +// "A": ["one": 1, "two": 2], +// "B": ["three": 3, "four": 4], +// ], +// .dictionary([ +// "A": .dictionary([ +// "one": .primitive(.integer(1)), +// "two": .primitive(.integer(2)), +// ]), +// "B": .dictionary([ +// "three": .primitive(.integer(3)), +// "four": .primitive(.integer(4)), +// ]), +// ]) +// ), +// +// // A dictionary of structs. +// makeCase( +// [ +// "barkey": SimpleStruct(foo: "bar"), +// "bazkey": SimpleStruct(foo: "baz"), +// ], +// .dictionary([ +// "barkey": .dictionary([ +// "foo": .primitive(.string("bar")) +// ]), +// "bazkey": .dictionary([ +// "foo": .primitive(.string("baz")) +// ]), +// ]) +// ), +// +// // An dictionary of dictionaries of structs. +// makeCase( +// [ +// "outBar": +// [ +// "inBar": SimpleStruct(foo: "bar") +// ], +// "outBaz": [ +// "inBaz": SimpleStruct(foo: "baz") +// ], +// ], +// .dictionary([ +// "outBar": .dictionary([ +// "inBar": .dictionary([ +// "foo": .primitive(.string("bar")) +// ]) +// ]), +// "outBaz": .dictionary([ +// "inBaz": .dictionary([ +// "foo": .primitive(.string("baz")) +// ]) +// ]), +// ]) +// ), + + func test( + _ node: URIParsedNode, + _ expectedValue: T, + file: StaticString = #file, + line: UInt = #line + ) throws { + let decoder = URIValueFromNodeDecoder(node: node) + let decodedValue = try decoder.decodeRoot(T.self) + XCTAssertEqual( + decodedValue, + expectedValue, + file: file, + line: line + ) + } + } +} diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIValueToNodeEncoder.swift b/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIValueToNodeEncoder.swift index f334925d..7b3bf23b 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIValueToNodeEncoder.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIValueToNodeEncoder.swift @@ -16,7 +16,7 @@ import XCTest final class Test_URIValueToNodeEncoder: Test_Runtime { - func testTranslating() throws { + func testEncoding() throws { struct Case { var value: any Encodable var expectedNode: URIEncodedNode @@ -225,11 +225,11 @@ final class Test_URIValueToNodeEncoder: Test_Runtime { ]) ), ] - let translator = URIValueToNodeEncoder() + let encoder = URIValueToNodeEncoder() for testCase in cases { - let translatedNode = try translator.encodeValue(testCase.value) + let encodedNode = try encoder.encodeValue(testCase.value) XCTAssertEqual( - translatedNode, + encodedNode, testCase.expectedNode, file: testCase.file, line: testCase.line diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift new file mode 100644 index 00000000..afbe1fad --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift @@ -0,0 +1,240 @@ +//===----------------------------------------------------------------------===// +// +// 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 XCTest +@testable import OpenAPIRuntime + +final class Test_URICodingRoundtrip: Test_Runtime { + + // TODO: Fill these in, even using complicated cases. +// func testRoundtrip() throws { +// struct Case { +// var value: any Encodable +// var expectedNode: URIEncodedNode +// var file: StaticString = #file +// var line: UInt = #line +// } +// func makeCase( +// _ value: any Encodable, +// _ expectedNode: URIEncodedNode, +// file: StaticString = #file, +// line: UInt = #line +// ) +// -> Case +// { +// .init(value: value, expectedNode: expectedNode, file: file, line: line) +// } +// +// struct SimpleStruct: Encodable { +// var foo: String +// var bar: Int? +// } +// +// struct NestedStruct: Encodable { +// var simple: SimpleStruct +// } +// +// let cases: [Case] = [ +// +// // An empty string. +// makeCase( +// "", +// .primitive(.string("")) +// ), +// +// // A string with a space. +// makeCase( +// "Hello World", +// .primitive(.string("Hello World")) +// ), +// +// // An integer. +// makeCase( +// 1234, +// .primitive(.integer(1234)) +// ), +// +// // A float. +// makeCase( +// 12.34, +// .primitive(.double(12.34)) +// ), +// +// // A bool. +// makeCase( +// true, +// .primitive(.bool(true)) +// ), +// +// // A simple array. +// makeCase( +// ["a", "b", "c"], +// .array([ +// .primitive(.string("a")), +// .primitive(.string("b")), +// .primitive(.string("c")), +// ]) +// ), +// +// // A nested array. +// makeCase( +// [["a"], ["b", "c"]], +// .array([ +// .array([ +// .primitive(.string("a")) +// ]), +// .array([ +// .primitive(.string("b")), +// .primitive(.string("c")), +// ]), +// ]) +// ), +// +// // A struct. +// makeCase( +// SimpleStruct(foo: "bar"), +// .dictionary([ +// "foo": .primitive(.string("bar")) +// ]) +// ), +// +// // A nested struct. +// makeCase( +// NestedStruct(simple: SimpleStruct(foo: "bar")), +// .dictionary([ +// "simple": .dictionary([ +// "foo": .primitive(.string("bar")) +// ]) +// ]) +// ), +// +// // An array of structs. +// makeCase( +// [ +// SimpleStruct(foo: "bar"), +// SimpleStruct(foo: "baz"), +// ], +// .array([ +// .dictionary([ +// "foo": .primitive(.string("bar")) +// ]), +// .dictionary([ +// "foo": .primitive(.string("baz")) +// ]), +// ]) +// ), +// +// // An array of arrays of structs. +// makeCase( +// [ +// [ +// SimpleStruct(foo: "bar") +// ], +// [ +// SimpleStruct(foo: "baz") +// ], +// ], +// .array([ +// .array([ +// .dictionary([ +// "foo": .primitive(.string("bar")) +// ]) +// ]), +// .array([ +// .dictionary([ +// "foo": .primitive(.string("baz")) +// ]) +// ]), +// ]) +// ), +// +// // A simple dictionary. +// makeCase( +// ["one": 1, "two": 2], +// .dictionary([ +// "one": .primitive(.integer(1)), +// "two": .primitive(.integer(2)), +// ]) +// ), +// +// // A nested dictionary. +// makeCase( +// [ +// "A": ["one": 1, "two": 2], +// "B": ["three": 3, "four": 4], +// ], +// .dictionary([ +// "A": .dictionary([ +// "one": .primitive(.integer(1)), +// "two": .primitive(.integer(2)), +// ]), +// "B": .dictionary([ +// "three": .primitive(.integer(3)), +// "four": .primitive(.integer(4)), +// ]), +// ]) +// ), +// +// // A dictionary of structs. +// makeCase( +// [ +// "barkey": SimpleStruct(foo: "bar"), +// "bazkey": SimpleStruct(foo: "baz"), +// ], +// .dictionary([ +// "barkey": .dictionary([ +// "foo": .primitive(.string("bar")) +// ]), +// "bazkey": .dictionary([ +// "foo": .primitive(.string("baz")) +// ]), +// ]) +// ), +// +// // An dictionary of dictionaries of structs. +// makeCase( +// [ +// "outBar": +// [ +// "inBar": SimpleStruct(foo: "bar") +// ], +// "outBaz": [ +// "inBaz": SimpleStruct(foo: "baz") +// ], +// ], +// .dictionary([ +// "outBar": .dictionary([ +// "inBar": .dictionary([ +// "foo": .primitive(.string("bar")) +// ]) +// ]), +// "outBaz": .dictionary([ +// "inBaz": .dictionary([ +// "foo": .primitive(.string("baz")) +// ]) +// ]), +// ]) +// ), +// ] +// let encoder = URIValueToNodeEncoder() +// for testCase in cases { +// let encodedNode = try encoder.encodeValue(testCase.value) +// XCTAssertEqual( +// encodedNode, +// testCase.expectedNode, +// file: testCase.file, +// line: testCase.line +// ) +// } +// } +} From e6d4b2fcb49dba3c8c3dd4b315a12ea19110c0cb Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 23 Aug 2023 17:33:34 +0200 Subject: [PATCH 06/17] Decoder working as well now --- .../Common/URICoderConfiguration.swift | 18 +- .../URICoder/Decoding/URIDecoder.swift | 9 +- .../URIValueFromNodeDecoder+Keyed.swift | 218 +++++++++ .../URIValueFromNodeDecoder+Single.swift | 40 +- .../URIValueFromNodeDecoder+Unkeyed.swift | 225 +++++++++ .../Decoding/URIValueFromNodeDecoder.swift | 193 ++++++-- .../URICoder/Encoding/URIEncoder.swift | 6 +- .../URICoder/Parsing/URIParser.swift | 40 +- .../Serialization/URISerializer.swift | 18 +- .../Test_URIValueFromNodeDecoder.swift | 202 +++----- .../Encoding/Test_URIValueToNodeEncoder.swift | 43 +- .../URICoder/Parsing/Test_URIParser.swift | 14 +- .../Serialization/Test_URISerializer.swift | 6 +- .../URICoder/Test_URICodingRoundtrip.swift | 440 +++++++++--------- 14 files changed, 985 insertions(+), 487 deletions(-) create mode 100644 Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift create mode 100644 Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift b/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift index 98fa59eb..746c2b54 100644 --- a/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift +++ b/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift @@ -16,54 +16,54 @@ import Foundation /// A bag of configuration values used by the URI parser and serializer. struct URICoderConfiguration { - + // TODO: Wrap in a struct, as this will grow. // https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#style-values enum Style { case simple case form } - + var style: Style var explode: Bool var spaceEscapingCharacter: String - + private init(style: Style, explode: Bool, spaceEscapingCharacter: String) { self.style = style self.explode = explode self.spaceEscapingCharacter = spaceEscapingCharacter } - + static let formExplode: Self = .init( style: .form, explode: true, spaceEscapingCharacter: "%20" ) - + static let formUnexplode: Self = .init( style: .form, explode: false, spaceEscapingCharacter: "%20" ) - + static let simpleExplode: Self = .init( style: .simple, explode: true, spaceEscapingCharacter: "%20" ) - + static let simpleUnexplode: Self = .init( style: .simple, explode: false, spaceEscapingCharacter: "%20" ) - + static let formDataExplode: Self = .init( style: .form, explode: true, spaceEscapingCharacter: "+" ) - + static let formDataUnexplode: Self = .init( style: .form, explode: false, diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift index fb94448b..d3fa577c 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift @@ -18,9 +18,9 @@ import Foundation /// using the rules from RFC 6570, RFC 1866, and OpenAPI 3.0.3, depending on /// the configuration. struct URIDecoder: Sendable { - + private let configuration: URICoderConfiguration - + init(configuration: URICoderConfiguration) { self.configuration = configuration } @@ -44,7 +44,10 @@ extension URIDecoder { ) throws -> T { var parser = URIParser(configuration: configuration, data: data) let parsedNode = try parser.parseRoot() - let decoder = URIValueFromNodeDecoder(node: parsedNode) + let decoder = URIValueFromNodeDecoder( + node: parsedNode, + explode: configuration.explode + ) return try decoder.decodeRoot() } } diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift new file mode 100644 index 00000000..c3cd50be --- /dev/null +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift @@ -0,0 +1,218 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +struct URIKeyedDecodingContainer { + let decoder: URIValueFromNodeDecoder + let values: URIParsedNode +} + +extension URIKeyedDecodingContainer { + + private func _decodeValue(forKey key: Key) throws -> URIParsedValue { + guard let value = values[key.stringValue[...]]?.first else { + throw DecodingError.keyNotFound( + key, + .init(codingPath: codingPath, debugDescription: "Key not found.") + ) + } + return value + } + + private func _decodeBinaryFloatingPoint( + _: T.Type = T.self, + forKey key: Key + ) throws -> T { + guard let double = Double(try _decodeValue(forKey: key)) else { + throw DecodingError.typeMismatch( + T.self, + .init( + codingPath: codingPath, + debugDescription: "Failed to convert to Double." + ) + ) + } + return T(double) + } + + private func _decodeFixedWidthInteger( + _: T.Type = T.self, + forKey key: Key + ) throws -> T { + guard let parsedValue = T(try _decodeValue(forKey: key)) else { + throw DecodingError.typeMismatch( + T.self, + .init( + codingPath: codingPath, + debugDescription: "Failed to convert to the requested type." + ) + ) + } + return parsedValue + } + + private func _decodeNextLosslessStringConvertible( + _: T.Type = T.self, + forKey key: Key + ) throws -> T { + guard let parsedValue = T(String(try _decodeValue(forKey: key))) else { + throw DecodingError.typeMismatch( + T.self, + .init( + codingPath: codingPath, + debugDescription: "Failed to convert to the requested type." + ) + ) + } + return parsedValue + } +} + +extension URIKeyedDecodingContainer: KeyedDecodingContainerProtocol { + + var allKeys: [Key] { + values.keys.map { key in + Key.init(stringValue: String(key))! + } + } + + func contains(_ key: Key) -> Bool { + values[key.stringValue[...]] != nil + } + + var codingPath: [any CodingKey] { + decoder.codingPath + } + + func decodeNil(forKey key: Key) -> Bool { + false + } + + func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { + try _decodeNextLosslessStringConvertible(forKey: key) + } + + func decode(_ type: String.Type, forKey key: Key) throws -> String { + String(try _decodeValue(forKey: key)) + } + + func decode(_ type: Double.Type, forKey key: Key) throws -> Double { + try _decodeBinaryFloatingPoint(forKey: key) + } + + func decode(_ type: Float.Type, forKey key: Key) throws -> Float { + try _decodeBinaryFloatingPoint(forKey: key) + } + + func decode(_ type: Int.Type, forKey key: Key) throws -> Int { + try _decodeFixedWidthInteger(forKey: key) + } + + func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { + try _decodeFixedWidthInteger(forKey: key) + } + + func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { + try _decodeFixedWidthInteger(forKey: key) + } + + func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { + try _decodeFixedWidthInteger(forKey: key) + } + + func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { + try _decodeFixedWidthInteger(forKey: key) + } + + func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { + try _decodeFixedWidthInteger(forKey: key) + } + + func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { + try _decodeFixedWidthInteger(forKey: key) + } + + func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { + try _decodeFixedWidthInteger(forKey: key) + } + + func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { + try _decodeFixedWidthInteger(forKey: key) + } + + func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { + try _decodeFixedWidthInteger(forKey: key) + } + + func decode(_ type: T.Type, forKey key: Key) throws -> T where T: Decodable { + switch type { + case is Bool.Type: + return try decode(Bool.self, forKey: key) as! T + case is String.Type: + return try decode(String.self, forKey: key) as! T + case is Double.Type: + return try decode(Double.self, forKey: key) as! T + case is Float.Type: + return try decode(Float.self, forKey: key) as! T + case is Int.Type: + return try decode(Int.self, forKey: key) as! T + case is Int8.Type: + return try decode(Int8.self, forKey: key) as! T + case is Int16.Type: + return try decode(Int16.self, forKey: key) as! T + case is Int32.Type: + return try decode(Int32.self, forKey: key) as! T + case is Int64.Type: + return try decode(Int64.self, forKey: key) as! T + case is UInt.Type: + return try decode(UInt.self, forKey: key) as! T + case is UInt8.Type: + return try decode(UInt8.self, forKey: key) as! T + case is UInt16.Type: + return try decode(UInt16.self, forKey: key) as! T + case is UInt32.Type: + return try decode(UInt32.self, forKey: key) as! T + case is UInt64.Type: + return try decode(UInt64.self, forKey: key) as! T + default: + try decoder.push(.init(key)) + defer { + decoder.pop() + } + return try type.init(from: decoder) + } + } + + func nestedContainer( + keyedBy type: NestedKey.Type, + forKey key: Key + ) throws -> KeyedDecodingContainer where NestedKey: CodingKey { + throw URIValueFromNodeDecoder.GeneralError.nestedContainersNotSupported + } + + func nestedUnkeyedContainer( + forKey key: Key + ) throws -> any UnkeyedDecodingContainer { + throw URIValueFromNodeDecoder.GeneralError.nestedContainersNotSupported + } + + func superDecoder(forKey key: Key) throws -> any Decoder { + decoder + } + + func superDecoder() throws -> any Decoder { + decoder + } +} diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift index 5884bca1..37f1981a 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift @@ -14,7 +14,7 @@ import Foundation -struct URISingleValueDecodingContainer { +struct URISingleValueDecodingContainer { let _codingPath: [any CodingKey] let value: URIParsedValue } @@ -49,7 +49,7 @@ extension URISingleValueDecodingContainer { } return parsedValue } - + private func _decodeLosslessStringConvertible( _: T.Type = T.self ) throws -> T { @@ -67,72 +67,72 @@ extension URISingleValueDecodingContainer { } extension URISingleValueDecodingContainer: SingleValueDecodingContainer { - + var codingPath: [any CodingKey] { _codingPath } - + func decodeNil() -> Bool { false } - + func decode(_ type: Bool.Type) throws -> Bool { try _decodeLosslessStringConvertible() } - + func decode(_ type: String.Type) throws -> String { String(value) } - + func decode(_ type: Double.Type) throws -> Double { try _decodeBinaryFloatingPoint() } - + func decode(_ type: Float.Type) throws -> Float { try _decodeBinaryFloatingPoint() } - + func decode(_ type: Int.Type) throws -> Int { try _decodeFixedWidthInteger() } - + func decode(_ type: Int8.Type) throws -> Int8 { try _decodeFixedWidthInteger() } - + func decode(_ type: Int16.Type) throws -> Int16 { try _decodeFixedWidthInteger() } - + func decode(_ type: Int32.Type) throws -> Int32 { try _decodeFixedWidthInteger() } - + func decode(_ type: Int64.Type) throws -> Int64 { try _decodeFixedWidthInteger() } - + func decode(_ type: UInt.Type) throws -> UInt { try _decodeFixedWidthInteger() } - + func decode(_ type: UInt8.Type) throws -> UInt8 { try _decodeFixedWidthInteger() } - + func decode(_ type: UInt16.Type) throws -> UInt16 { try _decodeFixedWidthInteger() } - + func decode(_ type: UInt32.Type) throws -> UInt32 { try _decodeFixedWidthInteger() } - + func decode(_ type: UInt64.Type) throws -> UInt64 { try _decodeFixedWidthInteger() } - - func decode(_ type: T.Type) throws -> T where T : Decodable { + + func decode(_ type: T.Type) throws -> T where T: Decodable { switch type { case is Bool.Type: return try decode(Bool.self) as! T diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift new file mode 100644 index 00000000..8f8a6b79 --- /dev/null +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift @@ -0,0 +1,225 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +struct URIUnkeyedDecodingContainer { + let decoder: URIValueFromNodeDecoder + let values: URIParsedValueArray + private var index: Int + + init(decoder: URIValueFromNodeDecoder, values: URIParsedValueArray) { + self.decoder = decoder + self.values = values + self.index = values.startIndex + } +} + +extension URIUnkeyedDecodingContainer { + + private mutating func _decodingNext(in work: () throws -> R) throws -> R { + guard !isAtEnd else { + throw URIValueFromNodeDecoder.GeneralError.reachedEndOfUnkeyedContainer + } + defer { + values.formIndex(after: &index) + } + return try work() + } + + private mutating func _decodeNext() throws -> URIParsedValue { + try _decodingNext { [values, index] in + values[index] + } + } + + private mutating func _decodeNextBinaryFloatingPoint( + _: T.Type = T.self + ) throws -> T { + guard let double = Double(try _decodeNext()) else { + throw DecodingError.typeMismatch( + T.self, + .init( + codingPath: codingPath, + debugDescription: "Failed to convert to Double." + ) + ) + } + return T(double) + } + + private mutating func _decodeNextFixedWidthInteger( + _: T.Type = T.self + ) throws -> T { + guard let parsedValue = T(try _decodeNext()) else { + throw DecodingError.typeMismatch( + T.self, + .init( + codingPath: codingPath, + debugDescription: "Failed to convert to the requested type." + ) + ) + } + return parsedValue + } + + private mutating func _decodeNextLosslessStringConvertible( + _: T.Type = T.self + ) throws -> T { + guard let parsedValue = T(String(try _decodeNext())) else { + throw DecodingError.typeMismatch( + T.self, + .init( + codingPath: codingPath, + debugDescription: "Failed to convert to the requested type." + ) + ) + } + return parsedValue + } +} + +extension URIUnkeyedDecodingContainer: UnkeyedDecodingContainer { + + var count: Int? { + values.count + } + + var isAtEnd: Bool { + index == values.endIndex + } + + var currentIndex: Int { + index + } + + var codingPath: [any CodingKey] { + decoder.codingPath + } + + func decodeNil() -> Bool { + false + } + + mutating func decode(_ type: Bool.Type) throws -> Bool { + try _decodeNextLosslessStringConvertible() + } + + mutating func decode(_ type: String.Type) throws -> String { + String(try _decodeNext()) + } + + mutating func decode(_ type: Double.Type) throws -> Double { + try _decodeNextBinaryFloatingPoint() + } + + mutating func decode(_ type: Float.Type) throws -> Float { + try _decodeNextBinaryFloatingPoint() + } + + mutating func decode(_ type: Int.Type) throws -> Int { + try _decodeNextFixedWidthInteger() + } + + mutating func decode(_ type: Int8.Type) throws -> Int8 { + try _decodeNextFixedWidthInteger() + } + + mutating func decode(_ type: Int16.Type) throws -> Int16 { + try _decodeNextFixedWidthInteger() + } + + mutating func decode(_ type: Int32.Type) throws -> Int32 { + try _decodeNextFixedWidthInteger() + } + + mutating func decode(_ type: Int64.Type) throws -> Int64 { + try _decodeNextFixedWidthInteger() + } + + mutating func decode(_ type: UInt.Type) throws -> UInt { + try _decodeNextFixedWidthInteger() + } + + mutating func decode(_ type: UInt8.Type) throws -> UInt8 { + try _decodeNextFixedWidthInteger() + } + + mutating func decode(_ type: UInt16.Type) throws -> UInt16 { + try _decodeNextFixedWidthInteger() + } + + mutating func decode(_ type: UInt32.Type) throws -> UInt32 { + try _decodeNextFixedWidthInteger() + } + + mutating func decode(_ type: UInt64.Type) throws -> UInt64 { + try _decodeNextFixedWidthInteger() + } + + mutating func decode(_ type: T.Type) throws -> T where T: Decodable { + switch type { + case is Bool.Type: + return try decode(Bool.self) as! T + case is String.Type: + return try decode(String.self) as! T + case is Double.Type: + return try decode(Double.self) as! T + case is Float.Type: + return try decode(Float.self) as! T + case is Int.Type: + return try decode(Int.self) as! T + case is Int8.Type: + return try decode(Int8.self) as! T + case is Int16.Type: + return try decode(Int16.self) as! T + case is Int32.Type: + return try decode(Int32.self) as! T + case is Int64.Type: + return try decode(Int64.self) as! T + case is UInt.Type: + return try decode(UInt.self) as! T + case is UInt8.Type: + return try decode(UInt8.self) as! T + 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 + default: + return try _decodingNext { [decoder, currentIndex] in + try decoder.push(.init(intValue: currentIndex)) + defer { + decoder.pop() + } + return try type.init(from: decoder) + } + } + } + + mutating func nestedContainer( + keyedBy type: NestedKey.Type + ) throws -> KeyedDecodingContainer where NestedKey: CodingKey { + throw URIValueFromNodeDecoder.GeneralError.nestedContainersNotSupported + } + + mutating func nestedUnkeyedContainer() throws -> any UnkeyedDecodingContainer { + throw URIValueFromNodeDecoder.GeneralError.nestedContainersNotSupported + } + + mutating func superDecoder() throws -> any Decoder { + decoder + } +} diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift index 339a502f..ad59bd94 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift @@ -15,15 +15,17 @@ import Foundation final class URIValueFromNodeDecoder { - + private let node: URIParsedNode + private let explode: Bool private var codingStack: [CodingStackEntry] - - init(node: URIParsedNode) { + + init(node: URIParsedNode, explode: Bool) { self.node = node + self.explode = explode self.codingStack = [] } - + func decodeRoot(_ type: T.Type = T.self) throws -> T { precondition(codingStack.isEmpty) defer { @@ -36,8 +38,13 @@ final class URIValueFromNodeDecoder { extension URIValueFromNodeDecoder { enum GeneralError: Swift.Error { case unsupportedType(Any.Type) + case nestedContainersNotSupported + case reachedEndOfUnkeyedContainer + case codingKeyNotInt + case codingKeyOutOfBounds + case codingKeyNotFound } - + /// An entry in the coding stack for URIValueFromNodeDecoder. /// /// This is used to keep track of where we are in the decode. @@ -45,64 +52,166 @@ extension URIValueFromNodeDecoder { var key: URICoderCodingKey var element: URIParsedNode } - + /// The element at the current head of the coding stack. - private var currentElement: URIParsedNode? { - self.codingStack.last.map { $0.element } + private var currentElement: URIParsedNode { + codingStack.last?.element ?? node } -} -extension URIValueFromNodeDecoder: Decoder { - - var codingPath: [any CodingKey] { - // TODO: Fill in - [] + func push(_ codingKey: URICoderCodingKey) throws { + let nextElement: URIParsedNode + if let intValue = codingKey.intValue { + let value = try nestedValueInCurrentElementAsArray(at: intValue) + nextElement = ["": [value]] + } else { + let value = try nestedValuesInCurrentElementAsDictionary(forKey: codingKey.stringValue) + nextElement = ["": value] + } + codingStack.append(CodingStackEntry(key: codingKey, element: nextElement)) } - - var userInfo: [CodingUserInfoKey : Any] { - [:] + + func pop() { + codingStack.removeLast() } - - func container( - keyedBy type: Key.Type - ) throws -> KeyedDecodingContainer where Key : CodingKey { - fatalError() + + private func throwMismatch(_ message: String) throws -> Never { + throw DecodingError.typeMismatch( + String.self, + .init( + codingPath: codingPath, + debugDescription: message + ) + ) } - - func unkeyedContainer() throws -> any UnkeyedDecodingContainer { - fatalError() + + private func currentElementAsDictionary() throws -> URIParsedNode { + try nodeAsDictionary(currentElement) } - - func singleValueContainer() throws -> any SingleValueDecodingContainer { - - func throwMismatch(_ message: String) throws -> Never { - throw DecodingError.typeMismatch( - String.self, - .init( - codingPath: codingPath, - debugDescription: message - ) - ) + + private func nodeAsDictionary(_ node: URIParsedNode) 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 { + return node + } + let values = try nodeAsArray(node) + guard values.count % 2 == 0 else { + try throwMismatch("Cannot parse an unexploded dictionary an odd number of elements.") + } + 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 + } + + private func currentElementAsArray() throws -> URIParsedValueArray { + try nodeAsArray(currentElement) + } + + private func nodeAsArray(_ node: URIParsedNode) throws -> URIParsedValueArray { + // A valid array represented in a node is a single key-value pair, + // doesn't matter what the key is, and the values are the elements + // of the array. + guard !node.isEmpty else { + try throwMismatch("Cannot parse a value from an empty node.") + } + guard node.count == 1 else { + try throwMismatch("Cannot parse a value from a node with multiple key-value pairs.") } - + let values = node.first!.value + return values + } + + private func currentElementAsSingleValue() throws -> URIParsedValue { + try nodeAsSingleValue(node: currentElement) + } + + private func nodeAsSingleValue(node: URIParsedNode) 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. - guard !node.isEmpty else { - try throwMismatch("Cannot parse a single value from an empty node.") + try throwMismatch("Cannot parse a value from an empty node.") } guard node.count == 1 else { - try throwMismatch("Cannot parse a single value from a node with multiple key-value pairs.") + try throwMismatch("Cannot parse a value from a node with multiple key-value pairs.") } let values = node.first!.value guard !values.isEmpty else { - try throwMismatch("Cannot parse a single value from a node with an empty value array.") + try throwMismatch("Cannot parse a value from a node with an empty value array.") } guard values.count == 1 else { - try throwMismatch("Cannot parse a single value from a node with multiple values.") + try throwMismatch("Cannot parse a value from a node with multiple values.") } let value = values[0] + return value + } + + 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 nestedValuesInCurrentElementAsDictionary( + forKey key: String + ) throws -> URIParsedValueArray { + let values = try currentElementAsDictionary() + guard let value = values[key[...]] else { + throw GeneralError.codingKeyNotFound + } + return value + } +} + +extension URIValueFromNodeDecoder: Decoder { + + var codingPath: [any CodingKey] { + codingStack.map(\.key) + } + + 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 + ) + ) + } + + func unkeyedContainer() throws -> any UnkeyedDecodingContainer { + let values = try currentElementAsArray() + return URIUnkeyedDecodingContainer( + decoder: self, + values: values + ) + } + + func singleValueContainer() throws -> any SingleValueDecodingContainer { + let value = try currentElementAsSingleValue() return URISingleValueDecodingContainer( _codingPath: codingPath, value: value diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift index 374eb3b3..abba2a1c 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift @@ -18,13 +18,13 @@ import Foundation /// using the rules from RFC 6570, RFC 1866, and OpenAPI 3.0.3, depending on /// the configuration. struct URIEncoder: Sendable { - + private let serializer: URISerializer - + init(serializer: URISerializer) { self.serializer = serializer } - + init(configuration: URICoderConfiguration) { self.init(serializer: .init(configuration: configuration)) } diff --git a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift index e33b0f27..acbf6a3b 100644 --- a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift +++ b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift @@ -55,7 +55,7 @@ struct URIParser: Sendable { } } -fileprivate enum ParsingError: Swift.Error { +private enum ParsingError: Swift.Error { case malformedKeyValuePair(String.SubSequence) } @@ -69,7 +69,7 @@ extension URIParser { if data.isEmpty { return ["": [""]] } - + switch (configuration.style, configuration.explode) { case (.form, true): return try parseExplodedFormRoot() @@ -81,12 +81,12 @@ extension URIParser { return try parseUnexplodedSimpleRoot() } } - + private mutating func parseExplodedFormRoot() throws -> URIParsedNode { try parseGenericRoot { data, appendPair in let keyValueSeparator: Character = "=" let pairSeparator: Character = "&" - + while !data.isEmpty { let (firstResult, firstValue) = data.parseUpToEitherCharacterOrEnd( first: keyValueSeparator, @@ -109,13 +109,13 @@ extension URIParser { } } } - + private mutating func parseUnexplodedFormRoot() throws -> URIParsedNode { try parseGenericRoot { data, appendPair in let keyValueSeparator: Character = "=" let pairSeparator: Character = "&" let valueSeparator: Character = "," - + while !data.isEmpty { let (firstResult, firstValue) = data.parseUpToEitherCharacterOrEnd( first: keyValueSeparator, @@ -163,7 +163,7 @@ extension URIParser { try parseGenericRoot { data, appendPair in let keyValueSeparator: Character = "=" let pairSeparator: Character = "," - + while !data.isEmpty { let (firstResult, firstValue) = data.parseUpToEitherCharacterOrEnd( first: keyValueSeparator, @@ -224,14 +224,14 @@ extension URIParser { } return root } - + private func unescapeValue(_ escapedValue: Raw) -> Raw { Self.unescapeValue( escapedValue, spaceEscapingCharacter: configuration.spaceEscapingCharacter ) } - + private static func unescapeValue( _ escapedValue: Raw, spaceEscapingCharacter: String @@ -248,12 +248,12 @@ extension URIParser { // MARK: - Substring utilities extension String.SubSequence { - + fileprivate enum ParseUpToEitherCharacterResult { case foundFirst case foundSecondOrEnd } - + fileprivate mutating func parseUpToEitherCharacterOrEnd( first: Character, second: Character @@ -263,18 +263,17 @@ extension String.SubSequence { return (.foundSecondOrEnd, .init()) } var currentIndex = startIndex - + func finalize( _ result: ParseUpToEitherCharacterResult ) -> (ParseUpToEitherCharacterResult, Self) { let parsed = self[startIndex.. Self { let parsed = self[startIndex.. String { // The space character needs to be encoded based on the config, // so first allow it to be unescaped, and then we'll do a second // pass and only encode the space based on the config. - let partiallyEncoded = unsafeString.addingPercentEncoding( - withAllowedCharacters: .unreservedAndSpace - ) ?? "" + let partiallyEncoded = + unsafeString.addingPercentEncoding( + withAllowedCharacters: .unreservedAndSpace + ) ?? "" let fullyEncoded = partiallyEncoded.replacingOccurrences( of: " ", with: configuration.spaceEscapingCharacter @@ -79,7 +80,7 @@ extension URISerializer { } extension URISerializer { - + private enum SerializationError: Swift.Error { case nestedContainersNotSupported } @@ -224,7 +225,8 @@ extension URISerializer { guard !dictionary.isEmpty else { return } - let sortedDictionary = dictionary + let sortedDictionary = + dictionary .sorted { a, b in a.key.localizedCaseInsensitiveCompare(b.key) == .orderedAscending diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift index c41ef8b2..827c4524 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift @@ -17,9 +17,16 @@ import XCTest final class Test_URIValueFromNodeDecoder: Test_Runtime { func testDecoding() throws { - struct SimpleStruct: Encodable { + struct SimpleStruct: Decodable, Equatable { var foo: String var bar: Int? + var color: SimpleEnum? + } + + enum SimpleEnum: String, Decodable, Equatable { + case red + case green + case blue } // An empty string. @@ -34,177 +41,82 @@ final class Test_URIValueFromNodeDecoder: Test_Runtime { "Hello World" ) + // An enum. + try test( + ["": ["red"]], + SimpleEnum.red + ) + // An integer. try test( ["": ["1234"]], 1234 ) - + // A float. try test( ["": ["12.34"]], 12.34 ) - + // A bool. try test( ["": ["true"]], true ) - // A simple array. + // A simple array of strings. try test( ["": ["a", "b", "c"]], ["a", "b", "c"] ) -// // A nested array. -// makeCase( -// [["a"], ["b", "c"]], -// .array([ -// .array([ -// .primitive(.string("a")) -// ]), -// .array([ -// .primitive(.string("b")), -// .primitive(.string("c")), -// ]), -// ]) -// ), -// -// // A struct. -// makeCase( -// SimpleStruct(foo: "bar"), -// .dictionary([ -// "foo": .primitive(.string("bar")) -// ]) -// ), -// -// // A nested struct. -// makeCase( -// NestedStruct(simple: SimpleStruct(foo: "bar")), -// .dictionary([ -// "simple": .dictionary([ -// "foo": .primitive(.string("bar")) -// ]) -// ]) -// ), -// -// // An array of structs. -// makeCase( -// [ -// SimpleStruct(foo: "bar"), -// SimpleStruct(foo: "baz"), -// ], -// .array([ -// .dictionary([ -// "foo": .primitive(.string("bar")) -// ]), -// .dictionary([ -// "foo": .primitive(.string("baz")) -// ]), -// ]) -// ), -// -// // An array of arrays of structs. -// makeCase( -// [ -// [ -// SimpleStruct(foo: "bar") -// ], -// [ -// SimpleStruct(foo: "baz") -// ], -// ], -// .array([ -// .array([ -// .dictionary([ -// "foo": .primitive(.string("bar")) -// ]) -// ]), -// .array([ -// .dictionary([ -// "foo": .primitive(.string("baz")) -// ]) -// ]), -// ]) -// ), -// -// // A simple dictionary. -// makeCase( -// ["one": 1, "two": 2], -// .dictionary([ -// "one": .primitive(.integer(1)), -// "two": .primitive(.integer(2)), -// ]) -// ), -// -// // A nested dictionary. -// makeCase( -// [ -// "A": ["one": 1, "two": 2], -// "B": ["three": 3, "four": 4], -// ], -// .dictionary([ -// "A": .dictionary([ -// "one": .primitive(.integer(1)), -// "two": .primitive(.integer(2)), -// ]), -// "B": .dictionary([ -// "three": .primitive(.integer(3)), -// "four": .primitive(.integer(4)), -// ]), -// ]) -// ), -// -// // A dictionary of structs. -// makeCase( -// [ -// "barkey": SimpleStruct(foo: "bar"), -// "bazkey": SimpleStruct(foo: "baz"), -// ], -// .dictionary([ -// "barkey": .dictionary([ -// "foo": .primitive(.string("bar")) -// ]), -// "bazkey": .dictionary([ -// "foo": .primitive(.string("baz")) -// ]), -// ]) -// ), -// -// // An dictionary of dictionaries of structs. -// makeCase( -// [ -// "outBar": -// [ -// "inBar": SimpleStruct(foo: "bar") -// ], -// "outBaz": [ -// "inBaz": SimpleStruct(foo: "baz") -// ], -// ], -// .dictionary([ -// "outBar": .dictionary([ -// "inBar": .dictionary([ -// "foo": .primitive(.string("bar")) -// ]) -// ]), -// "outBaz": .dictionary([ -// "inBaz": .dictionary([ -// "foo": .primitive(.string("baz")) -// ]) -// ]), -// ]) -// ), - + // A simple array of enums. + try test( + ["": ["red", "green", "blue"]], + [.red, .green, .blue] as [SimpleEnum] + ) + + // A struct. + try test( + ["foo": ["bar"]], + SimpleStruct(foo: "bar") + ) + + // A struct with a nested enum. + try test( + ["foo": ["bar"], "color": ["blue"]], + SimpleStruct(foo: "bar", color: .blue) + ) + + // A simple dictionary. + try test( + ["one": ["1"], "two": ["2"]], + ["one": 1, "two": 2] + ) + + // A dictionary of enums. + try test( + ["one": ["blue"], "two": ["green"]], + ["one": .blue, "two": .green] as [String: SimpleEnum] + ) + + enum IsExploded: Equatable { + case exploded + case unexploded + } + func test( _ node: URIParsedNode, _ expectedValue: T, + _ isExploded: IsExploded = .exploded, file: StaticString = #file, line: UInt = #line ) throws { - let decoder = URIValueFromNodeDecoder(node: node) + let decoder = URIValueFromNodeDecoder( + node: node, + explode: isExploded == .exploded + ) let decodedValue = try decoder.decodeRoot(T.self) XCTAssertEqual( decodedValue, diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIValueToNodeEncoder.swift b/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIValueToNodeEncoder.swift index 7b3bf23b..d6967014 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIValueToNodeEncoder.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIValueToNodeEncoder.swift @@ -34,9 +34,15 @@ final class Test_URIValueToNodeEncoder: Test_Runtime { .init(value: value, expectedNode: expectedNode, file: file, line: line) } + enum SimpleEnum: String, Encodable { + case foo + case bar + } + struct SimpleStruct: Encodable { var foo: String var bar: Int? + var val: SimpleEnum? } struct NestedStruct: Encodable { @@ -75,7 +81,13 @@ final class Test_URIValueToNodeEncoder: Test_Runtime { .primitive(.bool(true)) ), - // A simple array. + // An enum. + makeCase( + SimpleEnum.foo, + .primitive(.string("foo")) + ), + + // A simple array of strings. makeCase( ["a", "b", "c"], .array([ @@ -85,6 +97,15 @@ final class Test_URIValueToNodeEncoder: Test_Runtime { ]) ), + // A simple array of enums. + makeCase( + [SimpleEnum.foo, SimpleEnum.bar], + .array([ + .primitive(.string("foo")), + .primitive(.string("bar")), + ]) + ), + // A nested array. makeCase( [["a"], ["b", "c"]], @@ -101,9 +122,10 @@ final class Test_URIValueToNodeEncoder: Test_Runtime { // A struct. makeCase( - SimpleStruct(foo: "bar"), + SimpleStruct(foo: "bar", val: .foo), .dictionary([ - "foo": .primitive(.string("bar")) + "foo": .primitive(.string("bar")), + "val": .primitive(.string("foo")), ]) ), @@ -121,14 +143,15 @@ final class Test_URIValueToNodeEncoder: Test_Runtime { makeCase( [ SimpleStruct(foo: "bar"), - SimpleStruct(foo: "baz"), + SimpleStruct(foo: "baz", val: .bar), ], .array([ .dictionary([ "foo": .primitive(.string("bar")) ]), .dictionary([ - "foo": .primitive(.string("baz")) + "foo": .primitive(.string("baz")), + "val": .primitive(.string("bar")), ]), ]) ), @@ -157,7 +180,7 @@ final class Test_URIValueToNodeEncoder: Test_Runtime { ]) ), - // A simple dictionary. + // A simple dictionary of string -> int pairs. makeCase( ["one": 1, "two": 2], .dictionary([ @@ -166,6 +189,14 @@ final class Test_URIValueToNodeEncoder: Test_Runtime { ]) ), + // A simple dictionary of string -> enum pairs. + makeCase( + ["one": SimpleEnum.bar], + .dictionary([ + "one": .primitive(.string("bar")) + ]) + ), + // A nested dictionary. makeCase( [ diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift index 47b11a17..12202ad0 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift @@ -133,7 +133,7 @@ final class Test_URIParser: Test_Runtime { try testVariant(.formUnexplode, variants.formUnexplode) try testVariant(.simpleExplode, variants.simpleExplode) try testVariant(.simpleUnexplode, variants.simpleUnexplode) - try testVariant(.formDataExplode,variants.formDataExplode) + try testVariant(.formDataExplode, variants.formDataExplode) try testVariant(.formDataUnexplode, variants.formDataUnexplode) } } @@ -144,7 +144,7 @@ extension Test_URIParser { struct Variant { var name: String var config: URICoderConfiguration - + static let formExplode: Self = .init( name: "formExplode", config: .formExplode @@ -171,26 +171,26 @@ extension Test_URIParser { ) } struct Variants { - + struct Input: ExpressibleByStringLiteral { var string: String var valueOverride: URIParsedNode? - + init(string: String, valueOverride: URIParsedNode? = nil) { self.string = string self.valueOverride = valueOverride } - + static func custom(_ string: String, value: URIParsedNode) -> Self { .init(string: string, valueOverride: value) } - + init(stringLiteral value: String) { self.string = value self.valueOverride = nil } } - + var formExplode: Input var formUnexplode: Input var simpleExplode: Input diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift b/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift index b578d51c..1e25109b 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift @@ -119,7 +119,7 @@ final class Test_URISerializer: Test_Runtime { value: .dictionary([ "semi": .primitive(.string(";")), "dot": .primitive(.string(".")), - "comma": .primitive(.string(",")) + "comma": .primitive(.string(",")), ]), key: "keys", .init( @@ -151,7 +151,7 @@ final class Test_URISerializer: Test_Runtime { try testVariant(.formUnexplode, testCase.variants.formUnexplode) try testVariant(.simpleExplode, testCase.variants.simpleExplode) try testVariant(.simpleUnexplode, testCase.variants.simpleUnexplode) - try testVariant(.formDataExplode,testCase.variants.formDataExplode) + try testVariant(.formDataExplode, testCase.variants.formDataExplode) try testVariant(.formDataUnexplode, testCase.variants.formDataUnexplode) } } @@ -162,7 +162,7 @@ extension Test_URISerializer { struct Variant { var name: String var config: URICoderConfiguration - + static let formExplode: Self = .init( name: "formExplode", config: .formExplode diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift index afbe1fad..7783f09a 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift @@ -17,224 +17,224 @@ import XCTest final class Test_URICodingRoundtrip: Test_Runtime { // TODO: Fill these in, even using complicated cases. -// func testRoundtrip() throws { -// struct Case { -// var value: any Encodable -// var expectedNode: URIEncodedNode -// var file: StaticString = #file -// var line: UInt = #line -// } -// func makeCase( -// _ value: any Encodable, -// _ expectedNode: URIEncodedNode, -// file: StaticString = #file, -// line: UInt = #line -// ) -// -> Case -// { -// .init(value: value, expectedNode: expectedNode, file: file, line: line) -// } -// -// struct SimpleStruct: Encodable { -// var foo: String -// var bar: Int? -// } -// -// struct NestedStruct: Encodable { -// var simple: SimpleStruct -// } -// -// let cases: [Case] = [ -// -// // An empty string. -// makeCase( -// "", -// .primitive(.string("")) -// ), -// -// // A string with a space. -// makeCase( -// "Hello World", -// .primitive(.string("Hello World")) -// ), -// -// // An integer. -// makeCase( -// 1234, -// .primitive(.integer(1234)) -// ), -// -// // A float. -// makeCase( -// 12.34, -// .primitive(.double(12.34)) -// ), -// -// // A bool. -// makeCase( -// true, -// .primitive(.bool(true)) -// ), -// -// // A simple array. -// makeCase( -// ["a", "b", "c"], -// .array([ -// .primitive(.string("a")), -// .primitive(.string("b")), -// .primitive(.string("c")), -// ]) -// ), -// -// // A nested array. -// makeCase( -// [["a"], ["b", "c"]], -// .array([ -// .array([ -// .primitive(.string("a")) -// ]), -// .array([ -// .primitive(.string("b")), -// .primitive(.string("c")), -// ]), -// ]) -// ), -// -// // A struct. -// makeCase( -// SimpleStruct(foo: "bar"), -// .dictionary([ -// "foo": .primitive(.string("bar")) -// ]) -// ), -// -// // A nested struct. -// makeCase( -// NestedStruct(simple: SimpleStruct(foo: "bar")), -// .dictionary([ -// "simple": .dictionary([ -// "foo": .primitive(.string("bar")) -// ]) -// ]) -// ), -// -// // An array of structs. -// makeCase( -// [ -// SimpleStruct(foo: "bar"), -// SimpleStruct(foo: "baz"), -// ], -// .array([ -// .dictionary([ -// "foo": .primitive(.string("bar")) -// ]), -// .dictionary([ -// "foo": .primitive(.string("baz")) -// ]), -// ]) -// ), -// -// // An array of arrays of structs. -// makeCase( -// [ -// [ -// SimpleStruct(foo: "bar") -// ], -// [ -// SimpleStruct(foo: "baz") -// ], -// ], -// .array([ -// .array([ -// .dictionary([ -// "foo": .primitive(.string("bar")) -// ]) -// ]), -// .array([ -// .dictionary([ -// "foo": .primitive(.string("baz")) -// ]) -// ]), -// ]) -// ), -// -// // A simple dictionary. -// makeCase( -// ["one": 1, "two": 2], -// .dictionary([ -// "one": .primitive(.integer(1)), -// "two": .primitive(.integer(2)), -// ]) -// ), -// -// // A nested dictionary. -// makeCase( -// [ -// "A": ["one": 1, "two": 2], -// "B": ["three": 3, "four": 4], -// ], -// .dictionary([ -// "A": .dictionary([ -// "one": .primitive(.integer(1)), -// "two": .primitive(.integer(2)), -// ]), -// "B": .dictionary([ -// "three": .primitive(.integer(3)), -// "four": .primitive(.integer(4)), -// ]), -// ]) -// ), -// -// // A dictionary of structs. -// makeCase( -// [ -// "barkey": SimpleStruct(foo: "bar"), -// "bazkey": SimpleStruct(foo: "baz"), -// ], -// .dictionary([ -// "barkey": .dictionary([ -// "foo": .primitive(.string("bar")) -// ]), -// "bazkey": .dictionary([ -// "foo": .primitive(.string("baz")) -// ]), -// ]) -// ), -// -// // An dictionary of dictionaries of structs. -// makeCase( -// [ -// "outBar": -// [ -// "inBar": SimpleStruct(foo: "bar") -// ], -// "outBaz": [ -// "inBaz": SimpleStruct(foo: "baz") -// ], -// ], -// .dictionary([ -// "outBar": .dictionary([ -// "inBar": .dictionary([ -// "foo": .primitive(.string("bar")) -// ]) -// ]), -// "outBaz": .dictionary([ -// "inBaz": .dictionary([ -// "foo": .primitive(.string("baz")) -// ]) -// ]), -// ]) -// ), -// ] -// let encoder = URIValueToNodeEncoder() -// for testCase in cases { -// let encodedNode = try encoder.encodeValue(testCase.value) -// XCTAssertEqual( -// encodedNode, -// testCase.expectedNode, -// file: testCase.file, -// line: testCase.line -// ) -// } -// } + // func testRoundtrip() throws { + // struct Case { + // var value: any Encodable + // var expectedNode: URIEncodedNode + // var file: StaticString = #file + // var line: UInt = #line + // } + // func makeCase( + // _ value: any Encodable, + // _ expectedNode: URIEncodedNode, + // file: StaticString = #file, + // line: UInt = #line + // ) + // -> Case + // { + // .init(value: value, expectedNode: expectedNode, file: file, line: line) + // } + // + // struct SimpleStruct: Encodable { + // var foo: String + // var bar: Int? + // } + // + // struct NestedStruct: Encodable { + // var simple: SimpleStruct + // } + // + // let cases: [Case] = [ + // + // // An empty string. + // makeCase( + // "", + // .primitive(.string("")) + // ), + // + // // A string with a space. + // makeCase( + // "Hello World", + // .primitive(.string("Hello World")) + // ), + // + // // An integer. + // makeCase( + // 1234, + // .primitive(.integer(1234)) + // ), + // + // // A float. + // makeCase( + // 12.34, + // .primitive(.double(12.34)) + // ), + // + // // A bool. + // makeCase( + // true, + // .primitive(.bool(true)) + // ), + // + // // A simple array. + // makeCase( + // ["a", "b", "c"], + // .array([ + // .primitive(.string("a")), + // .primitive(.string("b")), + // .primitive(.string("c")), + // ]) + // ), + // + // // A nested array. + // makeCase( + // [["a"], ["b", "c"]], + // .array([ + // .array([ + // .primitive(.string("a")) + // ]), + // .array([ + // .primitive(.string("b")), + // .primitive(.string("c")), + // ]), + // ]) + // ), + // + // // A struct. + // makeCase( + // SimpleStruct(foo: "bar"), + // .dictionary([ + // "foo": .primitive(.string("bar")) + // ]) + // ), + // + // // A nested struct. + // makeCase( + // NestedStruct(simple: SimpleStruct(foo: "bar")), + // .dictionary([ + // "simple": .dictionary([ + // "foo": .primitive(.string("bar")) + // ]) + // ]) + // ), + // + // // An array of structs. + // makeCase( + // [ + // SimpleStruct(foo: "bar"), + // SimpleStruct(foo: "baz"), + // ], + // .array([ + // .dictionary([ + // "foo": .primitive(.string("bar")) + // ]), + // .dictionary([ + // "foo": .primitive(.string("baz")) + // ]), + // ]) + // ), + // + // // An array of arrays of structs. + // makeCase( + // [ + // [ + // SimpleStruct(foo: "bar") + // ], + // [ + // SimpleStruct(foo: "baz") + // ], + // ], + // .array([ + // .array([ + // .dictionary([ + // "foo": .primitive(.string("bar")) + // ]) + // ]), + // .array([ + // .dictionary([ + // "foo": .primitive(.string("baz")) + // ]) + // ]), + // ]) + // ), + // + // // A simple dictionary. + // makeCase( + // ["one": 1, "two": 2], + // .dictionary([ + // "one": .primitive(.integer(1)), + // "two": .primitive(.integer(2)), + // ]) + // ), + // + // // A nested dictionary. + // makeCase( + // [ + // "A": ["one": 1, "two": 2], + // "B": ["three": 3, "four": 4], + // ], + // .dictionary([ + // "A": .dictionary([ + // "one": .primitive(.integer(1)), + // "two": .primitive(.integer(2)), + // ]), + // "B": .dictionary([ + // "three": .primitive(.integer(3)), + // "four": .primitive(.integer(4)), + // ]), + // ]) + // ), + // + // // A dictionary of structs. + // makeCase( + // [ + // "barkey": SimpleStruct(foo: "bar"), + // "bazkey": SimpleStruct(foo: "baz"), + // ], + // .dictionary([ + // "barkey": .dictionary([ + // "foo": .primitive(.string("bar")) + // ]), + // "bazkey": .dictionary([ + // "foo": .primitive(.string("baz")) + // ]), + // ]) + // ), + // + // // An dictionary of dictionaries of structs. + // makeCase( + // [ + // "outBar": + // [ + // "inBar": SimpleStruct(foo: "bar") + // ], + // "outBaz": [ + // "inBaz": SimpleStruct(foo: "baz") + // ], + // ], + // .dictionary([ + // "outBar": .dictionary([ + // "inBar": .dictionary([ + // "foo": .primitive(.string("bar")) + // ]) + // ]), + // "outBaz": .dictionary([ + // "inBaz": .dictionary([ + // "foo": .primitive(.string("baz")) + // ]) + // ]), + // ]) + // ), + // ] + // let encoder = URIValueToNodeEncoder() + // for testCase in cases { + // let encodedNode = try encoder.encodeValue(testCase.value) + // XCTAssertEqual( + // encodedNode, + // testCase.expectedNode, + // file: testCase.file, + // line: testCase.line + // ) + // } + // } } From 1befb30d8167679bfdb258757d401ce3fcabb088 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 24 Aug 2023 09:13:21 +0200 Subject: [PATCH 07/17] Allow caching the parsed result --- .../URICoder/Decoding/URIDecoder.swift | 47 +++++++++- .../URIValueFromNodeDecoder+Single.swift | 6 +- .../Decoding/URIValueFromNodeDecoder.swift | 89 +++++++++++-------- .../URICoder/Decoder/Test_URIDecoder.swift | 6 +- .../Test_URIValueFromNodeDecoder.swift | 64 ++++++++----- 5 files changed, 148 insertions(+), 64 deletions(-) diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift index d3fa577c..ce7286a1 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift @@ -36,16 +36,61 @@ extension URIDecoder { /// /// - Parameters: /// - type: The type to decode. + /// - key: The key of the decoded value. Only used with certain styles + /// and explode options, ignored otherwise. /// - data: The URI-encoded string. /// - Returns: The decoded value. func decode( _ type: T.Type = T.self, + forKey key: String, from data: String ) throws -> T { + try withCachedParser(from: data) { decoder in + try decoder.decode(type, forKey: key) + } + } + + /// Make multiple decode calls on the parsed URI. + /// + /// Use to avoid repeatedly reparsing the raw string. + /// - Parameters: + /// - data: The URI-encoded string. + /// - calls: The closure that contains 0 or more calls to + /// URICachedDecoder's decode method. + /// - Returns: The result of the closure invocation. + func withCachedParser( + from data: String, + 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) + return try calls(decoder) + } +} + +struct URICachedDecoder { + + fileprivate let configuration: URICoderConfiguration + fileprivate let node: URIParsedNode + + /// Attempt to decode an object from an URI 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. + func decode( + _ type: T.Type = T.self, + forKey key: String + ) throws -> T { let decoder = URIValueFromNodeDecoder( - node: parsedNode, + node: node, + rootKey: key[...], explode: configuration.explode ) return try decoder.decodeRoot() diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift index 37f1981a..cae0aabb 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift @@ -15,7 +15,7 @@ import Foundation struct URISingleValueDecodingContainer { - let _codingPath: [any CodingKey] + let codingPath: [any CodingKey] let value: URIParsedValue } @@ -68,10 +68,6 @@ extension URISingleValueDecodingContainer { extension URISingleValueDecodingContainer: SingleValueDecodingContainer { - var codingPath: [any CodingKey] { - _codingPath - } - func decodeNil() -> Bool { false } diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift index ad59bd94..1453bf04 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift @@ -17,11 +17,13 @@ import Foundation final class URIValueFromNodeDecoder { private let node: URIParsedNode + private let rootKey: URIParsedKey private let explode: Bool private var codingStack: [CodingStackEntry] - init(node: URIParsedNode, explode: Bool) { + init(node: URIParsedNode, rootKey: URIParsedKey, explode: Bool) { self.node = node + self.rootKey = rootKey self.explode = explode self.codingStack = [] } @@ -44,28 +46,34 @@ extension URIValueFromNodeDecoder { case codingKeyOutOfBounds case codingKeyNotFound } + + private enum URIDecodedNode { + case single(URIParsedValue) + case array(URIParsedValueArray) + case dictionary(URIParsedNode) + } /// An entry in the coding stack for URIValueFromNodeDecoder. /// /// This is used to keep track of where we are in the decode. private struct CodingStackEntry { var key: URICoderCodingKey - var element: URIParsedNode + var element: URIDecodedNode } /// The element at the current head of the coding stack. - private var currentElement: URIParsedNode { - codingStack.last?.element ?? node + private var currentElement: URIDecodedNode { + codingStack.last?.element ?? .dictionary(node) } func push(_ codingKey: URICoderCodingKey) throws { - let nextElement: URIParsedNode + let nextElement: URIDecodedNode if let intValue = codingKey.intValue { let value = try nestedValueInCurrentElementAsArray(at: intValue) - nextElement = ["": [value]] + nextElement = .single(value) } else { - let value = try nestedValuesInCurrentElementAsDictionary(forKey: codingKey.stringValue) - nextElement = ["": value] + let values = try nestedValuesInCurrentElementAsDictionary(forKey: codingKey.stringValue) + nextElement = .array(values) } codingStack.append(CodingStackEntry(key: codingKey, element: nextElement)) } @@ -83,22 +91,35 @@ extension URIValueFromNodeDecoder { ) ) } + + private func rootValue(in node: URIParsedNode) throws -> URIParsedValueArray { + guard let value = node[rootKey] else { + throw DecodingError.keyNotFound( + URICoderCodingKey(stringValue: String(rootKey)), + .init(codingPath: codingPath, debugDescription: "Value for root key not found.") + ) + } + return value + } private func currentElementAsDictionary() throws -> URIParsedNode { try nodeAsDictionary(currentElement) } - private func nodeAsDictionary(_ node: URIParsedNode) throws -> URIParsedNode { + 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"]] + // 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 { - return node + guard case let .dictionary(values) = node else { + try throwMismatch("Cannot treat a single value or an array as a dictionary.") + } + return values } let values = try nodeAsArray(node) guard values.count % 2 == 0 else { @@ -120,42 +141,38 @@ extension URIValueFromNodeDecoder { try nodeAsArray(currentElement) } - private func nodeAsArray(_ node: URIParsedNode) throws -> URIParsedValueArray { - // A valid array represented in a node is a single key-value pair, - // doesn't matter what the key is, and the values are the elements - // of the array. - guard !node.isEmpty else { - try throwMismatch("Cannot parse a value from an empty node.") - } - guard node.count == 1 else { - try throwMismatch("Cannot parse a value from a node with multiple key-value pairs.") + 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) } - let values = node.first!.value - return values } private func currentElementAsSingleValue() throws -> URIParsedValue { - try nodeAsSingleValue(node: currentElement) + try nodeAsSingleValue(currentElement) } - private func nodeAsSingleValue(node: URIParsedNode) throws -> URIParsedValue { + 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. - guard !node.isEmpty else { - try throwMismatch("Cannot parse a value from an empty node.") - } - guard node.count == 1 else { - try throwMismatch("Cannot parse a value from a node with multiple key-value pairs.") - } - let values = node.first!.value - guard !values.isEmpty else { - try throwMismatch("Cannot parse a value from a node with an empty value array.") + 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) } - guard values.count == 1 else { + guard array.count == 1 else { try throwMismatch("Cannot parse a value from a node with multiple values.") } - let value = values[0] + let value = array[0] return value } @@ -213,7 +230,7 @@ extension URIValueFromNodeDecoder: Decoder { func singleValueContainer() throws -> any SingleValueDecodingContainer { let value = try currentElementAsSingleValue() return URISingleValueDecodingContainer( - _codingPath: codingPath, + codingPath: codingPath, value: value ) } diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift index 5c1c9807..b403cb1e 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift @@ -21,7 +21,11 @@ final class Test_URIDecoder: Test_Runtime { var bar: String } let decoder = URIDecoder(configuration: .formDataExplode) - let decodedValue = try decoder.decode(Foo.self, from: "bar=hello+world") + let decodedValue = try decoder.decode( + Foo.self, + forKey: "", + from: "bar=hello+world" + ) XCTAssertEqual(decodedValue, Foo(bar: "hello world")) } } diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift index 827c4524..4cc222d9 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift @@ -31,74 +31,94 @@ final class Test_URIValueFromNodeDecoder: Test_Runtime { // An empty string. try test( - ["": [""]], - "" + ["root": [""]], + "", + key: "root" ) // A string with a space. try test( - ["": ["Hello World"]], - "Hello World" + ["root": ["Hello World"]], + "Hello World", + key: "root" ) // An enum. try test( - ["": ["red"]], - SimpleEnum.red + ["root": ["red"]], + SimpleEnum.red, + key: "root" ) // An integer. try test( - ["": ["1234"]], - 1234 + ["root": ["1234"]], + 1234, + key: "root" ) // A float. try test( - ["": ["12.34"]], - 12.34 + ["root": ["12.34"]], + 12.34, + key: "root" ) // A bool. try test( - ["": ["true"]], - true + ["root": ["true"]], + true, + key: "root" ) // A simple array of strings. try test( - ["": ["a", "b", "c"]], - ["a", "b", "c"] + ["root": ["a", "b", "c"]], + ["a", "b", "c"], + key: "root" ) // A simple array of enums. try test( - ["": ["red", "green", "blue"]], - [.red, .green, .blue] as [SimpleEnum] + ["root": ["red", "green", "blue"]], + [.red, .green, .blue] as [SimpleEnum], + key: "root" ) // A struct. try test( ["foo": ["bar"]], - SimpleStruct(foo: "bar") + SimpleStruct(foo: "bar"), + key: "root" ) // A struct with a nested enum. try test( ["foo": ["bar"], "color": ["blue"]], - SimpleStruct(foo: "bar", color: .blue) + SimpleStruct(foo: "bar", color: .blue), + key: "root" ) // A simple dictionary. try test( ["one": ["1"], "two": ["2"]], - ["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", + .unexploded ) // A dictionary of enums. try test( ["one": ["blue"], "two": ["green"]], - ["one": .blue, "two": .green] as [String: SimpleEnum] + ["one": .blue, "two": .green] as [String: SimpleEnum], + key: "root" ) enum IsExploded: Equatable { @@ -109,12 +129,14 @@ final class Test_URIValueFromNodeDecoder: Test_Runtime { func test( _ node: URIParsedNode, _ expectedValue: T, + key: String, _ isExploded: IsExploded = .exploded, file: StaticString = #file, line: UInt = #line ) throws { let decoder = URIValueFromNodeDecoder( - node: node, + node: node, + rootKey: key[...], explode: isExploded == .exploded ) let decodedValue = try decoder.decodeRoot(T.self) From fcc413b091432c87e75d6f938189cc6c3f511111 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 24 Aug 2023 09:16:54 +0200 Subject: [PATCH 08/17] Update tests --- .../URICoder/Decoding/URIDecoder.swift | 12 ++++++------ .../URICoder/Decoding/URIValueFromNodeDecoder.swift | 4 ++-- .../OpenAPIRuntime/URICoder/Parsing/URIParser.swift | 6 +----- .../URICoder/Decoder/Test_URIDecoder.swift | 2 +- .../Decoder/Test_URIValueFromNodeDecoder.swift | 8 ++++---- .../URICoder/Parsing/Test_URIParser.swift | 4 ++-- 6 files changed, 16 insertions(+), 20 deletions(-) diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift index ce7286a1..71347ed2 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift @@ -42,16 +42,16 @@ extension URIDecoder { /// - Returns: The decoded value. func decode( _ type: T.Type = T.self, - forKey key: String, + forKey key: String = "", from data: String ) throws -> T { try withCachedParser(from: data) { decoder in try decoder.decode(type, forKey: key) } } - + /// Make multiple decode calls on the parsed URI. - /// + /// /// Use to avoid repeatedly reparsing the raw string. /// - Parameters: /// - data: The URI-encoded string. @@ -70,10 +70,10 @@ extension URIDecoder { } struct URICachedDecoder { - + fileprivate let configuration: URICoderConfiguration fileprivate let node: URIParsedNode - + /// Attempt to decode an object from an URI string. /// /// Under the hood, URICachedDecoder already has a pre-parsed URIParsedNode @@ -86,7 +86,7 @@ struct URICachedDecoder { /// - Returns: The decoded value. func decode( _ type: T.Type = T.self, - forKey key: String + forKey key: String = "" ) throws -> T { let decoder = URIValueFromNodeDecoder( node: node, diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift index 1453bf04..cfeb4e99 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift @@ -46,7 +46,7 @@ extension URIValueFromNodeDecoder { case codingKeyOutOfBounds case codingKeyNotFound } - + private enum URIDecodedNode { case single(URIParsedValue) case array(URIParsedValueArray) @@ -91,7 +91,7 @@ extension URIValueFromNodeDecoder { ) ) } - + private func rootValue(in node: URIParsedNode) throws -> URIParsedValueArray { guard let value = node[rootKey] else { throw DecodingError.keyNotFound( diff --git a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift index acbf6a3b..2158b7d2 100644 --- a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift +++ b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift @@ -63,13 +63,9 @@ private enum ParsingError: Swift.Error { extension URIParser { 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 data.isEmpty { - return ["": [""]] + return [:] } - switch (configuration.style, configuration.explode) { case (.form, true): return try parseExplodedFormRoot() diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift index b403cb1e..8304102c 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift @@ -22,7 +22,7 @@ final class Test_URIDecoder: Test_Runtime { } let decoder = URIDecoder(configuration: .formDataExplode) let decodedValue = try decoder.decode( - Foo.self, + Foo.self, forKey: "", from: "bar=hello+world" ) diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift index 4cc222d9..8d36feba 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift @@ -88,14 +88,14 @@ final class Test_URIValueFromNodeDecoder: Test_Runtime { // A struct. try test( ["foo": ["bar"]], - SimpleStruct(foo: "bar"), + SimpleStruct(foo: "bar"), key: "root" ) // A struct with a nested enum. try test( ["foo": ["bar"], "color": ["blue"]], - SimpleStruct(foo: "bar", color: .blue), + SimpleStruct(foo: "bar", color: .blue), key: "root" ) @@ -105,7 +105,7 @@ final class Test_URIValueFromNodeDecoder: Test_Runtime { ["one": 1, "two": 2], key: "root" ) - + // A unexploded simple dictionary. try test( ["root": ["one", "1", "two", "2"]], @@ -135,7 +135,7 @@ final class Test_URIValueFromNodeDecoder: Test_Runtime { line: UInt = #line ) throws { let decoder = URIValueFromNodeDecoder( - node: node, + node: node, rootKey: key[...], explode: isExploded == .exploded ) diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift index 12202ad0..3204e908 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift @@ -31,8 +31,8 @@ final class Test_URIParser: Test_Runtime { .init( formExplode: "empty=", formUnexplode: "empty=", - simpleExplode: .custom("", value: ["": [""]]), - simpleUnexplode: .custom("", value: ["": [""]]), + simpleExplode: .custom("", value: [:]), + simpleUnexplode: .custom("", value: [:]), formDataExplode: "empty=", formDataUnexplode: "empty=" ), From ed6e073646ec32f61921a3f7e0317525fd8bd295 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 24 Aug 2023 10:01:31 +0200 Subject: [PATCH 09/17] Added rountrip tests --- .../URICoder/Decoding/URIDecoder.swift | 1 + .../Decoding/URIValueFromNodeDecoder.swift | 15 +- .../URICoder/Parsing/URIParser.swift | 4 +- .../Test_URIValueFromNodeDecoder.swift | 21 +- .../URICoder/Parsing/Test_URIParser.swift | 4 +- .../URICoder/Test_URICodingRoundtrip.swift | 481 ++++++++++-------- 6 files changed, 293 insertions(+), 233 deletions(-) diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift index 71347ed2..0c12837b 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift @@ -91,6 +91,7 @@ struct URICachedDecoder { let decoder = URIValueFromNodeDecoder( node: node, rootKey: key[...], + style: configuration.style, explode: configuration.explode ) return try decoder.decodeRoot() diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift index cfeb4e99..92aaee0d 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift @@ -18,12 +18,19 @@ final class URIValueFromNodeDecoder { private let node: URIParsedNode private let rootKey: URIParsedKey + private let style: URICoderConfiguration.Style private let explode: Bool private var codingStack: [CodingStackEntry] - init(node: URIParsedNode, rootKey: URIParsedKey, explode: Bool) { + init( + node: URIParsedNode, + rootKey: URIParsedKey, + style: URICoderConfiguration.Style, + explode: Bool + ) { self.node = node self.rootKey = rootKey + self.style = style self.explode = explode self.codingStack = [] } @@ -94,6 +101,12 @@ extension URIValueFromNodeDecoder { 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 + } throw DecodingError.keyNotFound( URICoderCodingKey(stringValue: String(rootKey)), .init(codingPath: codingPath, debugDescription: "Value for root key not found.") diff --git a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift index 2158b7d2..0e338150 100644 --- a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift +++ b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift @@ -63,8 +63,10 @@ private enum ParsingError: Swift.Error { extension URIParser { 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 data.isEmpty { - return [:] + return ["": [""]] } switch (configuration.style, configuration.explode) { case (.form, true): diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift index 8d36feba..e5964107 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift @@ -36,6 +36,14 @@ final class Test_URIValueFromNodeDecoder: Test_Runtime { key: "root" ) + // An empty string with a simple style. + try test( + ["root": [""]], + "", + key: "root", + style: .simple + ) + // A string with a space. try test( ["root": ["Hello World"]], @@ -111,7 +119,7 @@ final class Test_URIValueFromNodeDecoder: Test_Runtime { ["root": ["one", "1", "two", "2"]], ["one": 1, "two": 2], key: "root", - .unexploded + explode: false ) // A dictionary of enums. @@ -121,23 +129,20 @@ final class Test_URIValueFromNodeDecoder: Test_Runtime { key: "root" ) - enum IsExploded: Equatable { - case exploded - case unexploded - } - func test( _ node: URIParsedNode, _ expectedValue: T, key: String, - _ isExploded: IsExploded = .exploded, + style: URICoderConfiguration.Style = .form, + explode: Bool = true, file: StaticString = #file, line: UInt = #line ) throws { let decoder = URIValueFromNodeDecoder( node: node, rootKey: key[...], - explode: isExploded == .exploded + style: style, + explode: explode ) let decodedValue = try decoder.decodeRoot(T.self) XCTAssertEqual( diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift index 3204e908..12202ad0 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift @@ -31,8 +31,8 @@ final class Test_URIParser: Test_Runtime { .init( formExplode: "empty=", formUnexplode: "empty=", - simpleExplode: .custom("", value: [:]), - simpleUnexplode: .custom("", value: [:]), + simpleExplode: .custom("", value: ["": [""]]), + simpleUnexplode: .custom("", value: ["": [""]]), formDataExplode: "empty=", formDataUnexplode: "empty=" ), diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift index 7783f09a..3d53ec23 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift @@ -16,225 +16,264 @@ import XCTest final class Test_URICodingRoundtrip: Test_Runtime { - // TODO: Fill these in, even using complicated cases. - // func testRoundtrip() throws { - // struct Case { - // var value: any Encodable - // var expectedNode: URIEncodedNode - // var file: StaticString = #file - // var line: UInt = #line - // } - // func makeCase( - // _ value: any Encodable, - // _ expectedNode: URIEncodedNode, - // file: StaticString = #file, - // line: UInt = #line - // ) - // -> Case - // { - // .init(value: value, expectedNode: expectedNode, file: file, line: line) - // } - // - // struct SimpleStruct: Encodable { - // var foo: String - // var bar: Int? - // } - // - // struct NestedStruct: Encodable { - // var simple: SimpleStruct - // } - // - // let cases: [Case] = [ - // - // // An empty string. - // makeCase( - // "", - // .primitive(.string("")) - // ), - // - // // A string with a space. - // makeCase( - // "Hello World", - // .primitive(.string("Hello World")) - // ), - // - // // An integer. - // makeCase( - // 1234, - // .primitive(.integer(1234)) - // ), - // - // // A float. - // makeCase( - // 12.34, - // .primitive(.double(12.34)) - // ), - // - // // A bool. - // makeCase( - // true, - // .primitive(.bool(true)) - // ), - // - // // A simple array. - // makeCase( - // ["a", "b", "c"], - // .array([ - // .primitive(.string("a")), - // .primitive(.string("b")), - // .primitive(.string("c")), - // ]) - // ), - // - // // A nested array. - // makeCase( - // [["a"], ["b", "c"]], - // .array([ - // .array([ - // .primitive(.string("a")) - // ]), - // .array([ - // .primitive(.string("b")), - // .primitive(.string("c")), - // ]), - // ]) - // ), - // - // // A struct. - // makeCase( - // SimpleStruct(foo: "bar"), - // .dictionary([ - // "foo": .primitive(.string("bar")) - // ]) - // ), - // - // // A nested struct. - // makeCase( - // NestedStruct(simple: SimpleStruct(foo: "bar")), - // .dictionary([ - // "simple": .dictionary([ - // "foo": .primitive(.string("bar")) - // ]) - // ]) - // ), - // - // // An array of structs. - // makeCase( - // [ - // SimpleStruct(foo: "bar"), - // SimpleStruct(foo: "baz"), - // ], - // .array([ - // .dictionary([ - // "foo": .primitive(.string("bar")) - // ]), - // .dictionary([ - // "foo": .primitive(.string("baz")) - // ]), - // ]) - // ), - // - // // An array of arrays of structs. - // makeCase( - // [ - // [ - // SimpleStruct(foo: "bar") - // ], - // [ - // SimpleStruct(foo: "baz") - // ], - // ], - // .array([ - // .array([ - // .dictionary([ - // "foo": .primitive(.string("bar")) - // ]) - // ]), - // .array([ - // .dictionary([ - // "foo": .primitive(.string("baz")) - // ]) - // ]), - // ]) - // ), - // - // // A simple dictionary. - // makeCase( - // ["one": 1, "two": 2], - // .dictionary([ - // "one": .primitive(.integer(1)), - // "two": .primitive(.integer(2)), - // ]) - // ), - // - // // A nested dictionary. - // makeCase( - // [ - // "A": ["one": 1, "two": 2], - // "B": ["three": 3, "four": 4], - // ], - // .dictionary([ - // "A": .dictionary([ - // "one": .primitive(.integer(1)), - // "two": .primitive(.integer(2)), - // ]), - // "B": .dictionary([ - // "three": .primitive(.integer(3)), - // "four": .primitive(.integer(4)), - // ]), - // ]) - // ), - // - // // A dictionary of structs. - // makeCase( - // [ - // "barkey": SimpleStruct(foo: "bar"), - // "bazkey": SimpleStruct(foo: "baz"), - // ], - // .dictionary([ - // "barkey": .dictionary([ - // "foo": .primitive(.string("bar")) - // ]), - // "bazkey": .dictionary([ - // "foo": .primitive(.string("baz")) - // ]), - // ]) - // ), - // - // // An dictionary of dictionaries of structs. - // makeCase( - // [ - // "outBar": - // [ - // "inBar": SimpleStruct(foo: "bar") - // ], - // "outBaz": [ - // "inBaz": SimpleStruct(foo: "baz") - // ], - // ], - // .dictionary([ - // "outBar": .dictionary([ - // "inBar": .dictionary([ - // "foo": .primitive(.string("bar")) - // ]) - // ]), - // "outBaz": .dictionary([ - // "inBaz": .dictionary([ - // "foo": .primitive(.string("baz")) - // ]) - // ]), - // ]) - // ), - // ] - // let encoder = URIValueToNodeEncoder() - // for testCase in cases { - // let encodedNode = try encoder.encodeValue(testCase.value) - // XCTAssertEqual( - // encodedNode, - // testCase.expectedNode, - // file: testCase.file, - // line: testCase.line - // ) - // } - // } + func testRoundtrip() throws { + + struct SimpleStruct: Codable, Equatable { + var foo: String + var bar: Int + var color: SimpleEnum + } + + enum SimpleEnum: String, Codable, Equatable { + case red + case green + case blue + } + + // An empty string. + try _test( + "", + key: "root", + .init( + formExplode: "root=", + formUnexplode: "root=", + simpleExplode: "", + simpleUnexplode: "", + formDataExplode: "root=", + formDataUnexplode: "root=" + ) + ) + + // An string with a space. + try _test( + "Hello World!", + key: "root", + .init( + formExplode: "root=Hello%20World%21", + formUnexplode: "root=Hello%20World%21", + simpleExplode: "Hello%20World%21", + simpleUnexplode: "Hello%20World%21", + formDataExplode: "root=Hello+World%21", + formDataUnexplode: "root=Hello+World%21" + ) + ) + + // An enum. + try _test( + SimpleEnum.red, + key: "root", + .init( + formExplode: "root=red", + formUnexplode: "root=red", + simpleExplode: "red", + simpleUnexplode: "red", + formDataExplode: "root=red", + formDataUnexplode: "root=red" + ) + ) + + // An integer. + try _test( + 1234, + key: "root", + .init( + formExplode: "root=1234", + formUnexplode: "root=1234", + simpleExplode: "1234", + simpleUnexplode: "1234", + formDataExplode: "root=1234", + formDataUnexplode: "root=1234" + ) + ) + + // A float. + try _test( + 12.34, + key: "root", + .init( + formExplode: "root=12.34", + formUnexplode: "root=12.34", + simpleExplode: "12.34", + simpleUnexplode: "12.34", + formDataExplode: "root=12.34", + formDataUnexplode: "root=12.34" + ) + ) + + // A bool. + try _test( + true, + key: "root", + .init( + formExplode: "root=true", + formUnexplode: "root=true", + simpleExplode: "true", + simpleUnexplode: "true", + formDataExplode: "root=true", + formDataUnexplode: "root=true" + ) + ) + + // A simple array of strings. + try _test( + ["a", "b", "c"], + key: "list", + .init( + formExplode: "list=a&list=b&list=c", + formUnexplode: "list=a,b,c", + simpleExplode: "a,b,c", + simpleUnexplode: "a,b,c", + formDataExplode: "list=a&list=b&list=c", + formDataUnexplode: "list=a,b,c" + ) + ) + + // A simple array of enums. + try _test( + [.red, .green, .blue] as [SimpleEnum], + key: "list", + .init( + formExplode: "list=red&list=green&list=blue", + formUnexplode: "list=red,green,blue", + simpleExplode: "red,green,blue", + simpleUnexplode: "red,green,blue", + formDataExplode: "list=red&list=green&list=blue", + formDataUnexplode: "list=red,green,blue" + ) + ) + + // A struct. + try _test( + SimpleStruct(foo: "hi!", bar: 24, color: .red), + key: "keys", + .init( + formExplode: "bar=24&color=red&foo=hi%21", + formUnexplode: "keys=bar,24,color,red,foo,hi%21", + simpleExplode: "bar=24,color=red,foo=hi%21", + simpleUnexplode: "bar,24,color,red,foo,hi%21", + formDataExplode: "bar=24&color=red&foo=hi%21", + formDataUnexplode: "keys=bar,24,color,red,foo,hi%21" + ) + ) + + // A simple dictionary. + try _test( + ["foo": "hi!", "bar": "24", "color": "red"], + key: "keys", + .init( + formExplode: "bar=24&color=red&foo=hi%21", + formUnexplode: "keys=bar,24,color,red,foo,hi%21", + simpleExplode: "bar=24,color=red,foo=hi%21", + simpleUnexplode: "bar,24,color,red,foo,hi%21", + formDataExplode: "bar=24&color=red&foo=hi%21", + formDataUnexplode: "keys=bar,24,color,red,foo,hi%21" + ) + ) + } + + struct Variant { + var name: String + var configuration: URICoderConfiguration + + static let formExplode: Self = .init( + name: "formExplode", + configuration: .formExplode + ) + static let formUnexplode: Self = .init( + name: "formUnexplode", + configuration: .formUnexplode + ) + static let simpleExplode: Self = .init( + name: "simpleExplode", + configuration: .simpleExplode + ) + static let simpleUnexplode: Self = .init( + name: "simpleUnexplode", + configuration: .simpleUnexplode + ) + static let formDataExplode: Self = .init( + name: "formDataExplode", + configuration: .formDataExplode + ) + static let formDataUnexplode: Self = .init( + name: "formDataUnexplode", + configuration: .formDataUnexplode + ) + } + struct Variants { + var formExplode: String + var formUnexplode: String + var simpleExplode: String + var simpleUnexplode: String + var formDataExplode: String + var formDataUnexplode: String + } + + func _test( + _ value: T, + key: String, + _ variants: Variants, + file: StaticString = #file, + line: UInt = #line + ) throws { + func testVariant( + name: String, + configuration: URICoderConfiguration, + expectedString: String + ) throws { + let encoder = URIEncoder(configuration: configuration) + let encodedString = try encoder.encode(value, forKey: key) + XCTAssertEqual( + encodedString, + expectedString, + "Variant: \(name)", + file: file, + line: line + ) + let decoder = URIDecoder(configuration: configuration) + let decodedValue = try decoder.decode( + T.self, + forKey: key, + from: encodedString + ) + XCTAssertEqual( + decodedValue, + value, + "Variant: \(name)", + file: file, + line: line + ) + } + try testVariant( + name: "formExplode", + configuration: .formExplode, + expectedString: variants.formExplode + ) + try testVariant( + name: "formUnexplode", + configuration: .formUnexplode, + expectedString: variants.formUnexplode + ) + try testVariant( + name: "simpleExplode", + configuration: .simpleExplode, + expectedString: variants.simpleExplode + ) + try testVariant( + name: "simpleUnexplode", + configuration: .simpleUnexplode, + expectedString: variants.simpleUnexplode + ) + try testVariant( + name: "formDataExplode", + configuration: .formDataExplode, + expectedString: variants.formDataExplode + ) + try testVariant( + name: "formDataUnexplode", + configuration: .formDataUnexplode, + expectedString: variants.formDataUnexplode + ) + } + } From ae171d53e131f3c5fd5480c7473fbf391396a847 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 24 Aug 2023 13:13:53 +0200 Subject: [PATCH 10/17] Improve tests, fix a few bugs around empty containers --- .../Common/URICoderConfiguration.swift | 2 - .../Decoding/URIValueFromNodeDecoder.swift | 10 +- .../URICoder/Parsing/URIParser.swift | 10 +- .../URICoder/Parsing/Test_URIParser.swift | 11 ++ .../URICoder/Test_URICodingRoundtrip.swift | 125 +++++++++++++----- 5 files changed, 119 insertions(+), 39 deletions(-) diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift b/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift index 746c2b54..2ee3394f 100644 --- a/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift +++ b/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift @@ -17,8 +17,6 @@ import Foundation /// A bag of configuration values used by the URI parser and serializer. struct URICoderConfiguration { - // TODO: Wrap in a struct, as this will grow. - // https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#style-values enum Style { case simple case form diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift index 92aaee0d..b6504ce7 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift @@ -107,10 +107,7 @@ extension URIValueFromNodeDecoder { // string key. return valueForFallbackKey } - throw DecodingError.keyNotFound( - URICoderCodingKey(stringValue: String(rootKey)), - .init(codingPath: codingPath, debugDescription: "Value for root key not found.") - ) + return [] } return value } @@ -135,6 +132,11 @@ extension URIValueFromNodeDecoder { 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.") } diff --git a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift index 0e338150..a442105a 100644 --- a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift +++ b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift @@ -64,9 +64,15 @@ private enum ParsingError: Swift.Error { extension URIParser { 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. + // 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 { - return ["": [""]] + switch configuration.style { + case .form: + return [:] + case .simple: + return ["": [""]] + } } switch (configuration.style, configuration.explode) { case (.form, true): diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift index 12202ad0..8ae57d60 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift @@ -40,6 +40,17 @@ final class Test_URIParser: Test_Runtime { "empty": [""] ] ), + makeCase( + .init( + formExplode: "", + formUnexplode: "", + simpleExplode: .custom("", value: ["": [""]]), + simpleUnexplode: .custom("", value: ["": [""]]), + formDataExplode: "", + formDataUnexplode: "" + ), + value: [:] + ), makeCase( .init( formExplode: "who=fred", diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift index 3d53ec23..6293665f 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift @@ -22,6 +22,7 @@ final class Test_URICodingRoundtrip: Test_Runtime { var foo: String var bar: Int var color: SimpleEnum + var empty: String } enum SimpleEnum: String, Codable, Equatable { @@ -127,6 +128,20 @@ final class Test_URICodingRoundtrip: Test_Runtime { formDataUnexplode: "list=a,b,c" ) ) + + // An empty array of strings. + try _test( + [] as [String], + key: "list", + .init( + formExplode: "", + formUnexplode: "", + simpleExplode: .custom("", value: [""]), + simpleUnexplode: .custom("", value: [""]), + formDataExplode: "", + formDataUnexplode: "" + ) + ) // A simple array of enums. try _test( @@ -144,29 +159,58 @@ final class Test_URICodingRoundtrip: Test_Runtime { // A struct. try _test( - SimpleStruct(foo: "hi!", bar: 24, color: .red), + SimpleStruct(foo: "hi!", bar: 24, color: .red, empty: ""), + key: "keys", + .init( + formExplode: "bar=24&color=red&empty=&foo=hi%21", + formUnexplode: "keys=bar,24,color,red,empty,,foo,hi%21", + simpleExplode: "bar=24,color=red,empty=,foo=hi%21", + simpleUnexplode: "bar,24,color,red,empty,,foo,hi%21", + formDataExplode: "bar=24&color=red&empty=&foo=hi%21", + formDataUnexplode: "keys=bar,24,color,red,empty,,foo,hi%21" + ) + ) + + // An empty struct. + struct EmptyStruct: Codable, Equatable { } + try _test( + EmptyStruct(), key: "keys", .init( - formExplode: "bar=24&color=red&foo=hi%21", - formUnexplode: "keys=bar,24,color,red,foo,hi%21", - simpleExplode: "bar=24,color=red,foo=hi%21", - simpleUnexplode: "bar,24,color,red,foo,hi%21", - formDataExplode: "bar=24&color=red&foo=hi%21", - formDataUnexplode: "keys=bar,24,color,red,foo,hi%21" + formExplode: "", + formUnexplode: "", + simpleExplode: "", + simpleUnexplode: "", + formDataExplode: "", + formDataUnexplode: "" ) ) // A simple dictionary. try _test( - ["foo": "hi!", "bar": "24", "color": "red"], + ["foo": "hi!", "bar": "24", "color": "red", "empty": ""], key: "keys", .init( - formExplode: "bar=24&color=red&foo=hi%21", - formUnexplode: "keys=bar,24,color,red,foo,hi%21", - simpleExplode: "bar=24,color=red,foo=hi%21", - simpleUnexplode: "bar,24,color,red,foo,hi%21", - formDataExplode: "bar=24&color=red&foo=hi%21", - formDataUnexplode: "keys=bar,24,color,red,foo,hi%21" + formExplode: "bar=24&color=red&empty=&foo=hi%21", + formUnexplode: "keys=bar,24,color,red,empty,,foo,hi%21", + simpleExplode: "bar=24,color=red,empty=,foo=hi%21", + simpleUnexplode: "bar,24,color,red,empty,,foo,hi%21", + formDataExplode: "bar=24&color=red&empty=&foo=hi%21", + formDataUnexplode: "keys=bar,24,color,red,empty,,foo,hi%21" + ) + ) + + // An empty dictionary. + try _test( + [:] as [String: String], + key: "keys", + .init( + formExplode: "", + formUnexplode: "", + simpleExplode: .custom("", value: ["": ""]), + simpleUnexplode: .custom("", value: ["": ""]), + formDataExplode: "", + formDataUnexplode: "" ) ) } @@ -200,32 +244,51 @@ final class Test_URICodingRoundtrip: Test_Runtime { configuration: .formDataUnexplode ) } - struct Variants { - var formExplode: String - var formUnexplode: String - var simpleExplode: String - var simpleUnexplode: String - var formDataExplode: String - var formDataUnexplode: String + struct Variants { + + struct Input: ExpressibleByStringLiteral { + var string: String + var customValue: T? + + init(string: String, customValue: T?) { + self.string = string + self.customValue = customValue + } + + init(stringLiteral value: String) { + self.init(string: value, customValue: nil) + } + + static func custom(_ string: String, value: T) -> Self { + .init(string: string, customValue: value) + } + } + + var formExplode: Input + var formUnexplode: Input + var simpleExplode: Input + var simpleUnexplode: Input + var formDataExplode: Input + var formDataUnexplode: Input } func _test( _ value: T, key: String, - _ variants: Variants, + _ variants: Variants, file: StaticString = #file, line: UInt = #line ) throws { func testVariant( name: String, configuration: URICoderConfiguration, - expectedString: String + variant: Variants.Input ) throws { let encoder = URIEncoder(configuration: configuration) let encodedString = try encoder.encode(value, forKey: key) XCTAssertEqual( encodedString, - expectedString, + variant.string, "Variant: \(name)", file: file, line: line @@ -238,7 +301,7 @@ final class Test_URICodingRoundtrip: Test_Runtime { ) XCTAssertEqual( decodedValue, - value, + variant.customValue ?? value, "Variant: \(name)", file: file, line: line @@ -247,32 +310,32 @@ final class Test_URICodingRoundtrip: Test_Runtime { try testVariant( name: "formExplode", configuration: .formExplode, - expectedString: variants.formExplode + variant: variants.formExplode ) try testVariant( name: "formUnexplode", configuration: .formUnexplode, - expectedString: variants.formUnexplode + variant: variants.formUnexplode ) try testVariant( name: "simpleExplode", configuration: .simpleExplode, - expectedString: variants.simpleExplode + variant: variants.simpleExplode ) try testVariant( name: "simpleUnexplode", configuration: .simpleUnexplode, - expectedString: variants.simpleUnexplode + variant: variants.simpleUnexplode ) try testVariant( name: "formDataExplode", configuration: .formDataExplode, - expectedString: variants.formDataExplode + variant: variants.formDataExplode ) try testVariant( name: "formDataUnexplode", configuration: .formDataUnexplode, - expectedString: variants.formDataUnexplode + variant: variants.formDataUnexplode ) } From e36c893ae775c070613bf23c540a6059b9a378dc Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 24 Aug 2023 13:14:09 +0200 Subject: [PATCH 11/17] Formatting --- .../URICoder/Test_URICodingRoundtrip.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift index 6293665f..12a8a3b5 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift @@ -128,7 +128,7 @@ final class Test_URICodingRoundtrip: Test_Runtime { formDataUnexplode: "list=a,b,c" ) ) - + // An empty array of strings. try _test( [] as [String], @@ -170,9 +170,9 @@ final class Test_URICodingRoundtrip: Test_Runtime { formDataUnexplode: "keys=bar,24,color,red,empty,,foo,hi%21" ) ) - + // An empty struct. - struct EmptyStruct: Codable, Equatable { } + struct EmptyStruct: Codable, Equatable {} try _test( EmptyStruct(), key: "keys", @@ -199,7 +199,7 @@ final class Test_URICodingRoundtrip: Test_Runtime { formDataUnexplode: "keys=bar,24,color,red,empty,,foo,hi%21" ) ) - + // An empty dictionary. try _test( [:] as [String: String], @@ -245,25 +245,25 @@ final class Test_URICodingRoundtrip: Test_Runtime { ) } struct Variants { - + struct Input: ExpressibleByStringLiteral { var string: String var customValue: T? - + init(string: String, customValue: T?) { self.string = string self.customValue = customValue } - + init(stringLiteral value: String) { self.init(string: value, customValue: nil) } - + static func custom(_ string: String, value: T) -> Self { .init(string: string, customValue: value) } } - + var formExplode: Input var formUnexplode: Input var simpleExplode: Input From a24c805046b2930af0ac919fd44f23f67dc39bb8 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 24 Aug 2023 15:31:23 +0200 Subject: [PATCH 12/17] PR feedback --- .../Common/URICoderConfiguration.swift | 21 ++++++++++++------- .../URICoder/Parsing/URIParser.swift | 4 ++-- .../Serialization/URISerializer.swift | 2 +- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift b/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift index 2ee3394f..f3eae2f7 100644 --- a/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift +++ b/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift @@ -22,11 +22,16 @@ struct URICoderConfiguration { case form } + enum SpaceEscapingCharacter: String { + case percentEncoded = "%20" + case plus = "+" + } + var style: Style var explode: Bool - var spaceEscapingCharacter: String + var spaceEscapingCharacter: SpaceEscapingCharacter - private init(style: Style, explode: Bool, spaceEscapingCharacter: String) { + private init(style: Style, explode: Bool, spaceEscapingCharacter: SpaceEscapingCharacter) { self.style = style self.explode = explode self.spaceEscapingCharacter = spaceEscapingCharacter @@ -35,36 +40,36 @@ struct URICoderConfiguration { static let formExplode: Self = .init( style: .form, explode: true, - spaceEscapingCharacter: "%20" + spaceEscapingCharacter: .percentEncoded ) static let formUnexplode: Self = .init( style: .form, explode: false, - spaceEscapingCharacter: "%20" + spaceEscapingCharacter: .percentEncoded ) static let simpleExplode: Self = .init( style: .simple, explode: true, - spaceEscapingCharacter: "%20" + spaceEscapingCharacter: .percentEncoded ) static let simpleUnexplode: Self = .init( style: .simple, explode: false, - spaceEscapingCharacter: "%20" + spaceEscapingCharacter: .percentEncoded ) static let formDataExplode: Self = .init( style: .form, explode: true, - spaceEscapingCharacter: "+" + spaceEscapingCharacter: .plus ) static let formDataUnexplode: Self = .init( style: .form, explode: false, - spaceEscapingCharacter: "+" + spaceEscapingCharacter: .plus ) } diff --git a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift index a442105a..2155a227 100644 --- a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift +++ b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift @@ -238,11 +238,11 @@ extension URIParser { private static func unescapeValue( _ escapedValue: Raw, - spaceEscapingCharacter: String + spaceEscapingCharacter: URICoderConfiguration.SpaceEscapingCharacter ) -> Raw { // The inverse of URISerializer.computeSafeString. let partiallyDecoded = escapedValue.replacingOccurrences( - of: spaceEscapingCharacter, + of: spaceEscapingCharacter.rawValue, with: " " ) return (partiallyDecoded.removingPercentEncoding ?? "")[...] diff --git a/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift b/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift index 5b6f8405..10424eeb 100644 --- a/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift +++ b/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift @@ -73,7 +73,7 @@ extension URISerializer { ) ?? "" let fullyEncoded = partiallyEncoded.replacingOccurrences( of: " ", - with: configuration.spaceEscapingCharacter + with: configuration.spaceEscapingCharacter.rawValue ) return fullyEncoded } From d8214422de17e7a21dc2dcc8c2294a2a06e5697f Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 25 Aug 2023 10:02:53 +0200 Subject: [PATCH 13/17] Add Date support --- .../Common/URICoderConfiguration.swift | 43 +------------ .../URICoder/Common/URIEncodedNode.swift | 1 + .../URICoder/Decoding/URIDecoder.swift | 3 +- .../URIValueFromNodeDecoder+Keyed.swift | 4 ++ .../URIValueFromNodeDecoder+Single.swift | 3 + .../URIValueFromNodeDecoder+Unkeyed.swift | 4 ++ .../Decoding/URIValueFromNodeDecoder.swift | 19 +++++- .../URIValueToNodeEncoder+Keyed.swift | 2 + .../URIValueToNodeEncoder+Single.swift | 2 + .../URIValueToNodeEncoder+Unkeyed.swift | 2 + .../Encoding/URIValueToNodeEncoder.swift | 12 +++- .../Serialization/URISerializer.swift | 2 + .../Test_URIValueFromNodeDecoder.swift | 3 +- .../URICoder/Test_URICodingRoundtrip.swift | 52 +++++++++++++--- .../URICoder/URICoderTestUtils.swift | 62 +++++++++++++++++++ 15 files changed, 160 insertions(+), 54 deletions(-) create mode 100644 Tests/OpenAPIRuntimeTests/URICoder/URICoderTestUtils.swift diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift b/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift index f3eae2f7..79430798 100644 --- a/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift +++ b/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift @@ -30,46 +30,5 @@ struct URICoderConfiguration { var style: Style var explode: Bool var spaceEscapingCharacter: SpaceEscapingCharacter - - private init(style: Style, explode: Bool, spaceEscapingCharacter: SpaceEscapingCharacter) { - self.style = style - self.explode = explode - self.spaceEscapingCharacter = spaceEscapingCharacter - } - - static let formExplode: Self = .init( - style: .form, - explode: true, - spaceEscapingCharacter: .percentEncoded - ) - - static let formUnexplode: Self = .init( - style: .form, - explode: false, - spaceEscapingCharacter: .percentEncoded - ) - - static let simpleExplode: Self = .init( - style: .simple, - explode: true, - spaceEscapingCharacter: .percentEncoded - ) - - static let simpleUnexplode: Self = .init( - style: .simple, - explode: false, - spaceEscapingCharacter: .percentEncoded - ) - - static let formDataExplode: Self = .init( - style: .form, - explode: true, - spaceEscapingCharacter: .plus - ) - - static let formDataUnexplode: Self = .init( - style: .form, - explode: false, - spaceEscapingCharacter: .plus - ) + var dateTranscoder: any DateTranscoder } diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift b/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift index b3a5b954..8c2f5bca 100644 --- a/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift +++ b/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift @@ -26,6 +26,7 @@ enum URIEncodedNode: Equatable { case string(String) case integer(Int) case double(Double) + case date(Date) } } diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift index 0c12837b..88b3b8c3 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift @@ -92,7 +92,8 @@ struct URICachedDecoder { node: node, rootKey: key[...], style: configuration.style, - explode: configuration.explode + explode: configuration.explode, + dateTranscoder: configuration.dateTranscoder ) return try decoder.decodeRoot() } diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift index c3cd50be..094b5e34 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift @@ -186,6 +186,10 @@ extension URIKeyedDecodingContainer: KeyedDecodingContainerProtocol { return try decode(UInt32.self, forKey: key) as! T 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)) defer { diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift index cae0aabb..96f6ca96 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift @@ -15,6 +15,7 @@ import Foundation struct URISingleValueDecodingContainer { + let dateTranscoder: any DateTranscoder let codingPath: [any CodingKey] let value: URIParsedValue } @@ -158,6 +159,8 @@ extension URISingleValueDecodingContainer: SingleValueDecodingContainer { return try decode(UInt32.self) as! T case is UInt64.Type: return try decode(UInt64.self) as! T + case is Date.Type: + return try dateTranscoder.decode(String(value)) as! T default: throw URIValueFromNodeDecoder.GeneralError.unsupportedType(T.self) } diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift index 8f8a6b79..089d7bbd 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift @@ -198,6 +198,10 @@ extension URIUnkeyedDecodingContainer: UnkeyedDecodingContainer { 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(_decodeNext())) as! T default: return try _decodingNext { [decoder, currentIndex] in try decoder.push(.init(intValue: currentIndex)) diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift index b6504ce7..cc179ae2 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift @@ -20,18 +20,21 @@ final class URIValueFromNodeDecoder { private let rootKey: URIParsedKey private let style: URICoderConfiguration.Style private let explode: Bool + let dateTranscoder: any DateTranscoder private var codingStack: [CodingStackEntry] init( node: URIParsedNode, rootKey: URIParsedKey, style: URICoderConfiguration.Style, - explode: Bool + explode: Bool, + dateTranscoder: any DateTranscoder ) { self.node = node self.rootKey = rootKey self.style = style self.explode = explode + self.dateTranscoder = dateTranscoder self.codingStack = [] } @@ -40,7 +43,18 @@ final class URIValueFromNodeDecoder { defer { precondition(codingStack.isEmpty) } - return try T.init(from: self) + + // We have to catch the special values early, otherwise we fall + // back to their Codable implementations, which don't give us + // a chance to customize the coding in the containers. + let value: T + switch type { + case is Date.Type: + value = try singleValueContainer().decode(Date.self) as! T + default: + value = try T.init(from: self) + } + return value } } @@ -245,6 +259,7 @@ extension URIValueFromNodeDecoder: Decoder { func singleValueContainer() throws -> any SingleValueDecodingContainer { let value = try currentElementAsSingleValue() return URISingleValueDecodingContainer( + dateTranscoder: dateTranscoder, codingPath: codingPath, value: value ) diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift index 4f27fa74..c61a0b31 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift @@ -141,6 +141,8 @@ extension URIKeyedEncodingContainer: KeyedEncodingContainerProtocol { try encode(value, forKey: key) case let value as Bool: try encode(value, forKey: key) + case let value as Date: + try _insertValue(.date(value), atKey: key) default: encoder.push(key: .init(key), newStorage: .unset) try value.encode(to: encoder) diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift index c5bcee82..59d9cecd 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift @@ -131,6 +131,8 @@ extension URISingleValueEncodingContainer { try encode(value) case let value as Bool: try encode(value) + case let value as Date: + try _setValue(.date(value)) default: throw URIValueToNodeEncoder.GeneralError.nestedValueInSingleValueContainer } diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift index 9a86d543..c7ad2f98 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift @@ -160,6 +160,8 @@ extension URIUnkeyedEncodingContainer: UnkeyedEncodingContainer { try encode(value) case let value as Bool: try encode(value) + case let value as Date: + try _appendValue(.date(value)) default: encoder.push(key: .init(intValue: count), newStorage: .unset) try value.encode(to: encoder) diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift index db43036a..9deca44b 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift @@ -51,7 +51,17 @@ final class URIValueToNodeEncoder { storage: .unset ) } - try value.encode(to: self) + + // We have to catch the special values early, otherwise we fall + // back to their Codable implementations, which don't give us + // a chance to customize the coding in the containers. + if let date = value as? Date { + var container = singleValueContainer() + try container.encode(date) + } else { + try value.encode(to: self) + } + let encodedValue = currentStackEntry.storage return encodedValue } diff --git a/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift b/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift index 10424eeb..6b433f6a 100644 --- a/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift +++ b/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift @@ -158,6 +158,8 @@ extension URISerializer { stringValue = int.description case .double(let double): stringValue = double.description + case .date(let date): + stringValue = try configuration.dateTranscoder.encode(date) } data.append(stringValue) } diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift index e5964107..8a67ac0e 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift @@ -142,7 +142,8 @@ final class Test_URIValueFromNodeDecoder: Test_Runtime { node: node, rootKey: key[...], style: style, - explode: explode + explode: explode, + dateTranscoder: .iso8601 ) let decodedValue = try decoder.decodeRoot(T.self) XCTAssertEqual( diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift index 12a8a3b5..5ea841a9 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift @@ -23,6 +23,7 @@ final class Test_URICodingRoundtrip: Test_Runtime { var bar: Int var color: SimpleEnum var empty: String + var date: Date } enum SimpleEnum: String, Codable, Equatable { @@ -115,6 +116,20 @@ final class Test_URICodingRoundtrip: Test_Runtime { ) ) + // A Date. + try _test( + Date(timeIntervalSince1970: 1_692_948_899), + key: "root", + .init( + formExplode: "root=2023-08-25T07:34:59Z", + formUnexplode: "root=2023-08-25T07:34:59Z", + simpleExplode: "2023-08-25T07:34:59Z", + simpleUnexplode: "2023-08-25T07:34:59Z", + formDataExplode: "root=2023-08-25T07:34:59Z", + formDataUnexplode: "root=2023-08-25T07:34:59Z" + ) + ) + // A simple array of strings. try _test( ["a", "b", "c"], @@ -129,6 +144,23 @@ final class Test_URICodingRoundtrip: Test_Runtime { ) ) + // A simple array of dates. + try _test( + [ + Date(timeIntervalSince1970: 1_692_948_899), + Date(timeIntervalSince1970: 1_692_948_901), + ], + key: "list", + .init( + formExplode: "list=2023-08-25T07:34:59Z&list=2023-08-25T07:35:01Z", + formUnexplode: "list=2023-08-25T07:34:59Z,2023-08-25T07:35:01Z", + simpleExplode: "2023-08-25T07:34:59Z,2023-08-25T07:35:01Z", + simpleUnexplode: "2023-08-25T07:34:59Z,2023-08-25T07:35:01Z", + formDataExplode: "list=2023-08-25T07:34:59Z&list=2023-08-25T07:35:01Z", + formDataUnexplode: "list=2023-08-25T07:34:59Z,2023-08-25T07:35:01Z" + ) + ) + // An empty array of strings. try _test( [] as [String], @@ -159,15 +191,21 @@ final class Test_URICodingRoundtrip: Test_Runtime { // A struct. try _test( - SimpleStruct(foo: "hi!", bar: 24, color: .red, empty: ""), + SimpleStruct( + foo: "hi!", + bar: 24, + color: .red, + empty: "", + date: Date(timeIntervalSince1970: 1_692_948_899) + ), key: "keys", .init( - formExplode: "bar=24&color=red&empty=&foo=hi%21", - formUnexplode: "keys=bar,24,color,red,empty,,foo,hi%21", - simpleExplode: "bar=24,color=red,empty=,foo=hi%21", - simpleUnexplode: "bar,24,color,red,empty,,foo,hi%21", - formDataExplode: "bar=24&color=red&empty=&foo=hi%21", - formDataUnexplode: "keys=bar,24,color,red,empty,,foo,hi%21" + formExplode: "bar=24&color=red&date=2023-08-25T07:34:59Z&empty=&foo=hi%21", + formUnexplode: "keys=bar,24,color,red,date,2023-08-25T07:34:59Z,empty,,foo,hi%21", + simpleExplode: "bar=24,color=red,date=2023-08-25T07:34:59Z,empty=,foo=hi%21", + simpleUnexplode: "bar,24,color,red,date,2023-08-25T07:34:59Z,empty,,foo,hi%21", + formDataExplode: "bar=24&color=red&date=2023-08-25T07:34:59Z&empty=&foo=hi%21", + formDataUnexplode: "keys=bar,24,color,red,date,2023-08-25T07:34:59Z,empty,,foo,hi%21" ) ) diff --git a/Tests/OpenAPIRuntimeTests/URICoder/URICoderTestUtils.swift b/Tests/OpenAPIRuntimeTests/URICoder/URICoderTestUtils.swift new file mode 100644 index 00000000..375c266a --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/URICoder/URICoderTestUtils.swift @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +@testable import OpenAPIRuntime + +extension URICoderConfiguration { + + private static let defaultDateTranscoder: any DateTranscoder = .iso8601 + + static let formExplode: Self = .init( + style: .form, + explode: true, + spaceEscapingCharacter: .percentEncoded, + dateTranscoder: defaultDateTranscoder + ) + + static let formUnexplode: Self = .init( + style: .form, + explode: false, + spaceEscapingCharacter: .percentEncoded, + dateTranscoder: defaultDateTranscoder + ) + + static let simpleExplode: Self = .init( + style: .simple, + explode: true, + spaceEscapingCharacter: .percentEncoded, + dateTranscoder: defaultDateTranscoder + ) + + static let simpleUnexplode: Self = .init( + style: .simple, + explode: false, + spaceEscapingCharacter: .percentEncoded, + dateTranscoder: defaultDateTranscoder + ) + + static let formDataExplode: Self = .init( + style: .form, + explode: true, + spaceEscapingCharacter: .plus, + dateTranscoder: defaultDateTranscoder + ) + + static let formDataUnexplode: Self = .init( + style: .form, + explode: false, + spaceEscapingCharacter: .plus, + dateTranscoder: defaultDateTranscoder + ) +} From efb7fc0c24e001e6103c2a8fd3328b6b5e8f9a38 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 25 Aug 2023 14:56:11 +0200 Subject: [PATCH 14/17] WIP on documenting the URI coder --- NOTICE.txt | 2 +- .../URICoder/Common/URICodeCodingKey.swift | 24 +++-- .../Common/URICoderConfiguration.swift | 20 +++- .../URICoder/Common/URIEncodedNode.swift | 45 ++++++++ .../URICoder/Common/URIParsedNode.swift | 7 ++ .../URICoder/Decoding/URIDecoder.swift | 36 ++++--- .../URIValueFromNodeDecoder+Keyed.swift | 28 +++++ .../URIValueFromNodeDecoder+Single.swift | 20 ++++ .../URIValueFromNodeDecoder+Unkeyed.swift | 32 ++++++ .../Decoding/URIValueFromNodeDecoder.swift | 102 +++++++++++++++++- 10 files changed, 291 insertions(+), 25 deletions(-) diff --git a/NOTICE.txt b/NOTICE.txt index ae3e5a4f..7b160cf4 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -35,7 +35,7 @@ This product contains derivations of various scripts and templates from SwiftNIO --- -This product contains a coder implementation inspired by swift-http-structured-headers. +This product contains coder implementations inspired by swift-http-structured-headers. * LICENSE (Apache License 2.0): * https://www.apache.org/licenses/LICENSE-2.0 diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URICodeCodingKey.swift b/Sources/OpenAPIRuntime/URICoder/Common/URICodeCodingKey.swift index cc2f51de..48744267 100644 --- a/Sources/OpenAPIRuntime/URICoder/Common/URICodeCodingKey.swift +++ b/Sources/OpenAPIRuntime/URICoder/Common/URICodeCodingKey.swift @@ -14,11 +14,26 @@ import Foundation -/// The coding key. -struct URICoderCodingKey: CodingKey { +/// The coding key used by the URI encoder and decoder. +struct URICoderCodingKey { + + /// The string to use in a named collection (e.g. a string-keyed dictionary). var stringValue: String + + /// The value to use in an integer-indexed collection (e.g. an int-keyed + /// dictionary). var intValue: Int? + /// Creates a new key with the same string and int value as the provided key. + /// - Parameter key: The key whose values to copy. + init(_ key: some CodingKey) { + self.stringValue = key.stringValue + self.intValue = key.intValue + } +} + +extension URICoderCodingKey: CodingKey { + init(stringValue: String) { self.stringValue = stringValue self.intValue = nil @@ -28,9 +43,4 @@ struct URICoderCodingKey: CodingKey { self.stringValue = "\(intValue)" self.intValue = intValue } - - init(_ key: some CodingKey) { - self.stringValue = key.stringValue - self.intValue = key.intValue - } } diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift b/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift index 79430798..bfb42c48 100644 --- a/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift +++ b/Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift @@ -14,21 +14,39 @@ import Foundation -/// A bag of configuration values used by the URI parser and serializer. +/// 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. enum Style { + + /// A style for simple string variable expansion. case simple + + /// A style for form-based URI expansion. case form } + /// A character used to escape the space character. enum SpaceEscapingCharacter: String { + + /// A percent encoded value for the space character. case percentEncoded = "%20" + + /// The plus character. case plus = "+" } + /// The variable expansion style. 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. var explode: Bool + + /// The character used to escape the space character. var spaceEscapingCharacter: SpaceEscapingCharacter + + /// The coder used for serializing the Date type. var dateTranscoder: any DateTranscoder } diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift b/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift index 8c2f5bca..c1c3132f 100644 --- a/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift +++ b/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift @@ -14,32 +14,67 @@ import Foundation +/// A node produced by `URIValueToNodeEncoder`. enum URIEncodedNode: Equatable { + /// No value. case unset + + /// A single primitive value. case primitive(Primitive) + + /// An array of nodes. case array([Self]) + + /// A dictionary with node values. case dictionary([String: Self]) + /// A primitive value. enum Primitive: Equatable { + + /// A boolean value. case bool(Bool) + + /// A string value. case string(String) + + /// An integer value. case integer(Int) + + /// A floating-point value. case double(Double) + + /// A date value. case date(Date) } } extension URIEncodedNode { + /// An error thrown by the methods modifying `URIEncodedNode`. enum InsertionError: Swift.Error { + + /// The encoder encoded a second primitive value. case settingPrimitiveValueAgain + + /// The encoder set a single value on a container. case settingValueOnAContainer + + /// The encoder appended to a node that wasn't an array. case appendingToNonArrayContainer + + /// The encoder inserted a value for key into a node that wasn't + /// a dictionary. case insertingChildValueIntoNonContainer + + /// The encoder added a value to an array, but the key was not a valid + /// integer key. case insertingChildValueIntoArrayUsingNonIntValueKey } + /// Sets the node to be a primitive node with the provided value. + /// - Parameter value: The primitive value to set into the node. + /// - Throws: If the node is already set. mutating func set(_ value: Primitive) throws { switch self { case .unset: @@ -51,6 +86,13 @@ extension URIEncodedNode { } } + /// Inserts a value for a key into the node, which is interpreted as a + /// dictionary. + /// - Parameters: + /// - childValue: The value to save under the provided key. + /// - key: The key to save the value for into the dictionary. + /// - Throws: If the node is already set to be anything else but a + /// dictionary. mutating func insert( _ childValue: Self, atKey key: Key @@ -85,6 +127,9 @@ extension URIEncodedNode { } } + /// 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. mutating func append(_ childValue: Self) throws { switch self { case .array(var items): diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URIParsedNode.swift b/Sources/OpenAPIRuntime/URICoder/Common/URIParsedNode.swift index ee6b7246..51ecbe2a 100644 --- a/Sources/OpenAPIRuntime/URICoder/Common/URIParsedNode.swift +++ b/Sources/OpenAPIRuntime/URICoder/Common/URIParsedNode.swift @@ -14,7 +14,14 @@ import Foundation +/// The type used for keys by `URIParser`. typealias URIParsedKey = String.SubSequence + +/// The type used for values by `URIParser`. 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] diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift index 88b3b8c3..3c22380b 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift @@ -19,8 +19,12 @@ import Foundation /// the configuration. struct URIDecoder: Sendable { + /// The configuration instructing the decoder how to interpret the raw + /// string. private let configuration: URICoderConfiguration + /// Creates a new decoder with the provided configuration. + /// - Parameter configuration: The configuration used by the decoder. init(configuration: URICoderConfiguration) { self.configuration = configuration } @@ -30,15 +34,15 @@ 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. + /// 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 - /// and explode options, ignored otherwise. - /// - data: The URI-encoded string. + /// - type: The type to decode. + /// - key: The key of the decoded value. Only used with certain styles + /// and explode options, ignored otherwise. + /// - data: The URI-encoded string. /// - Returns: The decoded value. func decode( _ type: T.Type = T.self, @@ -56,7 +60,7 @@ extension URIDecoder { /// - Parameters: /// - data: The URI-encoded string. /// - calls: The closure that contains 0 or more calls to - /// URICachedDecoder's decode method. + /// the `decode` method on `URICachedDecoder`. /// - Returns: The result of the closure invocation. func withCachedParser( from data: String, @@ -71,18 +75,22 @@ extension URIDecoder { 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 string. + /// 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. + /// 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. + /// - 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. func decode( _ type: T.Type = T.self, diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift index 094b5e34..03485d6b 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift @@ -14,13 +14,23 @@ import Foundation +/// A keyed container used by `URIValueFromNodeDecoder`. struct URIKeyedDecodingContainer { + + /// The associated decoder. let decoder: URIValueFromNodeDecoder + + /// The underlying dictionary. let values: URIParsedNode } extension URIKeyedDecodingContainer { + /// Returns the value found for the provided key in the underlying + /// dictionary. + /// - Parameter key: The key for which to return the value. + /// - 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 { throw DecodingError.keyNotFound( @@ -31,6 +41,12 @@ extension URIKeyedDecodingContainer { return value } + /// Returns the value found for the provided key in the underlying + /// dictionary converted to the provided type. + /// - Parameter key: The key for which to return the value. + /// - Returns: The converted value found for the provided key. + /// - Throws: An error if no value for the key was found or if the + /// conversion failed. private func _decodeBinaryFloatingPoint( _: T.Type = T.self, forKey key: Key @@ -47,6 +63,12 @@ extension URIKeyedDecodingContainer { return T(double) } + /// Returns the value found for the provided key in the underlying + /// dictionary converted to the provided type. + /// - Parameter key: The key for which to return the value. + /// - Returns: The converted value found for the provided key. + /// - Throws: An error if no value for the key was found or if the + /// conversion failed. private func _decodeFixedWidthInteger( _: T.Type = T.self, forKey key: Key @@ -63,6 +85,12 @@ extension URIKeyedDecodingContainer { return parsedValue } + /// Returns the value found for the provided key in the underlying + /// dictionary converted to the provided type. + /// - Parameter key: The key for which to return the value. + /// - Returns: The converted value found for the provided key. + /// - Throws: An error if no value for the key was found or if the + /// conversion failed. private func _decodeNextLosslessStringConvertible( _: T.Type = T.self, forKey key: Key diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift index 96f6ca96..5c3e0ad2 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift @@ -14,13 +14,25 @@ import Foundation +/// A single value container used by `URIValueFromNodeDecoder`. struct URISingleValueDecodingContainer { + + /// The coder used to serialize Date values. let dateTranscoder: any DateTranscoder + + /// The coding path of the container. let codingPath: [any CodingKey] + + /// The underlying value. let value: URIParsedValue } extension URISingleValueDecodingContainer { + + /// Returns the value found in the underlying node converted to + /// the provided type. + /// - Returns: The converted value found. + /// - Throws: An error if the conversion failed. private func _decodeBinaryFloatingPoint( _: T.Type = T.self ) throws -> T { @@ -36,6 +48,10 @@ extension URISingleValueDecodingContainer { return T(double) } + /// Returns the value found in the underlying node converted to + /// the provided type. + /// - Returns: The converted value found. + /// - Throws: An error if the conversion failed. private func _decodeFixedWidthInteger( _: T.Type = T.self ) throws -> T { @@ -51,6 +67,10 @@ extension URISingleValueDecodingContainer { return parsedValue } + /// Returns the value found in the underlying node converted to + /// the provided type. + /// - Returns: The converted value found. + /// - Throws: An error if the conversion failed. private func _decodeLosslessStringConvertible( _: T.Type = T.self ) throws -> T { diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift index 089d7bbd..abb55d7a 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift @@ -14,11 +14,22 @@ 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 @@ -28,6 +39,11 @@ struct URIUnkeyedDecodingContainer { extension URIUnkeyedDecodingContainer { + /// Returns the result from the provided closure run on the current + /// item in the underlying array and increments the index. + /// - Parameter work: The closure of work to run for the current item. + /// - Returns: The result of the closure. + /// - 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 @@ -38,12 +54,20 @@ extension URIUnkeyedDecodingContainer { return try work() } + /// Returns the the current item in the underlying array and increments + /// the index. + /// - 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] } } + /// Returns the next value converted to the provided type. + /// - Returns: The converted value. + /// - Throws: An error if the container ran out of items or if + /// the conversion failed. private mutating func _decodeNextBinaryFloatingPoint( _: T.Type = T.self ) throws -> T { @@ -59,6 +83,10 @@ extension URIUnkeyedDecodingContainer { return T(double) } + /// Returns the next value converted to the provided type. + /// - Returns: The converted value. + /// - Throws: An error if the container ran out of items or if + /// the conversion failed. private mutating func _decodeNextFixedWidthInteger( _: T.Type = T.self ) throws -> T { @@ -74,6 +102,10 @@ extension URIUnkeyedDecodingContainer { return parsedValue } + /// Returns the next value converted to the provided type. + /// - Returns: The converted value. + /// - Throws: An error if the container ran out of items or if + /// the conversion failed. private mutating func _decodeNextLosslessStringConvertible( _: T.Type = T.self ) throws -> T { diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift index cc179ae2..772123eb 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift @@ -14,15 +14,34 @@ import Foundation +/// A type that allows decoding `Decodable` values from a `URIParsedNode`. final class URIValueFromNodeDecoder { + /// The coder used for serializing Date values. + let dateTranscoder: any DateTranscoder + + /// The underlying root node. private let node: URIParsedNode + + /// 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 dateTranscoder: any DateTranscoder + + /// The stack of nested values within the root node. private var codingStack: [CodingStackEntry] + /// Creates a new decoder. + /// - Parameters: + /// - node: The underlying root node. + /// - 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, @@ -38,6 +57,10 @@ final class URIValueFromNodeDecoder { self.codingStack = [] } + /// Decodes the provided type from the root node. + /// - Parameter type: The type to decode from the decoder. + /// - Returns: The decoded value. + /// - Throws: When a decoding error occurs. func decodeRoot(_ type: T.Type = T.self) throws -> T { precondition(codingStack.isEmpty) defer { @@ -59,26 +82,53 @@ final class URIValueFromNodeDecoder { } extension URIValueFromNodeDecoder { + + /// A decoder error. enum GeneralError: Swift.Error { + + /// The decoder does not support the provided type. case unsupportedType(Any.Type) + + /// The decoder was asked to create a nested container. case nestedContainersNotSupported + + /// 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. + /// 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 + + /// The node at the key inside its parent. var element: URIDecodedNode } @@ -87,6 +137,10 @@ extension URIValueFromNodeDecoder { 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. func push(_ codingKey: URICoderCodingKey) throws { let nextElement: URIDecodedNode if let intValue = codingKey.intValue { @@ -99,10 +153,15 @@ extension URIValueFromNodeDecoder { codingStack.append(CodingStackEntry(key: codingKey, element: nextElement)) } + /// Pops the top container from the stack and restores the previously top + /// container to be the current top container. func pop() { codingStack.removeLast() } + /// Throws a type mismatch error with the provided message. + /// - Parameter message: The message to be embedded as debug description + /// inside the thrown `DecodingError`. private func throwMismatch(_ message: String) throws -> Never { throw DecodingError.typeMismatch( String.self, @@ -113,6 +172,9 @@ extension URIValueFromNodeDecoder { ) } + /// 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. private func rootValue(in node: URIParsedNode) throws -> URIParsedValueArray { guard let value = node[rootKey] else { if style == .simple, let valueForFallbackKey = node[""] { @@ -126,10 +188,18 @@ extension URIValueFromNodeDecoder { return 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. 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. @@ -166,10 +236,18 @@ extension URIValueFromNodeDecoder { return convertedNode } + /// 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. 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): @@ -181,10 +259,18 @@ extension URIValueFromNodeDecoder { } } + /// 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. private 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 @@ -205,6 +291,12 @@ extension URIValueFromNodeDecoder { 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 { @@ -215,6 +307,12 @@ extension URIValueFromNodeDecoder { return values[index] } + /// 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 { From 1eec75f183a3da052dc16c96666a1e1dfbcb903e Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 25 Aug 2023 16:08:29 +0200 Subject: [PATCH 15/17] Finished documenting the URI coder --- .../URICoder/Decoding/URIDecoder.swift | 30 ++++- .../URICoder/Encoding/URIEncoder.swift | 49 ++++++- .../URIValueToNodeEncoder+Keyed.swift | 24 ++++ .../URIValueToNodeEncoder+Single.swift | 14 +- .../URIValueToNodeEncoder+Unkeyed.swift | 12 ++ .../Encoding/URIValueToNodeEncoder.swift | 70 +++++++--- .../URICoder/Parsing/URIParser.swift | 95 +++++++++----- .../Serialization/URISerializer.swift | 122 +++++++++++------- 8 files changed, 309 insertions(+), 107 deletions(-) diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift index 3c22380b..d53c0341 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift @@ -14,9 +14,37 @@ import Foundation -/// A type that decodes a `Decodable` objects from an URI-encoded string +/// 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 /// the configuration. +/// +/// [RFC 6570 - Form-style query expansion.](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.8) +/// +/// | Example Template | Expansion | +/// | ---------------- | ----------------------------------| +/// | `{?who}` | `?who=fred` | +/// | `{?half}` | `?half=50%25` | +/// | `{?x,y}` | `?x=1024&y=768` | +/// | `{?x,y,empty}` | `?x=1024&y=768&empty=` | +/// | `{?x,y,undef}` | `?x=1024&y=768` | +/// | `{?list}` | `?list=red,green,blue` | +/// | `{?list\*}` | `?list=red&list=green&list=blue` | +/// | `{?keys}` | `?keys=semi,%3B,dot,.,comma,%2C` | +/// | `{?keys\*}` | `?semi=%3B&dot=.&comma=%2C` | +/// +/// [RFC 6570 - Simple string expansion.](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.2) +/// +/// | Example Template | Expansion | +/// | ---------------- | ----------------------------------| +/// | `{hello}` | `Hello%20World%21` | +/// | `{half}` | `50%25` | +/// | `{x,y}` | `1024,768` | +/// | `{x,empty}` | `1024,` | +/// | `{x,undef}` | `1024` | +/// | `{list}` | `red,green,blue` | +/// | `{list\*}` | `red,green,blue` | +/// | `{keys}` | `semi,%3B,dot,.,comma,%2C` | +/// | `{keys\*}` | `semi=%3B,dot=.,comma=%2C` | struct URIDecoder: Sendable { /// The configuration instructing the decoder how to interpret the raw diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift index abba2a1c..744d7a70 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift @@ -14,17 +14,52 @@ import Foundation -/// A type that encodes an `Encodable` objects to an URI-encoded string +/// 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 /// the configuration. +/// +/// [RFC 6570 - Form-style query expansion.](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.8) +/// +/// | Example Template | Expansion | +/// | ---------------- | ----------------------------------| +/// | `{?who}` | `?who=fred` | +/// | `{?half}` | `?half=50%25` | +/// | `{?x,y}` | `?x=1024&y=768` | +/// | `{?x,y,empty}` | `?x=1024&y=768&empty=` | +/// | `{?x,y,undef}` | `?x=1024&y=768` | +/// | `{?list}` | `?list=red,green,blue` | +/// | `{?list\*}` | `?list=red&list=green&list=blue` | +/// | `{?keys}` | `?keys=semi,%3B,dot,.,comma,%2C` | +/// | `{?keys\*}` | `?semi=%3B&dot=.&comma=%2C` | +/// +/// [RFC 6570 - Simple string expansion.](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.2) +/// +/// | Example Template | Expansion | +/// | ---------------- | ----------------------------------| +/// | `{hello}` | `Hello%20World%21` | +/// | `{half}` | `50%25` | +/// | `{x,y}` | `1024,768` | +/// | `{x,empty}` | `1024,` | +/// | `{x,undef}` | `1024` | +/// | `{list}` | `red,green,blue` | +/// | `{list\*}` | `red,green,blue` | +/// | `{keys}` | `semi,%3B,dot,.,comma,%2C` | +/// | `{keys\*}` | `semi=%3B,dot=.,comma=%2C` | struct URIEncoder: Sendable { + /// The serializer used to turn `URIEncodedNode` values to a string. private let serializer: URISerializer + /// Creates a new encoder. + /// - Parameter serializer: The serializer used to turn `URIEncodedNode` + /// values to a string. init(serializer: URISerializer) { self.serializer = serializer } + /// Creates a new encoder. + /// - Parameter configuration: The configuration instructing the encoder + /// how to serialize the value into an URI-encoded string. init(configuration: URICoderConfiguration) { self.init(serializer: .init(configuration: configuration)) } @@ -34,15 +69,15 @@ extension URIEncoder { /// Attempt to encode an object into an URI string. /// - /// Under the hood, URIEncoder first encodes the Encodable type - /// into a URIEncodableNode using URIValueToNodeEncoder, and then - /// URISerializer encodes the URIEncodableNode into a string based + /// Under the hood, `URIEncoder` first encodes the `Encodable` type + /// into a `URIEncodableNode` using `URIValueToNodeEncoder`, and then + /// `URISerializer` encodes the `URIEncodableNode` into a string based /// on the configured behavior. /// /// - Parameters: - /// - value: The value to encode. - /// - key: The key for which to encode the value. Can be an empty key, - /// in which case you still get a key-value pair, like `=foo`. + /// - value: The value to encode. + /// - key: The key for which to encode the value. Can be an empty key, + /// in which case you still get a key-value pair, like `=foo`. /// - Returns: The URI string. func encode( _ value: some Encodable, diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift index c61a0b31..06810db6 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift @@ -14,19 +14,38 @@ import Foundation +/// A keyed container used by `URIValueToNodeEncoder`. struct URIKeyedEncodingContainer { + + /// The associated encoder. let encoder: URIValueToNodeEncoder } extension URIKeyedEncodingContainer { + + /// Inserts the provided node into the underlying dictionary at + /// the provided key. + /// - Parameters: + /// - node: The child node to insert. + /// - key: The key for the child node. private func _insertValue(_ node: URIEncodedNode, atKey key: Key) throws { try encoder.currentStackEntry.storage.insert(node, atKey: key) } + /// Inserts the provided primitive value into the underlying dictionary at + /// the provided key. + /// - Parameters: + /// - node: The primitive value to insert. + /// - key: The key for the value. private func _insertValue(_ node: URIEncodedNode.Primitive, atKey key: Key) throws { try _insertValue(.primitive(node), atKey: key) } + /// Inserts the provided value into the underlying dictionary at + /// the provided key. + /// - Parameters: + /// - node: The value to insert. + /// - key: The key for the value. private func _insertBinaryFloatingPoint( _ value: some BinaryFloatingPoint, atKey key: Key @@ -34,6 +53,11 @@ extension URIKeyedEncodingContainer { try _insertValue(.double(Double(value)), atKey: key) } + /// Inserts the provided value into the underlying dictionary at + /// the provided key. + /// - Parameters: + /// - node: The value to insert. + /// - key: The key for the value. private func _insertFixedWidthInteger( _ value: some FixedWidthInteger, atKey key: Key diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift index 59d9cecd..f661ca61 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift @@ -14,19 +14,29 @@ import Foundation -struct URISingleValueEncodingContainer: SingleValueEncodingContainer { +/// A single value container used by `URIValueToNodeEncoder`. +struct URISingleValueEncodingContainer { + + /// The associated encoder. let encoder: URIValueToNodeEncoder } extension URISingleValueEncodingContainer { + + /// Sets the provided primitive value to the underlying node. + /// - Parameter node: The primitive value to set. private func _setValue(_ node: URIEncodedNode.Primitive) throws { try encoder.currentStackEntry.storage.set(node) } + /// Sets the provided value to the underlying node. + /// - Parameter node: The value to set. private func _setBinaryFloatingPoint(_ value: some BinaryFloatingPoint) throws { try _setValue(.double(Double(value))) } + /// Sets the provided value to the underlying node. + /// - Parameter node: The value to set. private func _setFixedWidthInteger(_ value: some FixedWidthInteger) throws { guard let validatedValue = Int(exactly: value) else { throw URIValueToNodeEncoder.GeneralError.integerOutOfRange @@ -35,7 +45,7 @@ extension URISingleValueEncodingContainer { } } -extension URISingleValueEncodingContainer { +extension URISingleValueEncodingContainer: SingleValueEncodingContainer { var codingPath: [any CodingKey] { encoder.codingPath diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift index c7ad2f98..ee63f135 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift @@ -14,23 +14,35 @@ import Foundation +/// An unkeyed container used by `URIValueToNodeEncoder`. struct URIUnkeyedEncodingContainer { + + /// The associated encoder. let encoder: URIValueToNodeEncoder } extension URIUnkeyedEncodingContainer { + + /// Appends the provided node to the underlying array. + /// - Parameter node: The node to append. private func _appendValue(_ node: URIEncodedNode) throws { try encoder.currentStackEntry.storage.append(node) } + /// Appends the provided primitive value as a node to the underlying array. + /// - Parameter node: The value to append. private func _appendValue(_ node: URIEncodedNode.Primitive) throws { try _appendValue(.primitive(node)) } + /// Appends the provided value as a node to the underlying array. + /// - Parameter node: The value to append. private func _appendBinaryFloatingPoint(_ value: some BinaryFloatingPoint) throws { try _appendValue(.double(Double(value))) } + /// Appends the provided value as a node to the underlying array. + /// - Parameter node: The value to append. private func _appendFixedWidthInteger(_ value: some FixedWidthInteger) throws { guard let validatedValue = Int(exactly: value) else { throw URIValueToNodeEncoder.GeneralError.integerOutOfRange diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift index 9deca44b..919de179 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift @@ -14,27 +14,45 @@ import Foundation -/// Converts an Encodable type into a URIEncodableNode. +/// A type that converts an `Encodable` type into a `URIEncodableNode` value. final class URIValueToNodeEncoder { - /// An entry in the coding stack for \_URIEncoder. + /// An entry in the coding stack for `URIEncoder`. /// /// This is used to keep track of where we are in the encode. struct CodingStackEntry { + + /// The key at which to write the node. var key: URICoderCodingKey + + /// The node at the key inside its parent. var storage: URIEncodedNode } + /// An encoder error. enum GeneralError: Swift.Error { + + /// The encoder set a nil value, which isn't supported. case nilNotSupported + + /// The encoder set a Data value, which isn't supported. case dataNotSupported - case invalidEncoderCallForValue + + /// The encoder set a value for an index out of range of the container. case integerOutOfRange + + /// The encoder tried to treat case nestedValueInSingleValueContainer } - var _codingPath: [CodingStackEntry] + /// The stack of nested values within the root node. + private var _codingPath: [CodingStackEntry] + + /// The current value, which will be added on top of the stack once + /// finished encoding. var currentStackEntry: CodingStackEntry + + /// Creates a new encoder. init() { self._codingPath = [] self.currentStackEntry = CodingStackEntry( @@ -43,6 +61,9 @@ final class URIValueToNodeEncoder { ) } + /// Encodes the provided value into a node. + /// - Parameter value: The value to encode. + /// - Returns: The node with the encoded contents of the value. func encodeValue(_ value: some Encodable) throws -> URIEncodedNode { defer { _codingPath = [] @@ -67,27 +88,20 @@ final class URIValueToNodeEncoder { } } -extension URIValueToNodeEncoder: Encoder { - var codingPath: [any CodingKey] { - // The coding path meaningful to the types conforming to Codable. - // 1. Omit the root coding path. - // 2. Add the current stack entry's coding path. - (_codingPath - .dropFirst() - .map(\.key) - + [currentStackEntry.key]) - .map { $0 as any CodingKey } - } - - var userInfo: [CodingUserInfoKey: Any] { - [:] - } +extension URIValueToNodeEncoder { + /// Pushes a new container on top of the current stack, nesting into the + /// value at the provided key. + /// - Parameters: + /// - key: The coding key for the new value on top of the stack. + /// - newStorage: The node to push on top of the stack. func push(key: URICoderCodingKey, newStorage: URIEncodedNode) { _codingPath.append(currentStackEntry) currentStackEntry = .init(key: key, storage: newStorage) } + /// Pops the top container from the stack and restores the previously top + /// container to be the current top container. func pop() throws { // This is called when we've completed the storage in the current container. // We can pop the value at the base of the stack, then "insert" the current one @@ -97,6 +111,24 @@ extension URIValueToNodeEncoder: Encoder { try newCurrent.storage.insert(current.storage, atKey: current.key) currentStackEntry = newCurrent } +} + +extension URIValueToNodeEncoder: Encoder { + + var codingPath: [any CodingKey] { + // The coding path meaningful to the types conforming to Codable. + // 1. Omit the root coding path. + // 2. Add the current stack entry's coding path. + (_codingPath + .dropFirst() + .map(\.key) + + [currentStackEntry.key]) + .map { $0 as any CodingKey } + } + + var userInfo: [CodingUserInfoKey: Any] { + [:] + } func container( keyedBy type: Key.Type diff --git a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift index 2155a227..795acc6e 100644 --- a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift +++ b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift @@ -14,54 +14,44 @@ import Foundation -/// Parses data from a subset of variable expansions from RFC 6570. -/// -/// [RFC 6570 - Form-style query expansion.](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.8) -/// -/// | Example Template | Expansion | -/// | ---------------- | ----------------------------------| -/// | `{?who}` | `?who=fred` | -/// | `{?half}` | `?half=50%25` | -/// | `{?x,y}` | `?x=1024&y=768` | -/// | `{?x,y,empty}` | `?x=1024&y=768&empty=` | -/// | `{?x,y,undef}` | `?x=1024&y=768` | -/// | `{?list}` | `?list=red,green,blue` | -/// | `{?list\*}` | `?list=red&list=green&list=blue` | -/// | `{?keys}` | `?keys=semi,%3B,dot,.,comma,%2C` | -/// | `{?keys\*}` | `?semi=%3B&dot=.&comma=%2C` | -/// -/// [RFC 6570 - Simple string expansion.](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.2) -/// -/// | Example Template | Expansion | -/// | ---------------- | ----------------------------------| -/// | `{hello}` | `Hello%20World%21` | -/// | `{half}` | `50%25` | -/// | `{x,y}` | `1024,768` | -/// | `{x,empty}` | `1024,` | -/// | `{x,undef}` | `1024` | -/// | `{list}` | `red,green,blue` | -/// | `{list\*}` | `red,green,blue` | -/// | `{keys}` | `semi,%3B,dot,.,comma,%2C` | -/// | `{keys\*}` | `semi=%3B,dot=.,comma=%2C` | +/// A type that parses a `URIParsedNode` from a URI-encoded string. struct URIParser: Sendable { + /// The configuration instructing the parser how to interpret the raw + /// string. private let configuration: URICoderConfiguration - private typealias Raw = String.SubSequence + + /// The underlying raw string storage. private var data: Raw + /// Creates a new parser. + /// - Parameters: + /// - configuration: The configuration instructing the parser how + /// to interpret the raw string. + /// - data: The string to parse. init(configuration: URICoderConfiguration, data: String) { self.configuration = configuration self.data = data[...] } } +/// A typealias for the underlying raw string storage. +private typealias Raw = String.SubSequence + +/// A parser error. private enum ParsingError: Swift.Error { - case malformedKeyValuePair(String.SubSequence) + + /// A malformed key-value pair was detected. + case malformedKeyValuePair(Raw) } // MARK: - Parser implementations extension URIParser { + + /// Parses the root node from the underlying string, selecting the logic + /// based on the configuration. + /// - Returns: The parsed root node. 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 @@ -86,6 +76,9 @@ extension URIParser { } } + /// Parses the root node assuming the raw string uses the form style + /// and the explode parameter is enabled. + /// - Returns: The parsed root node. private mutating func parseExplodedFormRoot() throws -> URIParsedNode { try parseGenericRoot { data, appendPair in let keyValueSeparator: Character = "=" @@ -114,6 +107,9 @@ extension URIParser { } } + /// Parses the root node assuming the raw string uses the form style + /// and the explode parameter is disabled. + /// - Returns: The parsed root node. private mutating func parseUnexplodedFormRoot() throws -> URIParsedNode { try parseGenericRoot { data, appendPair in let keyValueSeparator: Character = "=" @@ -163,6 +159,9 @@ extension URIParser { } } + /// Parses the root node assuming the raw string uses the simple style + /// and the explode parameter is enabled. + /// - Returns: The parsed root node. private mutating func parseExplodedSimpleRoot() throws -> URIParsedNode { try parseGenericRoot { data, appendPair in let keyValueSeparator: Character = "=" @@ -191,6 +190,9 @@ extension URIParser { } } + /// Parses the root node assuming the raw string uses the simple style + /// and the explode parameter is disabled. + /// - Returns: The parsed root node. private mutating func parseUnexplodedSimpleRoot() throws -> URIParsedNode { // Unexploded simple dictionary cannot be told apart from // an array, so we just accumulate all pairs as standalone @@ -212,6 +214,11 @@ extension URIParser { // MARK: - URIParser utilities 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. private mutating func parseGenericRoot( _ parser: (inout Raw, (Raw, [Raw]) -> Void) throws -> Void ) throws -> URIParsedNode { @@ -229,6 +236,9 @@ extension URIParser { return root } + /// Removes escaping from the provided string. + /// - Parameter escapedValue: An escaped string. + /// - Returns: The provided string with escaping removed. private func unescapeValue(_ escapedValue: Raw) -> Raw { Self.unescapeValue( escapedValue, @@ -236,6 +246,12 @@ extension URIParser { ) } + /// Removes escaping from the provided string. + /// - Parameters: + /// - escapedValue: An escaped string. + /// - spaceEscapingCharacter: The character used to escape the space + /// character. + /// - Returns: The provided string with escaping removed. private static func unescapeValue( _ escapedValue: Raw, spaceEscapingCharacter: URICoderConfiguration.SpaceEscapingCharacter @@ -253,11 +269,23 @@ extension URIParser { extension String.SubSequence { + /// A result of calling `parseUpToEitherCharacterOrEnd`. fileprivate enum ParseUpToEitherCharacterResult { + + /// The first character was detected. case foundFirst + + /// The second character was detected, or the end was reached. case foundSecondOrEnd } + /// Accumulates characters until one of the parameter characters is found, + /// or the end is reached. Moves the underlying startIndex. + /// - Parameters: + /// - first: A character to stop at. + /// - second: Another character to stop at. + /// - Returns: A result indicating which character was detected, if any, and + /// the accumulated substring. fileprivate mutating func parseUpToEitherCharacterOrEnd( first: Character, second: Character @@ -292,6 +320,11 @@ extension String.SubSequence { return finalize(.foundSecondOrEnd) } + /// Accumulates characters until the provided character is found, + /// or the end is reached. Moves the underlying startIndex. + /// - Parameters: + /// - character: A character to stop at. + /// - Returns: The accumulated substring. fileprivate mutating func parseUpToCharacterOrEnd( _ character: Character ) -> Self { diff --git a/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift b/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift index 6b433f6a..f2dc75ad 100644 --- a/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift +++ b/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift @@ -14,55 +14,70 @@ import Foundation -/// Serializes data into a subset of variable expansions from RFC 6570. -/// -/// [RFC 6570 - Form-style query expansion.](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.8) -/// -/// | Example Template | Expansion | -/// | ---------------- | ----------------------------------| -/// | `{?who}` | `?who=fred` | -/// | `{?half}` | `?half=50%25` | -/// | `{?x,y}` | `?x=1024&y=768` | -/// | `{?x,y,empty}` | `?x=1024&y=768&empty=` | -/// | `{?x,y,undef}` | `?x=1024&y=768` | -/// | `{?list}` | `?list=red,green,blue` | -/// | `{?list\*}` | `?list=red&list=green&list=blue` | -/// | `{?keys}` | `?keys=semi,%3B,dot,.,comma,%2C` | -/// | `{?keys\*}` | `?semi=%3B&dot=.&comma=%2C` | -/// -/// [RFC 6570 - Simple string expansion.](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.2) -/// -/// | Example Template | Expansion | -/// | ---------------- | ----------------------------------| -/// | `{hello}` | `Hello%20World%21` | -/// | `{half}` | `50%25` | -/// | `{x,y}` | `1024,768` | -/// | `{x,empty}` | `1024,` | -/// | `{x,undef}` | `1024` | -/// | `{list}` | `red,green,blue` | -/// | `{list\*}` | `red,green,blue` | -/// | `{keys}` | `semi,%3B,dot,.,comma,%2C` | -/// | `{keys\*}` | `semi=%3B,dot=.,comma=%2C` | +/// A type that serializes a `URIEncodedNode` to a URI-encoded string. struct URISerializer { + /// The configuration instructing the serializer how to format the raw + /// string. private let configuration: URICoderConfiguration + + /// The underlying raw string storage. private var data: String + /// Creates a new serializer. + /// - Parameter configuration: The configuration instructing the serializer + /// how to format the raw string. init(configuration: URICoderConfiguration) { self.configuration = configuration self.data = "" } + + /// Serializes the provided node into the underlying string. + /// - Parameters: + /// - value: The node to serialize. + /// - key: The key to serialize the node under (details depend on the + /// style and explode parameters in the configuration). + /// - Returns: The URI-encoded data for the provided node. + mutating func serializeNode( + _ value: URIEncodedNode, + forKey key: String + ) throws -> String { + defer { + data.removeAll(keepingCapacity: true) + } + try serializeTopLevelNode(value, forKey: key) + return data + } } extension CharacterSet { + + /// A character set of unreserved symbols only from RFC 6570 (excludes + /// alphanumeric characters). fileprivate static let unreservedSymbols: CharacterSet = .init(charactersIn: "-._~") + + /// A character set of unreserved characters from RFC 6570. fileprivate static let unreserved: CharacterSet = .alphanumerics.union(unreservedSymbols) + + /// A character set with only the space character. fileprivate static let space: CharacterSet = .init(charactersIn: " ") + + /// A character set of unreserved characters and a space. fileprivate static let unreservedAndSpace: CharacterSet = .unreserved.union(space) } extension URISerializer { + /// A serializer error. + private enum SerializationError: Swift.Error { + + /// Nested containers are not supported. + case nestedContainersNotSupported + } + + /// Computes an escaped version of the provided string. + /// - Parameter unsafeString: A string that needs escaping. + /// - Returns: The provided string with percent-escaping applied. private func computeSafeString(_ unsafeString: String) -> String { // The space character needs to be encoded based on the config, // so first allow it to be unescaped, and then we'll do a second @@ -77,25 +92,10 @@ extension URISerializer { ) return fullyEncoded } -} - -extension URISerializer { - - private enum SerializationError: Swift.Error { - case nestedContainersNotSupported - } - - mutating func serializeNode( - _ value: URIEncodedNode, - forKey key: String - ) throws -> String { - defer { - data.removeAll(keepingCapacity: true) - } - try serializeTopLevelNode(value, forKey: key) - return data - } + /// Provides a raw string value for the provided key. + /// - Parameter key: The key to stringify. + /// - Returns: The escaped version of the provided key. private func stringifiedKey(_ key: String) throws -> String { // The root key is handled separately. guard !key.isEmpty else { @@ -105,6 +105,11 @@ extension URISerializer { return safeTopLevelKey } + /// Serializes the provided value into the underlying string. + /// - Parameters: + /// - value: The value to serialize. + /// - key: The key to serialize the value under (details depend on the + /// style and explode parameters in the configuration). private mutating func serializeTopLevelNode( _ value: URIEncodedNode, forKey key: String @@ -145,6 +150,8 @@ extension URISerializer { } } + /// Serializes the provided value into the underlying string. + /// - Parameter value: The primitive value to serialize. private mutating func serializePrimitiveValue( _ value: URIEncodedNode.Primitive ) throws { @@ -164,6 +171,13 @@ extension URISerializer { data.append(stringValue) } + /// Serializes the provided key-value pair into the underlying string. + /// - Parameters: + /// - value: The value to serialize. + /// - key: The key to serialize the value under (details depend on the + /// style and explode parameters in the configuration). + /// - separator: The separator to use, if nil, the key is not serialized, + /// only the value. private mutating func serializePrimitiveKeyValuePair( _ value: URIEncodedNode.Primitive, forKey key: String, @@ -176,6 +190,11 @@ extension URISerializer { try serializePrimitiveValue(value) } + /// Serializes the provided array into the underlying string. + /// - Parameters: + /// - array: The value to serialize. + /// - key: The key to serialize the value under (details depend on the + /// style and explode parameters in the configuration). private mutating func serializeArray( _ array: [URIEncodedNode.Primitive], forKey key: String @@ -220,6 +239,11 @@ extension URISerializer { } } + /// Serializes the provided dictionary into the underlying string. + /// - Parameters: + /// - dictionary: The value to serialize. + /// - key: The key to serialize the value under (details depend on the + /// style and explode parameters in the configuration). private mutating func serializeDictionary( _ dictionary: [String: URIEncodedNode.Primitive], forKey key: String @@ -273,6 +297,10 @@ extension URISerializer { } extension URICoderConfiguration { + + /// Returns the separator of a key and value in a pair for + /// the configuration. Can be nil, in which case no key should be + /// serialized, only the value. fileprivate var containerKeyAndValueSeparator: String? { switch (style, explode) { case (.form, false): From db844226d3c903f87f8d4889d3654e512dde88e0 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 25 Aug 2023 17:26:23 +0200 Subject: [PATCH 16/17] Fixed missing escaping of formatted dates --- .../Serialization/URISerializer.swift | 2 +- .../URICoder/Test_URICodingRoundtrip.swift | 36 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift b/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift index f2dc75ad..a5bf1f08 100644 --- a/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift +++ b/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift @@ -166,7 +166,7 @@ extension URISerializer { case .double(let double): stringValue = double.description case .date(let date): - stringValue = try configuration.dateTranscoder.encode(date) + stringValue = try computeSafeString(configuration.dateTranscoder.encode(date)) } data.append(stringValue) } diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift index 5ea841a9..77f0eb02 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift @@ -121,12 +121,12 @@ final class Test_URICodingRoundtrip: Test_Runtime { Date(timeIntervalSince1970: 1_692_948_899), key: "root", .init( - formExplode: "root=2023-08-25T07:34:59Z", - formUnexplode: "root=2023-08-25T07:34:59Z", - simpleExplode: "2023-08-25T07:34:59Z", - simpleUnexplode: "2023-08-25T07:34:59Z", - formDataExplode: "root=2023-08-25T07:34:59Z", - formDataUnexplode: "root=2023-08-25T07:34:59Z" + formExplode: "root=2023-08-25T07%3A34%3A59Z", + formUnexplode: "root=2023-08-25T07%3A34%3A59Z", + simpleExplode: "2023-08-25T07%3A34%3A59Z", + simpleUnexplode: "2023-08-25T07%3A34%3A59Z", + formDataExplode: "root=2023-08-25T07%3A34%3A59Z", + formDataUnexplode: "root=2023-08-25T07%3A34%3A59Z" ) ) @@ -152,12 +152,12 @@ final class Test_URICodingRoundtrip: Test_Runtime { ], key: "list", .init( - formExplode: "list=2023-08-25T07:34:59Z&list=2023-08-25T07:35:01Z", - formUnexplode: "list=2023-08-25T07:34:59Z,2023-08-25T07:35:01Z", - simpleExplode: "2023-08-25T07:34:59Z,2023-08-25T07:35:01Z", - simpleUnexplode: "2023-08-25T07:34:59Z,2023-08-25T07:35:01Z", - formDataExplode: "list=2023-08-25T07:34:59Z&list=2023-08-25T07:35:01Z", - formDataUnexplode: "list=2023-08-25T07:34:59Z,2023-08-25T07:35:01Z" + formExplode: "list=2023-08-25T07%3A34%3A59Z&list=2023-08-25T07%3A35%3A01Z", + formUnexplode: "list=2023-08-25T07%3A34%3A59Z,2023-08-25T07%3A35%3A01Z", + simpleExplode: "2023-08-25T07%3A34%3A59Z,2023-08-25T07%3A35%3A01Z", + 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" ) ) @@ -200,12 +200,12 @@ final class Test_URICodingRoundtrip: Test_Runtime { ), key: "keys", .init( - formExplode: "bar=24&color=red&date=2023-08-25T07:34:59Z&empty=&foo=hi%21", - formUnexplode: "keys=bar,24,color,red,date,2023-08-25T07:34:59Z,empty,,foo,hi%21", - simpleExplode: "bar=24,color=red,date=2023-08-25T07:34:59Z,empty=,foo=hi%21", - simpleUnexplode: "bar,24,color,red,date,2023-08-25T07:34:59Z,empty,,foo,hi%21", - formDataExplode: "bar=24&color=red&date=2023-08-25T07:34:59Z&empty=&foo=hi%21", - formDataUnexplode: "keys=bar,24,color,red,date,2023-08-25T07:34:59Z,empty,,foo,hi%21" + formExplode: "bar=24&color=red&date=2023-08-25T07%3A34%3A59Z&empty=&foo=hi%21", + formUnexplode: "keys=bar,24,color,red,date,2023-08-25T07%3A34%3A59Z,empty,,foo,hi%21", + simpleExplode: "bar=24,color=red,date=2023-08-25T07%3A34%3A59Z,empty=,foo=hi%21", + simpleUnexplode: "bar,24,color,red,date,2023-08-25T07%3A34%3A59Z,empty,,foo,hi%21", + formDataExplode: "bar=24&color=red&date=2023-08-25T07%3A34%3A59Z&empty=&foo=hi%21", + formDataUnexplode: "keys=bar,24,color,red,date,2023-08-25T07%3A34%3A59Z,empty,,foo,hi%21" ) ) From 17eb5df475815ba61fc517d8835c0e69d6bbeef3 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 29 Aug 2023 14:41:21 +0200 Subject: [PATCH 17/17] PR feedback - add a precondition for the first index provided --- Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift b/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift index c1c3132f..4dc882a4 100644 --- a/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift +++ b/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift @@ -117,7 +117,11 @@ extension URIEncodedNode { array.append(childValue) self = .array(array) case .unset: - if let _ = key.intValue { + if let intValue = key.intValue { + precondition( + intValue == 0, + "Unkeyed container inserting at an incorrect index" + ) self = .array([childValue]) } else { self = .dictionary([key.stringValue: childValue])