From 0c17d71f9059b4a73eeb215fe886d8f002790aec Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 10 Aug 2023 10:35:23 +0200 Subject: [PATCH 01/10] [Prototype] Accept header --- Sources/OpenAPIRuntime/Base/Acceptable.swift | 160 ++++++++++++++++++ .../OpenAPIRuntime/Base/OpenAPIMIMEType.swift | 6 - .../Conversion/Converter+Client.swift | 13 ++ .../Conversion/Converter+Server.swift | 21 +++ .../Conversion/FoundationExtensions.swift | 9 + .../OpenAPIRuntime/Errors/RuntimeError.swift | 3 + 6 files changed, 206 insertions(+), 6 deletions(-) create mode 100644 Sources/OpenAPIRuntime/Base/Acceptable.swift diff --git a/Sources/OpenAPIRuntime/Base/Acceptable.swift b/Sources/OpenAPIRuntime/Base/Acceptable.swift new file mode 100644 index 00000000..9e336580 --- /dev/null +++ b/Sources/OpenAPIRuntime/Base/Acceptable.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 +// +//===----------------------------------------------------------------------===// + +/// 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: 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, rhs: AcceptHeaderContentType) -> Bool { + lhs.quality < rhs.quality + } +} diff --git a/Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift b/Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift index d7386cb4..cc79ffd0 100644 --- a/Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift +++ b/Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift @@ -165,9 +165,3 @@ extension OpenAPIMIMEType: LosslessStringConvertible { .joined(separator: "; ") } } - -extension String { - fileprivate var trimmingLeadingAndTrailingSpaces: Self { - trimmingCharacters(in: .whitespacesAndNewlines) - } -} diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift index 0e6ed7a2..813cba8c 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift @@ -15,6 +15,19 @@ import Foundation extension Converter { + #warning("TODO: Add docs") + public func setAcceptHeader( + in headerFields: inout [HeaderField], + value: [AcceptHeaderContentType] + ) { + 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, diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift index 0a0ac42b..104d83d3 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift @@ -17,6 +17,27 @@ public extension Converter { // MARK: Miscs + #warning("TODO: Docs") + func extractAcceptHeaderIfPresent( + in headerFields: [HeaderField] + ) throws -> [AcceptHeaderContentType] { + guard let rawValue = headerFields.firstValue(name: "accept") else { + return AcceptHeaderContentType.defaultValues + } + let rawComponents = + rawValue + .split(separator: ",") + .map(String.init) + .map(\.trimmingLeadingAndTrailingSpaces) + let parsedComponents = try rawComponents.map { rawComponent in + guard let value = AcceptHeaderContentType(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: diff --git a/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift b/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift index 3590c853..4c2b5cb8 100644 --- a/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift @@ -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) + } +} diff --git a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift index 46185e04..205b0e33 100644 --- a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift +++ b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift @@ -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) @@ -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): From 213c6bda553c8db13e3abc7ac279b9443f96c80d Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 14 Aug 2023 15:01:49 +0200 Subject: [PATCH 02/10] PR feedback --- Sources/OpenAPIRuntime/Base/Acceptable.swift | 51 +++++++++++++------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/Sources/OpenAPIRuntime/Base/Acceptable.swift b/Sources/OpenAPIRuntime/Base/Acceptable.swift index 9e336580..3175ca73 100644 --- a/Sources/OpenAPIRuntime/Base/Acceptable.swift +++ b/Sources/OpenAPIRuntime/Base/Acceptable.swift @@ -73,12 +73,6 @@ extension QualityValue: RawRepresentable { } } -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 @@ -91,9 +85,31 @@ extension QualityValue: ExpressibleByFloatLiteral { } } +extension Array where Element == QualityValue { + + /// Returns a sorted array of quality values, where the highest + /// priority items come first. + public func sortedByQuality() -> Self { + sorted { a, b in + a.doubleValue < b.doubleValue + } + } +} + +extension Array where Element: AcceptableProtocol { + + /// Returns the default values for the acceptable type. + public var defaultValues: Self { + Element.defaultValues + } +} + /// A wrapper of an individual content type in the accept header. public struct AcceptHeaderContentType: Sendable, Equatable, Hashable { + /// The value representing the content type. + public var contentType: T + /// The quality value of this content type. /// /// Used to describe the order of priority in a comma-separated @@ -103,17 +119,14 @@ public struct AcceptHeaderContentType: Sendable, Equatabl /// 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 + public var quality: QualityValue /// 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. + /// - quality: The quality of the content type, between 0.0 and 1.0. /// - Precondition: Priority must be in the range 0.0 and 1.0 inclusive. - public init(quality: QualityValue = 1.0, contentType: T) { + public init(contentType: T, quality: QualityValue = 1.0) { self.quality = quality self.contentType = contentType } @@ -141,11 +154,11 @@ extension AcceptHeaderContentType: RawRepresentable { } else { quality = 1.0 } - guard let typeAndSubtype = T.init(rawValue: validMimeType.kind.description.lowercased()) else { + guard let typeAndSubtype = T(rawValue: validMimeType.kind.description.lowercased()) else { // Invalid type/subtype. return nil } - self.init(quality: quality, contentType: typeAndSubtype) + self.init(contentType: typeAndSubtype, quality: quality) } public var rawValue: String { @@ -153,8 +166,12 @@ extension AcceptHeaderContentType: RawRepresentable { } } -extension AcceptHeaderContentType: Comparable { - public static func < (lhs: AcceptHeaderContentType, rhs: AcceptHeaderContentType) -> Bool { - lhs.quality < rhs.quality +extension AcceptHeaderContentType { + + // TODO: Can we spell this as an extension of an array? + public func sortedByQuality(_ array: [AcceptHeaderContentType]) -> [AcceptHeaderContentType] { + array.sorted { a, b in + a.quality.doubleValue > b.quality.doubleValue + } } } From b8d041570017b7582ba53893d930495a263f5ba1 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 15 Aug 2023 11:32:05 +0200 Subject: [PATCH 03/10] PR feedback --- Sources/OpenAPIRuntime/Base/Acceptable.swift | 28 ++++++++++--------- .../Conversion/Converter+Client.swift | 9 ++++-- .../Conversion/Converter+Server.swift | 6 +++- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/Sources/OpenAPIRuntime/Base/Acceptable.swift b/Sources/OpenAPIRuntime/Base/Acceptable.swift index 3175ca73..47b468d2 100644 --- a/Sources/OpenAPIRuntime/Base/Acceptable.swift +++ b/Sources/OpenAPIRuntime/Base/Acceptable.swift @@ -91,24 +91,25 @@ extension Array where Element == QualityValue { /// priority items come first. public func sortedByQuality() -> Self { sorted { a, b in - a.doubleValue < b.doubleValue + a.doubleValue > b.doubleValue } } } -extension Array where Element: AcceptableProtocol { +extension Array { /// Returns the default values for the acceptable type. - public var defaultValues: Self { - Element.defaultValues + public static func defaultValues() -> [AcceptHeaderContentType] + where Element == AcceptHeaderContentType { + T.defaultValues.map { .init(contentType: $0) } } } /// A wrapper of an individual content type in the accept header. -public struct AcceptHeaderContentType: Sendable, Equatable, Hashable { +public struct AcceptHeaderContentType: Sendable, Equatable, Hashable { /// The value representing the content type. - public var contentType: T + public var contentType: ContentType /// The quality value of this content type. /// @@ -126,7 +127,7 @@ public struct AcceptHeaderContentType: Sendable, Equatabl /// - value: The value representing the content type. /// - quality: The quality of the content type, between 0.0 and 1.0. /// - Precondition: Priority must be in the range 0.0 and 1.0 inclusive. - public init(contentType: T, quality: QualityValue = 1.0) { + public init(contentType: ContentType, quality: QualityValue = 1.0) { self.quality = quality self.contentType = contentType } @@ -134,7 +135,7 @@ public struct AcceptHeaderContentType: Sendable, Equatabl /// 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) } + ContentType.defaultValues.map { .init(contentType: $0) } } } @@ -154,7 +155,7 @@ extension AcceptHeaderContentType: RawRepresentable { } else { quality = 1.0 } - guard let typeAndSubtype = T(rawValue: validMimeType.kind.description.lowercased()) else { + guard let typeAndSubtype = ContentType(rawValue: validMimeType.kind.description.lowercased()) else { // Invalid type/subtype. return nil } @@ -166,11 +167,12 @@ extension AcceptHeaderContentType: RawRepresentable { } } -extension AcceptHeaderContentType { +extension Array { - // TODO: Can we spell this as an extension of an array? - public func sortedByQuality(_ array: [AcceptHeaderContentType]) -> [AcceptHeaderContentType] { - array.sorted { a, b in + /// Returns the array sorted by the quality value, highest quality first. + public func sortedByQuality() -> [AcceptHeaderContentType] + where Element == AcceptHeaderContentType { + sorted { a, b in a.quality.doubleValue > b.quality.doubleValue } } diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift index 813cba8c..4195400e 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift @@ -15,15 +15,18 @@ import Foundation extension Converter { - #warning("TODO: Add docs") + /// Sets the "accept" header according to the provided content types. + /// - Parameters: + /// - headerFields: The header fields where to add the "accept" header. + /// - contentTypes: The array of acceptable content types by the client. public func setAcceptHeader( in headerFields: inout [HeaderField], - value: [AcceptHeaderContentType] + contentTypes: [AcceptHeaderContentType] ) { headerFields.append( .init( name: "accept", - value: value.map(\.rawValue).joined(separator: ", ") + value: contentTypes.map(\.rawValue).joined(separator: ", ") ) ) } diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift index 104d83d3..98e83e06 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift @@ -17,7 +17,11 @@ public extension Converter { // MARK: Miscs - #warning("TODO: Docs") + /// Returns the "accept" header parsed into individual content types. + /// - Parameter headerFields: The header fields to inspect for an "accept" + /// header. + /// - Returns: The parsed content types, or the default content types if + /// the header was not provided. func extractAcceptHeaderIfPresent( in headerFields: [HeaderField] ) throws -> [AcceptHeaderContentType] { From b2e90cd7aed53ac659e488293d3030a8f0a26e07 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 15 Aug 2023 15:59:29 +0200 Subject: [PATCH 04/10] Prototype working end-to-end --- Sources/OpenAPIRuntime/Base/Acceptable.swift | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Sources/OpenAPIRuntime/Base/Acceptable.swift b/Sources/OpenAPIRuntime/Base/Acceptable.swift index 47b468d2..128b451c 100644 --- a/Sources/OpenAPIRuntime/Base/Acceptable.swift +++ b/Sources/OpenAPIRuntime/Base/Acceptable.swift @@ -13,12 +13,8 @@ //===----------------------------------------------------------------------===// /// 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 } -} +public protocol AcceptableProtocol: RawRepresentable, Sendable, Equatable, Hashable, CaseIterable +where RawValue == String {} /// A quality value used to describe the order of priority in a comma-separated /// list of values, such as in the Accept header. @@ -101,7 +97,7 @@ extension Array { /// Returns the default values for the acceptable type. public static func defaultValues() -> [AcceptHeaderContentType] where Element == AcceptHeaderContentType { - T.defaultValues.map { .init(contentType: $0) } + T.allCases.map { .init(contentType: $0) } } } @@ -135,7 +131,7 @@ public struct AcceptHeaderContentType: Sendable /// Returns the default set of acceptable content types for this type, in /// the order specified in the OpenAPI document. public static var defaultValues: [Self] { - ContentType.defaultValues.map { .init(contentType: $0) } + ContentType.allCases.map { .init(contentType: $0) } } } From 9fb3596bfe3fbae3583172adb24c7e6dac5e5156 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 16 Aug 2023 12:09:18 +0200 Subject: [PATCH 05/10] Remove an unused extension --- Sources/OpenAPIRuntime/Base/Acceptable.swift | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Sources/OpenAPIRuntime/Base/Acceptable.swift b/Sources/OpenAPIRuntime/Base/Acceptable.swift index 128b451c..3546af0b 100644 --- a/Sources/OpenAPIRuntime/Base/Acceptable.swift +++ b/Sources/OpenAPIRuntime/Base/Acceptable.swift @@ -81,17 +81,6 @@ extension QualityValue: ExpressibleByFloatLiteral { } } -extension Array where Element == QualityValue { - - /// Returns a sorted array of quality values, where the highest - /// priority items come first. - public func sortedByQuality() -> Self { - sorted { a, b in - a.doubleValue > b.doubleValue - } - } -} - extension Array { /// Returns the default values for the acceptable type. From 15d2b021ec6f88c5d845a3d59371311ac44de2cd Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 18 Aug 2023 12:20:49 +0200 Subject: [PATCH 06/10] PR feedback: assert valid range of QualityValue --- Sources/OpenAPIRuntime/Base/Acceptable.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/Base/Acceptable.swift b/Sources/OpenAPIRuntime/Base/Acceptable.swift index 3546af0b..52f6ae53 100644 --- a/Sources/OpenAPIRuntime/Base/Acceptable.swift +++ b/Sources/OpenAPIRuntime/Base/Acceptable.swift @@ -45,7 +45,7 @@ public struct QualityValue: Sendable, Equatable, Hashable { public init(doubleValue: Double) { precondition( doubleValue >= 0.0 && doubleValue <= 1.0, - "Input number into quality value is out of range" + "Provided quality number is out of range, must be between 0.0 and 1.0, inclusive." ) self.thousands = UInt16(doubleValue * 1000) } @@ -71,6 +71,10 @@ extension QualityValue: RawRepresentable { extension QualityValue: ExpressibleByIntegerLiteral { public init(integerLiteral value: UInt16) { + precondition( + value >= 0 && value <= 1, + "Provided quality number is out of range, must be between 0 and 1, inclusive." + ) self.thousands = value * 1000 } } From 8ad4adbcbd37d6178fc59f286d8080fc4a9e21e7 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 23 Aug 2023 13:34:35 +0200 Subject: [PATCH 07/10] Add unit tests --- Sources/OpenAPIRuntime/Base/Acceptable.swift | 7 +- .../Base/Test_Acceptable.swift | 107 ++++++++++++++++++ 2 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 Tests/OpenAPIRuntimeTests/Base/Test_Acceptable.swift diff --git a/Sources/OpenAPIRuntime/Base/Acceptable.swift b/Sources/OpenAPIRuntime/Base/Acceptable.swift index 52f6ae53..1cc092d9 100644 --- a/Sources/OpenAPIRuntime/Base/Acceptable.swift +++ b/Sources/OpenAPIRuntime/Base/Acceptable.swift @@ -13,12 +13,11 @@ //===----------------------------------------------------------------------===// /// The protocol that all generated `AcceptableContentType` enums conform to. -public protocol AcceptableProtocol: RawRepresentable, Sendable, Equatable, Hashable, CaseIterable -where RawValue == String {} +public protocol AcceptableProtocol: RawRepresentable, Sendable, Hashable, CaseIterable where RawValue == String {} /// 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 { +public struct QualityValue: Sendable, Hashable { /// As the quality value only retains up to and including 3 decimal digits, /// we store it in terms of the thousands. @@ -95,7 +94,7 @@ extension Array { } /// A wrapper of an individual content type in the accept header. -public struct AcceptHeaderContentType: Sendable, Equatable, Hashable { +public struct AcceptHeaderContentType: Sendable, Hashable { /// The value representing the content type. public var contentType: ContentType diff --git a/Tests/OpenAPIRuntimeTests/Base/Test_Acceptable.swift b/Tests/OpenAPIRuntimeTests/Base/Test_Acceptable.swift new file mode 100644 index 00000000..4b866b9d --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Base/Test_Acceptable.swift @@ -0,0 +1,107 @@ +//===----------------------------------------------------------------------===// +// +// 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 +@_spi(Generated) import OpenAPIRuntime + +enum TestAcceptable: AcceptableProtocol { + case json + case other(String) + + init?(rawValue: String) { + switch rawValue { + case "application/json": + self = .json + default: + self = .other(rawValue) + } + } + + var rawValue: String { + switch self { + case .json: + return "application/json" + case .other(let string): + return string + } + } + + static var allCases: [TestAcceptable] { + [.json] + } +} + +final class Test_AcceptHeaderContentType: Test_Runtime { + func test() throws { + do { + let contentType = AcceptHeaderContentType(contentType: TestAcceptable.json) + XCTAssertEqual(contentType.contentType, .json) + XCTAssertEqual(contentType.quality, 1.0) + XCTAssertEqual(contentType.rawValue, "application/json") + XCTAssertEqual( + AcceptHeaderContentType(rawValue: "application/json"), + contentType + ) + } + do { + let contentType = AcceptHeaderContentType( + contentType: TestAcceptable.json, + quality: 0.5 + ) + XCTAssertEqual(contentType.contentType, .json) + XCTAssertEqual(contentType.quality, 0.5) + XCTAssertEqual(contentType.rawValue, "application/json; q=0.500") + XCTAssertEqual( + AcceptHeaderContentType(rawValue: "application/json; q=0.500"), + contentType + ) + } + do { + XCTAssertEqual( + AcceptHeaderContentType.defaultValues, + [ + .init(contentType: .json) + ] + ) + } + do { + let unsorted: [AcceptHeaderContentType] = [ + .init(contentType: .other("*/*"), quality: 0.3), + .init(contentType: .json, quality: 0.5), + ] + XCTAssertEqual( + unsorted.sortedByQuality(), + [ + .init(contentType: .json, quality: 0.5), + .init(contentType: .other("*/*"), quality: 0.3), + ] + ) + } + } +} + +final class Test_QualityValue: Test_Runtime { + func test() { + XCTAssertEqual(QualityValue().doubleValue, 1.0) + XCTAssertTrue(QualityValue().isDefault) + XCTAssertFalse(QualityValue(doubleValue: 0.5).isDefault) + XCTAssertEqual(QualityValue(doubleValue: 0.5).doubleValue, 0.5) + XCTAssertEqual(QualityValue(floatLiteral: 0.5).doubleValue, 0.5) + XCTAssertEqual(QualityValue(integerLiteral: 0).doubleValue, 0) + XCTAssertEqual(QualityValue(rawValue: "1.0")?.doubleValue, 1.0) + XCTAssertEqual(QualityValue(rawValue: "0.0")?.doubleValue, 0.0) + XCTAssertEqual(QualityValue(rawValue: "0.3")?.doubleValue, 0.3) + XCTAssertEqual(QualityValue(rawValue: "0.5")?.rawValue, "0.500") + XCTAssertNil(QualityValue(rawValue: "hi")) + } +} From 2507b14f3442eec7ed3fff3793b98dcfd4249468 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 23 Aug 2023 13:49:18 +0200 Subject: [PATCH 08/10] Add more tests --- .../Conversion/Test_Converter+Client.swift | 14 ++++++++++++++ .../Conversion/Test_Converter+Server.swift | 16 ++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift index 8dd24f23..370d44e9 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift @@ -16,6 +16,20 @@ import XCTest final class Test_ClientConverterExtensions: Test_Runtime { + func test_setAcceptHeader() throws { + var headerFields: [HeaderField] = [] + converter.setAcceptHeader( + in: &headerFields, + contentTypes: [.init(contentType: TestAcceptable.json, quality: 0.8)] + ) + XCTAssertEqual( + headerFields, + [ + .init(name: "accept", value: "application/json; q=0.800") + ] + ) + } + // MARK: Converter helper methods // | client | set | request path | text | string-convertible | required | renderedRequestPath | diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift index 5436d9dc..8cd8bc5e 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift @@ -16,6 +16,22 @@ import XCTest final class Test_ServerConverterExtensions: Test_Runtime { + func testExtractAccept() throws { + let headerFields: [HeaderField] = [ + .init(name: "accept", value: "application/json, */*; q=0.8") + ] + let accept: [AcceptHeaderContentType] = try converter.extractAcceptHeaderIfPresent( + in: headerFields + ) + XCTAssertEqual( + accept, + [ + .init(contentType: .json, quality: 1.0), + .init(contentType: .other("*/*"), quality: 0.8), + ] + ) + } + // MARK: Miscs func testValidateAccept() throws { From c33ec1706e2fe0e313dd4d119baa5a40b1a070b1 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 24 Aug 2023 13:21:27 +0200 Subject: [PATCH 09/10] Update Sources/OpenAPIRuntime/Base/Acceptable.swift Co-authored-by: George Barnett --- Sources/OpenAPIRuntime/Base/Acceptable.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/Base/Acceptable.swift b/Sources/OpenAPIRuntime/Base/Acceptable.swift index 1cc092d9..76b36d89 100644 --- a/Sources/OpenAPIRuntime/Base/Acceptable.swift +++ b/Sources/OpenAPIRuntime/Base/Acceptable.swift @@ -114,7 +114,7 @@ public struct AcceptHeaderContentType: Sendable /// - Parameters: /// - value: The value representing the content type. /// - quality: The quality of the content type, between 0.0 and 1.0. - /// - Precondition: Priority must be in the range 0.0 and 1.0 inclusive. + /// - Precondition: Quality must be in the range 0.0 and 1.0 inclusive. public init(contentType: ContentType, quality: QualityValue = 1.0) { self.quality = quality self.contentType = contentType From 068b34e4e9da4faa920e692f46ab1b77fcc69cef Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 24 Aug 2023 14:07:48 +0200 Subject: [PATCH 10/10] PR feedback --- Sources/OpenAPIRuntime/Base/Acceptable.swift | 5 ----- Tests/OpenAPIRuntimeTests/Base/Test_Acceptable.swift | 6 +++--- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/Sources/OpenAPIRuntime/Base/Acceptable.swift b/Sources/OpenAPIRuntime/Base/Acceptable.swift index 1cc092d9..f2c467f5 100644 --- a/Sources/OpenAPIRuntime/Base/Acceptable.swift +++ b/Sources/OpenAPIRuntime/Base/Acceptable.swift @@ -27,11 +27,6 @@ public struct QualityValue: Sendable, Hashable { /// 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 { diff --git a/Tests/OpenAPIRuntimeTests/Base/Test_Acceptable.swift b/Tests/OpenAPIRuntimeTests/Base/Test_Acceptable.swift index 4b866b9d..405af225 100644 --- a/Tests/OpenAPIRuntimeTests/Base/Test_Acceptable.swift +++ b/Tests/OpenAPIRuntimeTests/Base/Test_Acceptable.swift @@ -92,8 +92,8 @@ final class Test_AcceptHeaderContentType: Test_Runtime { final class Test_QualityValue: Test_Runtime { func test() { - XCTAssertEqual(QualityValue().doubleValue, 1.0) - XCTAssertTrue(QualityValue().isDefault) + XCTAssertEqual((1 as QualityValue).doubleValue, 1.0) + XCTAssertTrue((1 as QualityValue).isDefault) XCTAssertFalse(QualityValue(doubleValue: 0.5).isDefault) XCTAssertEqual(QualityValue(doubleValue: 0.5).doubleValue, 0.5) XCTAssertEqual(QualityValue(floatLiteral: 0.5).doubleValue, 0.5) @@ -101,7 +101,7 @@ final class Test_QualityValue: Test_Runtime { XCTAssertEqual(QualityValue(rawValue: "1.0")?.doubleValue, 1.0) XCTAssertEqual(QualityValue(rawValue: "0.0")?.doubleValue, 0.0) XCTAssertEqual(QualityValue(rawValue: "0.3")?.doubleValue, 0.3) - XCTAssertEqual(QualityValue(rawValue: "0.5")?.rawValue, "0.500") + XCTAssertEqual(QualityValue(rawValue: "0.54321")?.rawValue, "0.543") XCTAssertNil(QualityValue(rawValue: "hi")) } }