Skip to content

[Runtime] Multiple content types support #29

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

Merged
merged 20 commits into from
Aug 4, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f997ab1
[WIP] Multiple content types
czechboy0 Jul 27, 2023
66b78af
Try designing EncodedBodyContent away
czechboy0 Jul 28, 2023
00e9b40
Deprecate unit tests that call deprecated methods
czechboy0 Jul 28, 2023
f6c7f23
Stop using EncodableBodyContent in responses as well
czechboy0 Jul 28, 2023
59f199f
Update tests as well
czechboy0 Jul 28, 2023
25049e8
Move a function for a smaller PR diff
czechboy0 Jul 31, 2023
c384fc8
Merge branch 'main' into hd-draft-multiple-content-types
czechboy0 Aug 1, 2023
0ec13e5
Merge branch 'main' into hd-draft-multiple-content-types
czechboy0 Aug 1, 2023
a882dc8
Merge branch 'main' into hd-draft-multiple-content-types
czechboy0 Aug 3, 2023
ddc431d
Update Sources/OpenAPIRuntime/Conversion/Converter+Common.swift
czechboy0 Aug 3, 2023
efc85c2
PR feedback: Introduce a MIMEType wrapper type
czechboy0 Aug 3, 2023
7f832ac
Fix formatting
czechboy0 Aug 3, 2023
606aa0e
PR feedback: s/isValidContentType/isMatchingContentType
czechboy0 Aug 3, 2023
5752e18
PR feedback: no need to prefix fileprivate extensions
czechboy0 Aug 3, 2023
de2248b
PR feedback: restructure MIMEType to disallow json/*
czechboy0 Aug 3, 2023
09b68a7
PR feedback: rename MIMEType to OpenAPIMIMEType
czechboy0 Aug 3, 2023
6df8431
PR feedback: Add a comment about how we're handling duplicate paramet…
czechboy0 Aug 3, 2023
e0dae85
PR feedback: Make isMatchingContentType stricter + rename expected to…
czechboy0 Aug 3, 2023
f4678b2
Only allocate dictionaries if there are parameters
czechboy0 Aug 3, 2023
63f44b0
Merge branch 'main' into hd-draft-multiple-content-types
czechboy0 Aug 4, 2023
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
73 changes: 55 additions & 18 deletions Sources/OpenAPIRuntime/Conversion/Converter+Common.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,31 +17,68 @@ extension Converter {

// MARK: Miscs

/// Validates that the Content-Type header field (if present)
/// is compatible with the provided content-type substring.
/// Returns the content-type header from the provided header fields, if
/// present.
/// - Parameter headerFields: The header fields to inspect for the content
/// type header.
/// - Returns: The content type value, or nil if not found.
public func extractContentTypeIfPresent(in headerFields: [HeaderField]) -> String? {
headerFields.firstValue(name: "content-type")
}

/// Checks whether a concrete content type matches an expected content type.
///
/// Succeeds if no Content-Type header is found in the response headers.
/// The concrete content type can contain parameters, such as `charset`, but
/// they are ignored in the equality comparison.
///
/// The expected content type can contain wildcards, such as */* and text/*.
/// - Parameters:
/// - headerFields: Header fields to inspect for a content type.
/// - substring: Expected content type.
/// - Throws: If the response's Content-Type value is not compatible with the provided substring.
public func validateContentTypeIfPresent(
in headerFields: [HeaderField],
substring: String
) throws {
/// - received: The concrete content type to validate against the other.
/// - expected: The expected content type, can be a wildcard.
/// - Returns: A Boolean value representing whether the concrete content
/// type matches the expected one.
public func isValidContentType(received: String?, expected: String) -> Bool {
guard let received else {
return false
}
func parseContentType(_ value: String) -> (main: String, sub: String)? {
let components =
value
// Normalize to lowercase.
.lowercased()
// Drop any charset and other parameters.
.split(separator: ";")[0]
// Parse out main type and subtype.
.split(separator: "/")
.map(String.init)
guard components.count == 2 else {
return nil
}
return (components[0], components[1])
}
guard
let contentType = try getOptionalHeaderFieldAsText(
in: headerFields,
name: "content-type",
as: String.self
)
let receivedContentType = parseContentType(received),
let expectedContentType = parseContentType(expected)
else {
return
return false
}
if expectedContentType.main == "*" {
return true
}
if expectedContentType.main != receivedContentType.main {
return false
}
guard contentType.localizedCaseInsensitiveContains(substring) else {
throw RuntimeError.unexpectedContentTypeHeader(contentType)
if expectedContentType.sub == "*" {
return true
}
return expectedContentType.sub == receivedContentType.sub
}

/// Returns an error to be thrown when an unexpected content type is
/// received.
/// - Parameter contentType: The content type that was received.
public func makeUnexpectedContentTypeError(contentType: String?) -> any Error {
RuntimeError.unexpectedContentTypeHeader(contentType ?? "")
}

// MARK: - Converter helper methods
Expand Down
23 changes: 23 additions & 0 deletions Sources/OpenAPIRuntime/Deprecated/Deprecated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,29 @@ extension Converter {

extension Converter {

/// Validates that the Content-Type header field (if present)
/// is compatible with the provided content-type substring.
///
/// Succeeds if no Content-Type header is found in the response headers.
///
/// - Parameters:
/// - headerFields: Header fields to inspect for a content type.
/// - substring: Expected content type.
/// - Throws: If the response's Content-Type value is not compatible with
/// the provided substring.
@available(*, deprecated, message: "Use isValidContentType instead.")
public func validateContentTypeIfPresent(
in headerFields: [HeaderField],
substring: String
) throws {
guard let contentType = extractContentTypeIfPresent(in: headerFields) else {
return
}
guard isValidContentType(received: contentType, expected: substring) else {
throw RuntimeError.unexpectedContentTypeHeader(contentType)
}
}

// | client | set | request body | text | string-convertible | optional | setOptionalRequestBodyAsText |
@available(*, deprecated)
public func setOptionalRequestBodyAsText<T: _StringConvertible, C>(
Expand Down
76 changes: 24 additions & 52 deletions Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,61 +18,33 @@ final class Test_CommonConverterExtensions: Test_Runtime {

// MARK: Miscs

func testValidateContentType_match() throws {
let headerFields: [HeaderField] = [
.init(name: "content-type", value: "application/json")
]
XCTAssertNoThrow(
try converter.validateContentTypeIfPresent(
in: headerFields,
substring: "application/json"
)
)
}
func testContentTypeMatching() throws {
let cases: [(received: String, expected: String, isMatch: Bool)] = [
("application/json", "application/json", true),
("APPLICATION/JSON", "application/json", true),
("application/json", "application/*", true),
("application/json", "*/*", true),
("application/json", "text/*", false),
("application/json", "application/xml", false),
("application/json", "text/plain", false),

