diff --git a/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift b/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift index 5b057f04..ecc5ef82 100644 --- a/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift +++ b/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift @@ -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 '/'.", + ] + ) + } + } + } + } + + 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 '/'.", + ] + ) + } + } + } + } + } + } + + 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 '/'.", + ] + ) + } + } + } + + 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 '/'.", + ] + ) + } + } + } +} + /// Runs validation steps on the incoming OpenAPI document. /// - Parameters: /// - doc: The OpenAPI document to validate. @@ -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( diff --git a/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift b/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift index b791a0d7..e88f00ec 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift @@ -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 '/'.]" + ) + } + } + + 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 '/'.]" + ) + } + } + + 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 '/'.]" + ) + } + } + + 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 '/'.]" + ) + } + } + }