Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Refactor URIDecoder/URIParser to improve handling of the deepObject style #127

Merged
merged 11 commits into from
Nov 21, 2024
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
16 changes: 16 additions & 0 deletions Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ extension URIEncodedNode {
/// The encoder appended to a node that wasn't an array.
case appendingToNonArrayContainer

/// The encoder is trying to mark a container as array, but it's already
/// marked as a container of another type.
case markingExistingNonArrayContainerAsArray

/// The encoder inserted a value for key into a node that wasn't
/// a dictionary.
case insertingChildValueIntoNonContainer
Expand Down Expand Up @@ -128,6 +132,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.markingExistingNonArrayContainerAsArray
}
}

/// 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.

63 changes: 63 additions & 0 deletions Sources/OpenAPIRuntime/URICoder/Common/URIParsedTypes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//===----------------------------------------------------------------------===//
//
// 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

/// 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
}

// MARK: - Extensions

extension URIParsedKey: CustomStringConvertible {
// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
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,9 @@ extension URIKeyedDecodingContainer {

extension URIKeyedDecodingContainer: KeyedDecodingContainerProtocol {

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

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

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

Expand Down Expand Up @@ -153,7 +150,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