Skip to content

Commit

Permalink
Validate content type strings in validateDoc (#471)
Browse files Browse the repository at this point in the history
### Motivation

- Fixes #342.

### Modifications

- Create extracting and validation functions for content type strings.

### Result

- This will help catch invalid content types before code generation.

### Test Plan

- Test it on an openapi document to make sure the errors are thrown as
intended and wrote additional unit tests.

---------

Co-authored-by: Honza Dvorsky <[email protected]>
  • Loading branch information
PARAIPAN9 and czechboy0 authored Dec 20, 2023
1 parent 76994bf commit 85fec92
Show file tree
Hide file tree
Showing 2 changed files with 324 additions and 0 deletions.
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>'.",
]
)
}
}
}
}
}
}

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>'.]"
)
}
}

}

0 comments on commit 85fec92

Please sign in to comment.