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

Introduce URIEncoder and URIDecoder types #41

Merged
merged 20 commits into from
Aug 29, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions NOTICE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
36 changes: 36 additions & 0 deletions Sources/OpenAPIRuntime/URICoder/Common/URICodeCodingKey.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
72 changes: 72 additions & 0 deletions Sources/OpenAPIRuntime/URICoder/Common/URICoderConfiguration.swift
Original file line number Diff line number Diff line change
@@ -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 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"
czechboy0 marked this conversation as resolved.
Show resolved Hide resolved
)

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: "+"
)
}
99 changes: 99 additions & 0 deletions Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift
Original file line number Diff line number Diff line change
@@ -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 URIEncodedNode: 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 URIEncodedNode {

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<Key: CodingKey>(
_ 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])
czechboy0 marked this conversation as resolved.
Show resolved Hide resolved
} 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
}
}
}
20 changes: 20 additions & 0 deletions Sources/OpenAPIRuntime/URICoder/Common/URIParsedNode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//===----------------------------------------------------------------------===//
//
// 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

typealias URIParsedKey = String.SubSequence
typealias URIParsedValue = String.SubSequence
typealias URIParsedValueArray = [URIParsedValue]
typealias URIParsedNode = [URIParsedKey: URIParsedValueArray]
99 changes: 99 additions & 0 deletions Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift
Original file line number Diff line number Diff line change
@@ -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

/// 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.
/// - 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<T: Decodable>(
_ 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<R>(
czechboy0 marked this conversation as resolved.
Show resolved Hide resolved
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<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
)
return try decoder.decodeRoot()
}
}
Loading