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

Validate content type strings in validateDoc #471

85 changes: 85 additions & 0 deletions Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,87 @@

import OpenAPIKit

/// Validates all content types from an OpenAPI document represented by a ParsedOpenAPIRepresentation.
///
/// This function iterates through the paths, endpoints, and components of the OpenAPI document,
/// checking and reporting any invalid content types using the provided validation closure.
///
/// - Parameters:
/// - doc: The OpenAPI document representation.
/// - validate: A closure to validate each content type.
/// - Throws: An error with diagnostic information if any invalid content types are found.
func validateContentTypes(in doc: ParsedOpenAPIRepresentation, validate: (String) -> Bool) throws {
for (path, pathValue) in doc.paths {
guard case .b(let pathItem) = pathValue else { continue }
for endpoint in pathItem.endpoints {

if let eitherRequest = endpoint.operation.requestBody {
if case .b(let actualRequest) = eitherRequest {
for contentType in actualRequest.content.keys {
if !validate(contentType.rawValue) {
throw Diagnostic.error(
message: "Invalid content type string.",
context: [
"contentType": contentType.rawValue,
"location": "\(path.rawValue)/\(endpoint.method.rawValue)/requestBody",
"recoverySuggestion":
"Must have 2 components separated by a slash '<type>/<subtype>'.",
]
)
}
}
}
}

for eitherResponse in endpoint.operation.responses.values {
if case .b(let actualResponse) = eitherResponse {
for contentType in actualResponse.content.keys {
if !validate(contentType.rawValue) {
throw Diagnostic.error(
message: "Invalid content type string.",
context: [
"contentType": contentType.rawValue,
"location": "\(path.rawValue)/\(endpoint.method.rawValue)/responses",
"recoverySuggestion":
"Must have 2 components separated by a slash '<type>/<subtype>'.",
]
)
}
}
}
}
}
}

czechboy0 marked this conversation as resolved.
Show resolved Hide resolved
for (key, component) in doc.components.requestBodies {
for contentType in component.content.keys {
if !validate(contentType.rawValue) {
throw Diagnostic.error(
message: "Invalid content type string.",
context: [
"contentType": contentType.rawValue, "location": "#/components/requestBodies/\(key.rawValue)",
"recoverySuggestion": "Must have 2 components separated by a slash '<type>/<subtype>'.",
]
)
}
}
}

for (key, component) in doc.components.responses {
for contentType in component.content.keys {
if !validate(contentType.rawValue) {
throw Diagnostic.error(
message: "Invalid content type string.",
context: [
"contentType": contentType.rawValue, "location": "#/components/responses/\(key.rawValue)",
"recoverySuggestion": "Must have 2 components separated by a slash '<type>/<subtype>'.",
]
)
}
}
}
}