func testValidateContentType_match_substring() throws {
let headerFields: [HeaderField] = [
.init(name: "content-type", value: "application/json; charset=utf-8")
("text/plain; charset=UTF-8", "text/plain", true),
("TEXT/PLAIN; CHARSET=UTF-8", "text/plain", true),
("text/plain; charset=UTF-8", "text/*", true),
("text/plain; charset=UTF-8", "*/*", true),
("text/plain; charset=UTF-8", "application/*", false),
("text/plain; charset=UTF-8", "text/html", false),
]
XCTAssertNoThrow(
try converter.validateContentTypeIfPresent(
in: headerFields,
substring: "application/json"
)
)
}

func testValidateContentType_missing() throws {
let headerFields: [HeaderField] = []
XCTAssertNoThrow(
try converter.validateContentTypeIfPresent(
in: headerFields,
substring: "application/json"
for testCase in cases {
XCTAssertEqual(
converter.isValidContentType(
received: testCase.received,
expected: testCase.expected
),
testCase.isMatch,
"Wrong result for (\(testCase.received), \(testCase.expected), \(testCase.isMatch))"
)
)
}

func testValidateContentType_mismatch() throws {
let headerFields: [HeaderField] = [
.init(name: "content-type", value: "text/plain")
]
XCTAssertThrowsError(
try converter.validateContentTypeIfPresent(
in: headerFields,
substring: "application/json"
),
"Was expected to throw error on mismatch",
{ error in
guard
let err = error as? RuntimeError,
case .unexpectedContentTypeHeader(let contentType) = err
else {
XCTFail("Unexpected kind of error thrown")
return
}
XCTAssertEqual(contentType, "text/plain")
}
)
}
}

// MARK: Converter helper methods
Expand Down
61 changes: 61 additions & 0 deletions Tests/OpenAPIRuntimeTests/Deprecated/Test_Deprecated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,67 @@ final class Test_Deprecated: Test_Runtime {
)
}

@available(*, deprecated)
func testValidateContentType_match() throws {
let headerFields: [HeaderField] = [
.init(name: "content-type", value: "application/json")
]
XCTAssertNoThrow(
try converter.validateContentTypeIfPresent(
in: headerFields,
substring: "application/json"
)
)
}

@available(*, deprecated)
func testValidateContentType_match_substring() throws {
let headerFields: [HeaderField] = [
.init(name: "content-type", value: "application/json; charset=utf-8")
]
XCTAssertNoThrow(
try converter.validateContentTypeIfPresent(
in: headerFields,
substring: "application/json"
)
)
}

@available(*, deprecated)
func testValidateContentType_missing() throws {
let headerFields: [HeaderField] = []
XCTAssertNoThrow(
try converter.validateContentTypeIfPresent(
in: headerFields,
substring: "application/json"
)
)
}

@available(*, deprecated)
func testValidateContentType_mismatch() throws {
let headerFields: [HeaderField] = [
.init(name: "content-type", value: "text/plain")
]
XCTAssertThrowsError(
try converter.validateContentTypeIfPresent(
in: headerFields,
substring: "application/json"
),
"Was expected to throw error on mismatch",
{ error in
guard
let err = error as? RuntimeError,
case .unexpectedContentTypeHeader(let contentType) = err
else {
XCTFail("Unexpected kind of error thrown")
return
}
XCTAssertEqual(contentType, "text/plain")
}
)
}

// | server | set | response body | text | string-convertible | required | setResponseBodyAsText |
@available(*, deprecated)
func test_deprecated_setResponseBodyAsText_stringConvertible() throws {
Expand Down