Skip to content
Merged
5 changes: 3 additions & 2 deletions Sources/OpenAPIRuntime/Conversion/ParameterStyles.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

/// The serialization style used by a parameter.
///
/// Details: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#fixed-fields-10
/// Details: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.4.md#fixed-fields-10
@_spi(Generated) public enum ParameterStyle: Sendable {

/// The form style.
Expand All @@ -26,9 +26,10 @@
///
/// Details: https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.2
case simple

/// The deepObject style.
///
/// Details: https://spec.openapis.org/oas/v3.1.0.html#style-values
/// Details: https://spec.openapis.org/oas/v3.0.4.html#style-values
case deepObject
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,22 @@ import Foundation
/// A bag of configuration values used by the URI encoder and decoder.
struct URICoderConfiguration {

/// A variable expansion style as described by RFC 6570 and OpenAPI 3.0.3.
/// A variable expansion style as described by RFC 6570 and OpenAPI 3.0.4.
enum Style {

/// A style for simple string variable expansion.
///
/// The whole string always belongs to the root key.
case simple

/// A style for form-based URI expansion.
///
/// Only some key/value pairs can belong to the root key, rest are ignored.
case form

/// A style for nested variable expansion
///
/// Only some key/value pairs can belong to the root key, rest are ignored.
case deepObject
}

Expand All @@ -43,7 +50,7 @@ struct URICoderConfiguration {
var style: Style

/// A Boolean value indicating whether the key should be repeated with
/// each value, as described by RFC 6570 and OpenAPI 3.0.3.
/// each value, as described by RFC 6570 and OpenAPI 3.0.4.
var explode: Bool

/// The character used to escape the space character.
Expand Down
22 changes: 22 additions & 0 deletions Sources/OpenAPIRuntime/URICoder/Common/URIDecodedTypes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftOpenAPIGenerator open source project
//
// Copyright (c) 2024 Apple Inc. and the SwiftOpenAPIGenerator project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

/// A primitive value produced by `URIValueFromNodeDecoder`.
typealias URIDecodedPrimitive = URIParsedValue

/// An array value produced by `URIValueFromNodeDecoder`.
typealias URIDecodedArray = URIParsedValueArray

/// A dictionary value produced by `URIValueFromNodeDecoder`.
typealias URIDecodedDictionary = [Substring: URIParsedValueArray]
12 changes: 12 additions & 0 deletions Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,18 @@ extension URIEncodedNode {
}
}

/// Marks the node as an array, starting as empty.
/// - Throws: If the node is already set to be anything else but an array.
mutating func markAsArray() throws {
switch self {
case .array:
// Already an array.
break
case .unset: self = .array([])
default: throw InsertionError.appendingToNonArrayContainer
}
}

/// Appends a value to the array node.
/// - Parameter childValue: The node to append to the underlying array.
/// - Throws: If the node is already set to be anything else but an array.
Expand Down
27 changes: 0 additions & 27 deletions Sources/OpenAPIRuntime/URICoder/Common/URIParsedNode.swift

This file was deleted.

90 changes: 90 additions & 0 deletions Sources/OpenAPIRuntime/URICoder/Common/URIParsedTypes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftOpenAPIGenerator open source project
//
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Foundation

/// A component of a `URIParsedKey`.
typealias URIParsedKeyComponent = String.SubSequence

/// A parsed key for a parsed value.
///
/// For example, `foo=bar` in a `form` string would parse the key as `foo` (single component).
/// In an unexploded `form` string `root=foo,bar`, the key would be `root/foo` (two components).
/// In a `simple` string `bar`, the key would be empty (0 components).
struct URIParsedKey: Hashable {

/// The individual string components.
let components: [URIParsedKeyComponent]
/// Creates a new parsed key.
/// - Parameter components: The key components.
init(_ components: [URIParsedKeyComponent]) { self.components = components }

/// A new empty key.
static var empty: Self { .init([]) }
}

/// A primitive value produced by `URIParser`.
typealias URIParsedValue = String.SubSequence

/// An array of primitive values produced by `URIParser`.
typealias URIParsedValueArray = [URIParsedValue]

/// A key-value produced by `URIParser`.
struct URIParsedPair: Equatable {

/// The key of the pair.
///
/// In `foo=bar`, `foo` is the key.
var key: URIParsedKey

/// The value of the pair.
///
/// In `foo=bar`, `bar` is the value.
var value: URIParsedValue
}

/// An array of key-value pairs produced by `URIParser`.
typealias URIParsedPairArray = [URIParsedPair]

// MARK: - Extensions

extension URIParsedKey: CustomStringConvertible {
/// A textual representation of this instance.
///
/// Calling this property directly is discouraged. Instead, convert an
/// instance of any type to a string by using the `String(describing:)`
/// initializer. This initializer works with any type, and uses the custom
/// `description` property for types that conform to
/// `CustomStringConvertible`:
///
/// struct Point: CustomStringConvertible {
/// let x: Int, y: Int
///
/// var description: String {
/// return "(\(x), \(y))"
/// }
/// }
///
/// let p = Point(x: 21, y: 30)
/// let s = String(describing: p)
/// print(s)
/// // Prints "(21, 30)"
///
/// The conversion of `p` to a string in the assignment to `s` uses the
/// `Point` type's `description` property.
var description: String {
if components.isEmpty { return "<empty>" }
return components.joined(separator: "/")
}
}
94 changes: 13 additions & 81 deletions Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import Foundation

/// A type that decodes a `Decodable` value from an URI-encoded string
/// using the rules from RFC 6570, RFC 1866, and OpenAPI 3.0.3, depending on
/// using the rules from RFC 6570, RFC 1866, and OpenAPI 3.0.4, depending on
/// the configuration.
///
/// [RFC 6570 - Form-style query expansion.](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.8)
Expand Down Expand Up @@ -45,6 +45,13 @@ import Foundation
/// | `{list\*}` | `red,green,blue` |
/// | `{keys}` | `semi,%3B,dot,.,comma,%2C` |
/// | `{keys\*}` | `semi=%3B,dot=.,comma=%2C` |
///
/// [OpenAPI 3.0.4 - Deep object expansion.](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.4.md#style-examples)
///
/// | Example Template | Expansion |
/// | ---------------- | ----------------------------------------------------------|
/// | `{?keys\*}` | `?keys%5Bsemi%5D=%3B&keys%5Bdot%5D=.&keys%5Bcomma%5D=%2C` |
///
struct URIDecoder: Sendable {

/// The configuration instructing the decoder how to interpret the raw
Expand All @@ -60,10 +67,6 @@ extension URIDecoder {

/// Attempt to decode an object from an URI string.
///
/// Under the hood, `URIDecoder` first parses the string into a
/// `URIParsedNode` using `URIParser`, and then uses
/// `URIValueFromNodeDecoder` to decode the `Decodable` value.
///
/// - Parameters:
/// - type: The type to decode.
/// - key: The key of the decoded value. Only used with certain styles
Expand All @@ -72,15 +75,12 @@ extension URIDecoder {
/// - Returns: The decoded value.
/// - Throws: An error if decoding fails, for example, due to incompatible data or key.
func decode<T: Decodable>(_ type: T.Type = T.self, forKey key: String = "", from data: Substring) throws -> T {
try withCachedParser(from: data) { decoder in try decoder.decode(type, forKey: key) }
let decoder = URIValueFromNodeDecoder(data: data, rootKey: key[...], configuration: configuration)
return try decoder.decodeRoot(type)
}

/// Attempt to decode an object from an URI string, if present.
///
/// Under the hood, `URIDecoder` first parses the string into a
/// `URIParsedNode` using `URIParser`, and then uses
/// `URIValueFromNodeDecoder` to decode the `Decodable` value.
///
/// - Parameters:
/// - type: The type to decode.
/// - key: The key of the decoded value. Only used with certain styles
Expand All @@ -90,76 +90,8 @@ extension URIDecoder {
/// - Throws: An error if decoding fails, for example, due to incompatible data or key.
func decodeIfPresent<T: Decodable>(_ type: T.Type = T.self, forKey key: String = "", from data: Substring) throws
-> T?
{ try withCachedParser(from: data) { decoder in try decoder.decodeIfPresent(type, forKey: key) } }

/// 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
/// the `decode` method on `URICachedDecoder`.
/// - Returns: The result of the closure invocation.
/// - Throws: An error if parsing or decoding fails.
func withCachedParser<R>(from data: Substring, calls: (URICachedDecoder) throws -> R) throws -> R {
var parser = URIParser(configuration: configuration, data: data)
let parsedNode = try parser.parseRoot()
let decoder = URICachedDecoder(configuration: configuration, node: parsedNode)
return try calls(decoder)
}
}

struct URICachedDecoder {

/// The configuration used by the decoder.
fileprivate let configuration: URICoderConfiguration

/// The node from which to decode a value on demand.
fileprivate let node: URIParsedNode

/// Attempt to decode an object from an URI-encoded string.
///
/// Under the hood, `URICachedDecoder` already has a pre-parsed
/// `URIParsedNode` and uses `URIValueFromNodeDecoder` to decode
/// the `Decodable` value.
///
/// - Parameters:
/// - type: The type to decode.
/// - key: The key of the decoded value. Only used with certain styles
/// and explode options, ignored otherwise.
/// - Returns: The decoded value.
/// - Throws: An error if decoding fails.
func decode<T: Decodable>(_ type: T.Type = T.self, forKey key: String = "") throws -> T {
let decoder = URIValueFromNodeDecoder(
node: node,
rootKey: key[...],
style: configuration.style,
explode: configuration.explode,
dateTranscoder: configuration.dateTranscoder
)
return try decoder.decodeRoot()
}

/// Attempt to decode an object from an URI-encoded string, if present.
///
/// Under the hood, `URICachedDecoder` already has a pre-parsed
/// `URIParsedNode` and uses `URIValueFromNodeDecoder` to decode
/// the `Decodable` value.
///
/// - Parameters:
/// - type: The type to decode.
/// - key: The key of the decoded value. Only used with certain styles
/// and explode options, ignored otherwise.
/// - Returns: The decoded value.
/// - Throws: An error if decoding fails.
func decodeIfPresent<T: Decodable>(_ type: T.Type = T.self, forKey key: String = "") throws -> T? {
let decoder = URIValueFromNodeDecoder(
node: node,
rootKey: key[...],
style: configuration.style,
explode: configuration.explode,
dateTranscoder: configuration.dateTranscoder
)
return try decoder.decodeRootIfPresent()
{
let decoder = URIValueFromNodeDecoder(data: data, rootKey: key[...], configuration: configuration)
return try decoder.decodeRootIfPresent(type)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ struct URIKeyedDecodingContainer<Key: CodingKey> {

/// The associated decoder.
let decoder: URIValueFromNodeDecoder

/// The underlying dictionary.
let values: URIParsedNode
}

extension URIKeyedDecodingContainer {
Expand All @@ -32,7 +29,7 @@ extension URIKeyedDecodingContainer {
/// - Returns: The value found for the provided key.
/// - Throws: An error if no value for the key was found.
private func _decodeValue(forKey key: Key) throws -> URIParsedValue {
guard let value = values[key.stringValue[...]]?.first else {
guard let value = try decoder.nestedElementInCurrentDictionary(forKey: key.stringValue) else {
throw DecodingError.keyNotFound(key, .init(codingPath: codingPath, debugDescription: "Key not found."))
}
return value
Expand Down Expand Up @@ -97,9 +94,15 @@ extension URIKeyedDecodingContainer {

extension URIKeyedDecodingContainer: KeyedDecodingContainerProtocol {

var allKeys: [Key] { values.keys.map { key in Key.init(stringValue: String(key))! } }
var allKeys: [Key] {
do { return try decoder.elementKeysInCurrentDictionary().compactMap { .init(stringValue: $0) } } catch {
return []
}
}

func contains(_ key: Key) -> Bool { values[key.stringValue[...]] != nil }
func contains(_ key: Key) -> Bool {
do { return try decoder.containsElementInCurrentDictionary(forKey: key.stringValue) } catch { return false }
}

var codingPath: [any CodingKey] { decoder.codingPath }

Expand Down Expand Up @@ -153,7 +156,7 @@ extension URIKeyedDecodingContainer: KeyedDecodingContainerProtocol {
case is UInt64.Type: return try decode(UInt64.self, forKey: key) as! T
case is Date.Type: return try decoder.dateTranscoder.decode(String(_decodeValue(forKey: key))) as! T
default:
try decoder.push(.init(key))
decoder.push(.init(key))
defer { decoder.pop() }
return try type.init(from: decoder)
}
Expand Down
Loading