/// Runs validation steps on the incoming OpenAPI document.
/// - Parameters:
/// - doc: The OpenAPI document to validate.
Expand All @@ -30,6 +111,10 @@ func validateDoc(_ doc: ParsedOpenAPIRepresentation, config: Config) throws -> [
// block the generator from running.
// Validation errors continue to be fatal, such as
// structural issues, like non-unique operationIds, etc.
try validateContentTypes(in: doc) { contentType in
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
}

let warnings = try doc.validate(using: Validator().validating(.operationsContainResponses), strict: false)
let diagnostics: [Diagnostic] = warnings.map { warning in
.warning(
Expand Down
239 changes: 239 additions & 0 deletions Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,243 @@ final class Test_validateDoc: Test_Core {
XCTAssertThrowsError(try validateDoc(doc, config: .init(mode: .types, access: Config.defaultAccessModifier)))
}

func testValidateContentTypes_validContentTypes() throws {
let doc = OpenAPI.Document(
info: .init(title: "Test", version: "1.0.0"),
servers: [],
paths: [
"/path1": .b(
.init(
get: .init(
requestBody: .b(
.init(content: [.init(rawValue: "application/xml")!: .init(schema: .string)])
),
responses: [
.init(integerLiteral: 200): .b(
.init(
description: "Test description 1",
content: [.init(rawValue: "application/json")!: .init(schema: .string)]
)
)
]
)
)
),
"/path2": .b(
.init(
get: .init(
requestBody: .b(.init(content: [.init(rawValue: "text/html")!: .init(schema: .string)])),
responses: [
.init(integerLiteral: 200): .b(
.init(
description: "Test description 2",
content: [.init(rawValue: "text/plain")!: .init(schema: .string)]
)
)
]
)
)
),
],
components: .noComponents
)
XCTAssertNoThrow(
try validateContentTypes(in: doc) { contentType in
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
}
)
}

func testValidateContentTypes_invalidContentTypesInRequestBody() throws {
let doc = OpenAPI.Document(
info: .init(title: "Test", version: "1.0.0"),
servers: [],
paths: [
"/path1": .b(
.init(
get: .init(
requestBody: .b(.init(content: [.init(rawValue: "application/")!: .init(schema: .string)])),
responses: [
.init(integerLiteral: 200): .b(
.init(
description: "Test description 1",
content: [.init(rawValue: "application/json")!: .init(schema: .string)]
)
)
]
)
)
),
"/path2": .b(
.init(
get: .init(
requestBody: .b(.init(content: [.init(rawValue: "text/html")!: .init(schema: .string)])),
responses: [
.init(integerLiteral: 200): .b(
.init(
description: "Test description 2",
content: [.init(rawValue: "text/plain")!: .init(schema: .string)]
)
)
]
)
)
),
],
components: .noComponents
)
XCTAssertThrowsError(
try validateContentTypes(in: doc) { contentType in
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
}
) { error in
XCTAssertTrue(error is Diagnostic)
XCTAssertEqual(
error.localizedDescription,
"error: Invalid content type string. [context: contentType=application/, location=/path1/GET/requestBody, recoverySuggestion=Must have 2 components separated by a slash '<type>/<subtype>'.]"
)
}
}

func testValidateContentTypes_invalidContentTypesInResponses() throws {
let doc = OpenAPI.Document(
info: .init(title: "Test", version: "1.0.0"),
servers: [],
paths: [
"/path1": .b(
.init(
get: .init(
requestBody: .b(
.init(content: [.init(rawValue: "application/xml")!: .init(schema: .string)])
),
responses: [
.init(integerLiteral: 200): .b(
.init(
description: "Test description 1",
content: [.init(rawValue: "application/json")!: .init(schema: .string)]
)
)
]
)
)
),
"/path2": .b(
.init(
get: .init(
requestBody: .b(.init(content: [.init(rawValue: "text/html")!: .init(schema: .string)])),
responses: [
.init(integerLiteral: 200): .b(
.init(
description: "Test description 2",
content: [.init(rawValue: "/plain")!: .init(schema: .string)]
)
)
]
)
)
),
],
components: .noComponents
)
XCTAssertThrowsError(
try validateContentTypes(in: doc) { contentType in
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
}
) { error in
XCTAssertTrue(error is Diagnostic)
XCTAssertEqual(
error.localizedDescription,
"error: Invalid content type string. [context: contentType=/plain, location=/path2/GET/responses, recoverySuggestion=Must have 2 components separated by a slash '<type>/<subtype>'.]"
)
}
}

func testValidateContentTypes_invalidContentTypesInComponentsRequestBodies() throws {
let doc = OpenAPI.Document(
info: .init(title: "Test", version: "1.0.0"),
servers: [],
paths: [
"/path1": .b(
.init(
get: .init(
requestBody: .b(
.init(content: [.init(rawValue: "application/xml")!: .init(schema: .string)])
),
responses: [
.init(integerLiteral: 200): .b(
.init(
description: "Test description 1",
content: [.init(rawValue: "application/json")!: .init(schema: .string)]
)
)
]
)
)
)
],
components: .init(requestBodies: [
"exampleRequestBody1": .init(content: [.init(rawValue: "application/pdf")!: .init(schema: .string)]),
"exampleRequestBody2": .init(content: [.init(rawValue: "image/")!: .init(schema: .string)]),
])
)
XCTAssertThrowsError(
try validateContentTypes(in: doc) { contentType in
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
}
) { error in
XCTAssertTrue(error is Diagnostic)
XCTAssertEqual(
error.localizedDescription,
"error: Invalid content type string. [context: contentType=image/, location=#/components/requestBodies/exampleRequestBody2, recoverySuggestion=Must have 2 components separated by a slash '<type>/<subtype>'.]"
)
}
}

func testValidateContentTypes_invalidContentTypesInComponentsResponses() throws {
let doc = OpenAPI.Document(
info: .init(title: "Test", version: "1.0.0"),
servers: [],
paths: [
"/path1": .b(
.init(
get: .init(
requestBody: .b(
.init(content: [.init(rawValue: "application/xml")!: .init(schema: .string)])
),
responses: [
.init(integerLiteral: 200): .b(
.init(
description: "Test description 1",
content: [.init(rawValue: "application/json")!: .init(schema: .string)]
)
)
]
)
)
)
],
components: .init(responses: [
"exampleRequestBody1": .init(
description: "Test description 1",
content: [.init(rawValue: "application/pdf")!: .init(schema: .string)]
),
"exampleRequestBody2": .init(
description: "Test description 2",
content: [.init(rawValue: "")!: .init(schema: .string)]
),
])
)
XCTAssertThrowsError(
try validateContentTypes(in: doc) { contentType in
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
}
) { error in
XCTAssertTrue(error is Diagnostic)
XCTAssertEqual(
error.localizedDescription,
"error: Invalid content type string. [context: contentType=, location=#/components/responses/exampleRequestBody2, recoverySuggestion=Must have 2 components separated by a slash '<type>/<subtype>'.]"
)
}
}

}