Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
160 changes: 160 additions & 0 deletions Sources/OpenAPIRuntime/Base/Acceptable.swift
Original file line number Diff line number Diff line change
@@ -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
//
//===----------------------------------------------------------------------===//

/// The protocol that all generated `AcceptableContentType` enums conform to.
public protocol AcceptableProtocol: RawRepresentable, Sendable, Equatable, Hashable where RawValue == String {

/// Returns the default set of acceptable content types for this type, in
/// the order specified in the OpenAPI document.
static var defaultValues: [Self] { get }
}

/// A quality value used to describe the order of priority in a comma-separated
/// list of values, such as in the Accept header.
public struct QualityValue: Sendable, Equatable, Hashable {

/// As the quality value only retains up to and including 3 decimal digits,
/// we store it in terms of the thousands.
///
/// This allows predictable equality comparisons and sorting.
///
/// For example, 1000 thousands is the quality value of 1.0.
private let thousands: UInt16

/// Creates a new quality value of the default value 1.0.
public init() {
self.thousands = 1000
}

/// Returns a Boolean value indicating whether the quality value is
/// at its default value 1.0.
public var isDefault: Bool {
thousands == 1000
}

/// Creates a new quality value from the provided floating-point number.
///
/// - Precondition: The value must be between 0.0 and 1.0, inclusive.
public init(doubleValue: Double) {
precondition(
doubleValue >= 0.0 && doubleValue <= 1.0,
"Input number into quality value is out of range"
)
self.thousands = UInt16(doubleValue * 1000)
}

/// The value represented as a floating-point number between 0.0 and 1.0, inclusive.
public var doubleValue: Double {
Double(thousands) / 1000
}
}

extension QualityValue: RawRepresentable {
public init?(rawValue: String) {
guard let doubleValue = Double(rawValue) else {
return nil
}
self.init(doubleValue: doubleValue)
}

public var rawValue: String {
String(format: "%0.3f", doubleValue)
}
}

extension QualityValue: Comparable {
public static func < (lhs: QualityValue, rhs: QualityValue) -> Bool {
lhs.thousands > rhs.thousands
}
}

extension QualityValue: ExpressibleByIntegerLiteral {
public init(integerLiteral value: UInt16) {
self.thousands = value * 1000
}
}

extension QualityValue: ExpressibleByFloatLiteral {
public init(floatLiteral value: Double) {
self.init(doubleValue: value)
}
}

/// A wrapper of an individual content type in the accept header.
public struct AcceptHeaderContentType<T: AcceptableProtocol>: Sendable, Equatable, Hashable {

/// The quality value of this content type.
///
/// Used to describe the order of priority in a comma-separated
/// list of values.
///
/// Content types with a higher priority should be preferred by the server
/// when deciding which content type to use in the response.
///
/// Also called the "q-factor" or "q-value".
public let quality: QualityValue

/// The value representing the content type.
public let contentType: T

/// Creates a new content type from the provided parameters.
/// - Parameters:
/// - quality: The quality of the content type, between 0.0 and 1.0.
/// - value: The value representing the content type.
/// - Precondition: Priority must be in the range 0.0 and 1.0 inclusive.
public init(quality: QualityValue = 1.0, contentType: T) {
self.quality = quality
self.contentType = contentType
}

/// Returns the default set of acceptable content types for this type, in
/// the order specified in the OpenAPI document.
public static var defaultValues: [Self] {
T.defaultValues.map { .init(contentType: $0) }
}
}

extension AcceptHeaderContentType: RawRepresentable {
public init?(rawValue: String) {
guard let validMimeType = OpenAPIMIMEType(rawValue) else {
// Invalid MIME type.
return nil
}
let quality: QualityValue
if let rawQuality = validMimeType.parameters["q"] {
guard let parsedQuality = QualityValue(rawValue: rawQuality) else {
// Invalid quality parameter.
return nil
}
quality = parsedQuality
} else {
quality = 1.0
}
guard let typeAndSubtype = T.init(rawValue: validMimeType.kind.description.lowercased()) else {
// Invalid type/subtype.
return nil
}
self.init(quality: quality, contentType: typeAndSubtype)
}

public var rawValue: String {
contentType.rawValue + (quality.isDefault ? "" : "; q=\(quality.rawValue)")
}
}

extension AcceptHeaderContentType: Comparable {
public static func < (lhs: AcceptHeaderContentType<T>, rhs: AcceptHeaderContentType<T>) -> Bool {
lhs.quality < rhs.quality
}
}
6 changes: 0 additions & 6 deletions Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,3 @@ extension OpenAPIMIMEType: LosslessStringConvertible {
.joined(separator: "; ")
}
}

extension String {
fileprivate var trimmingLeadingAndTrailingSpaces: Self {
trimmingCharacters(in: .whitespacesAndNewlines)
}
}
13 changes: 13 additions & 0 deletions Sources/OpenAPIRuntime/Conversion/Converter+Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ import Foundation

extension Converter {

#warning("TODO: Add docs")
public func setAcceptHeader<T: AcceptableProtocol>(
in headerFields: inout [HeaderField],
value: [AcceptHeaderContentType<T>]
) {
headerFields.append(
.init(
name: "accept",
value: value.map(\.rawValue).joined(separator: ", ")
)
)
}

// | client | set | request path | text | string-convertible | required | renderedRequestPath |
public func renderedRequestPath(
template: String,
Expand Down
21 changes: 21 additions & 0 deletions Sources/OpenAPIRuntime/Conversion/Converter+Server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,27 @@ public extension Converter {

// MARK: Miscs

#warning("TODO: Docs")
func extractAcceptHeaderIfPresent<T: AcceptableProtocol>(
in headerFields: [HeaderField]
) throws -> [AcceptHeaderContentType<T>] {
guard let rawValue = headerFields.firstValue(name: "accept") else {
return AcceptHeaderContentType<T>.defaultValues
}
let rawComponents =
rawValue
.split(separator: ",")
.map(String.init)
.map(\.trimmingLeadingAndTrailingSpaces)
let parsedComponents = try rawComponents.map { rawComponent in
guard let value = AcceptHeaderContentType<T>(rawValue: rawComponent) else {
throw RuntimeError.malformedAcceptHeader(rawComponent)
}
return value
}
return parsedComponents
}

/// Validates that the Accept header in the provided response
/// is compatible with the provided content type substring.
/// - Parameters:
Expand Down
9 changes: 9 additions & 0 deletions Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,12 @@ extension URLComponents {
queryItems = groups.otherItems + [newItem]
}
}

extension String {

/// Returns the string with leading and trailing whitespace (such as spaces
/// and newlines) removed.
var trimmingLeadingAndTrailingSpaces: Self {
trimmingCharacters(in: .whitespacesAndNewlines)
}
}
3 changes: 3 additions & 0 deletions Sources/OpenAPIRuntime/Errors/RuntimeError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret
case missingRequiredHeaderField(String)
case unexpectedContentTypeHeader(String)
case unexpectedAcceptHeader(String)
case malformedAcceptHeader(String)

// Path
case missingRequiredPathParameter(String)
Expand Down Expand Up @@ -74,6 +75,8 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret
return "Unexpected Content-Type header: \(contentType)"
case .unexpectedAcceptHeader(let accept):
return "Unexpected Accept header: \(accept)"
case .malformedAcceptHeader(let accept):
return "Malformed Accept header: \(accept)"
case .missingRequiredPathParameter(let name):
return "Missing required path parameter named: \(name)"
case .missingRequiredQueryParameter(let name):
Expand Down