-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Runtime] Accept header ### Motivation SOAR-0003 was accepted, this is the runtime side of the implementation. ### Modifications - Introduced a new protocol `AcceptableProtocol`, which all the generated, operation-specific Accept enums conform to. - Introduced a new struct `QualityValue`, which wraps the quality parameter of the content type. Since the precision is capped at 3 decimal places, the internal storage is in 1000's, allowing for more reliable comparisons, as floating point numbers are only used when serialized into headers. - Introduced a new struct `AcceptHeaderContentType`, generic over `AcceptableProtocol`, which adds `QualityValue` to each generated Accept enum. - Introduced new extensions on `Converter` that allow setting and getting the Accept header. ### Result These are the requirements for apple/swift-openapi-generator#185. ### Test Plan Added unit tests for both `QualityValue` and `AcceptHeaderContentType`, and for the new `Converter` methods. Reviewed by: gjcairo Builds: ✔︎ pull request validation (5.8) - Build finished. ✔︎ pull request validation (5.9) - Build finished. ✔︎ pull request validation (api breakage) - Build finished. ✔︎ pull request validation (docc test) - Build finished. ✔︎ pull request validation (integration test) - Build finished. ✔︎ pull request validation (nightly) - Build finished. ✔︎ pull request validation (soundness) - Build finished. #37
- Loading branch information
Showing
9 changed files
with
352 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// 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, 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, 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 | ||
|
||
/// 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, | ||
"Provided quality number is out of range, must be between 0.0 and 1.0, inclusive." | ||
) | ||
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: 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 | ||
} | ||
} | ||
|
||
extension QualityValue: ExpressibleByFloatLiteral { | ||
public init(floatLiteral value: Double) { | ||
self.init(doubleValue: value) | ||
} | ||
} | ||
|
||
extension Array { | ||
|
||
/// Returns the default values for the acceptable type. | ||
public static func defaultValues<T: AcceptableProtocol>() -> [AcceptHeaderContentType<T>] | ||
where Element == AcceptHeaderContentType<T> { | ||
T.allCases.map { .init(contentType: $0) } | ||
} | ||
} | ||
|
||
/// A wrapper of an individual content type in the accept header. | ||
public struct AcceptHeaderContentType<ContentType: AcceptableProtocol>: Sendable, Hashable { | ||
|
||
/// The value representing the content type. | ||
public var contentType: ContentType | ||
|
||
/// 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 var quality: QualityValue | ||
|
||
/// Creates a new content type from the provided parameters. | ||
/// - Parameters: | ||
/// - value: The value representing the content type. | ||
/// - quality: The quality of the content type, between 0.0 and 1.0. | ||
/// - 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 | ||
} | ||
|
||
/// 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.allCases.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 = ContentType(rawValue: validMimeType.kind.description.lowercased()) else { | ||
// Invalid type/subtype. | ||
return nil | ||
} | ||
self.init(contentType: typeAndSubtype, quality: quality) | ||
} | ||
|
||
public var rawValue: String { | ||
contentType.rawValue + (quality.isDefault ? "" : "; q=\(quality.rawValue)") | ||
} | ||
} | ||
|
||
extension Array { | ||
|
||
/// Returns the array sorted by the quality value, highest quality first. | ||
public func sortedByQuality<T: AcceptableProtocol>() -> [AcceptHeaderContentType<T>] | ||
where Element == AcceptHeaderContentType<T> { | ||
sorted { a, b in | ||
a.quality.doubleValue > b.quality.doubleValue | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<TestAcceptable>(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<TestAcceptable>(rawValue: "application/json; q=0.500"), | ||
contentType | ||
) | ||
} | ||
do { | ||
XCTAssertEqual( | ||
AcceptHeaderContentType<TestAcceptable>.defaultValues, | ||
[ | ||
.init(contentType: .json) | ||
] | ||
) | ||
} | ||
do { | ||
let unsorted: [AcceptHeaderContentType<TestAcceptable>] = [ | ||
.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((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) | ||
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.54321")?.rawValue, "0.543") | ||
XCTAssertNil(QualityValue(rawValue: "hi")) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.