From 3f1f49be077ece0ef8de7ced49648c143089d771 Mon Sep 17 00:00:00 2001 From: PARAIPAN SORIN Date: Mon, 11 Dec 2023 17:05:08 +0200 Subject: [PATCH 1/6] Add specific errors to content type --- .../Translator/Content/ContentType.swift | 41 +++++++++++++++---- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift b/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift index 20a94573..ebb63180 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift @@ -143,14 +143,7 @@ struct ContentType: Hashable { /// not be empty. /// - Throws: If a malformed content type string is encountered. init(string: String) throws { - struct InvalidContentTypeString: Error, LocalizedError, CustomStringConvertible { - var string: String - var description: String { - "Invalid content type string: '\(string)', must have 2 components separated by a slash." - } - var errorDescription: String? { description } - } - guard !string.isEmpty else { throw InvalidContentTypeString(string: "") } + guard !string.isEmpty else { throw ContentTypeError.emptyString(string) } var semiComponents = string.split(separator: ";") let typeAndSubtypeComponent = semiComponents.removeFirst() self.originallyCasedParameterPairs = semiComponents.map { component in @@ -158,8 +151,12 @@ struct ContentType: Hashable { .joined(separator: "=") } let rawTypeAndSubtype = typeAndSubtypeComponent.trimmingCharacters(in: .whitespaces) + if rawTypeAndSubtype.isEmpty { throw ContentTypeError.whitespacesString(string) } let typeAndSubtype = rawTypeAndSubtype.split(separator: "/").map(String.init) - guard typeAndSubtype.count == 2 else { throw InvalidContentTypeString(string: rawTypeAndSubtype) } + guard typeAndSubtype.count == 2 else { + if typeAndSubtype.count > 2 { throw ContentTypeError.excessiveComponents(rawTypeAndSubtype) } + throw ContentTypeError.genericError(rawTypeAndSubtype) + } self.originallyCasedType = typeAndSubtype[0] self.originallyCasedSubtype = typeAndSubtype[1] } @@ -229,6 +226,32 @@ struct ContentType: Hashable { lhs.lowercasedTypeAndSubtype == rhs.lowercasedTypeAndSubtype } + private enum ContentTypeError: Error, LocalizedError, CustomStringConvertible { + case emptyString(_ contentType: String) + case whitespacesString(_ contentType: String) + case excessiveComponents(_ contentType: String) + case genericError(_ contentType: String) + + var description: String { + switch self { + case .emptyString(let contentTypeString): + return + "Invalid content type string: '\(contentTypeString)' is empty, must have 2 components separated by a slash '/'." + case .whitespacesString(let contentTypeString): + return + "Invalid content type string: '\(contentTypeString)' is formed of whitespaces, must have 2 components separated by a slash '/'." + case .excessiveComponents(let contentTypeString): + return + "Invalid content type string: '\(contentTypeString)' has an excessive number of components, must have 2 components separated by a slash '/'." + case .genericError(let contentTypeString): + return + "Invalid content type string: '\(contentTypeString)' must have 2 components separated by a slash '/'." + } + } + + var errorDescription: String? { description } + } + func hash(into hasher: inout Hasher) { hasher.combine(lowercasedTypeAndSubtype) } } From 6482529532bf036b7a545a24a10e0e8b640cfbef Mon Sep 17 00:00:00 2001 From: PARAIPAN SORIN Date: Tue, 19 Dec 2023 00:02:37 +0200 Subject: [PATCH 2/6] Revert changes --- .../Translator/Content/ContentType.swift | 41 ++++--------------- 1 file changed, 9 insertions(+), 32 deletions(-) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift b/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift index ebb63180..20a94573 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift @@ -143,7 +143,14 @@ struct ContentType: Hashable { /// not be empty. /// - Throws: If a malformed content type string is encountered. init(string: String) throws { - guard !string.isEmpty else { throw ContentTypeError.emptyString(string) } + struct InvalidContentTypeString: Error, LocalizedError, CustomStringConvertible { + var string: String + var description: String { + "Invalid content type string: '\(string)', must have 2 components separated by a slash." + } + var errorDescription: String? { description } + } + guard !string.isEmpty else { throw InvalidContentTypeString(string: "") } var semiComponents = string.split(separator: ";") let typeAndSubtypeComponent = semiComponents.removeFirst() self.originallyCasedParameterPairs = semiComponents.map { component in @@ -151,12 +158,8 @@ struct ContentType: Hashable { .joined(separator: "=") } let rawTypeAndSubtype = typeAndSubtypeComponent.trimmingCharacters(in: .whitespaces) - if rawTypeAndSubtype.isEmpty { throw ContentTypeError.whitespacesString(string) } let typeAndSubtype = rawTypeAndSubtype.split(separator: "/").map(String.init) - guard typeAndSubtype.count == 2 else { - if typeAndSubtype.count > 2 { throw ContentTypeError.excessiveComponents(rawTypeAndSubtype) } - throw ContentTypeError.genericError(rawTypeAndSubtype) - } + guard typeAndSubtype.count == 2 else { throw InvalidContentTypeString(string: rawTypeAndSubtype) } self.originallyCasedType = typeAndSubtype[0] self.originallyCasedSubtype = typeAndSubtype[1] } @@ -226,32 +229,6 @@ struct ContentType: Hashable { lhs.lowercasedTypeAndSubtype == rhs.lowercasedTypeAndSubtype } - private enum ContentTypeError: Error, LocalizedError, CustomStringConvertible { - case emptyString(_ contentType: String) - case whitespacesString(_ contentType: String) - case excessiveComponents(_ contentType: String) - case genericError(_ contentType: String) - - var description: String { - switch self { - case .emptyString(let contentTypeString): - return - "Invalid content type string: '\(contentTypeString)' is empty, must have 2 components separated by a slash '/'." - case .whitespacesString(let contentTypeString): - return - "Invalid content type string: '\(contentTypeString)' is formed of whitespaces, must have 2 components separated by a slash '/'." - case .excessiveComponents(let contentTypeString): - return - "Invalid content type string: '\(contentTypeString)' has an excessive number of components, must have 2 components separated by a slash '/'." - case .genericError(let contentTypeString): - return - "Invalid content type string: '\(contentTypeString)' must have 2 components separated by a slash '/'." - } - } - - var errorDescription: String? { description } - } - func hash(into hasher: inout Hasher) { hasher.combine(lowercasedTypeAndSubtype) } } From 4d8acc53e86ac9d226a5b8f044d168bf600b4f1f Mon Sep 17 00:00:00 2001 From: PARAIPAN SORIN Date: Tue, 19 Dec 2023 00:04:31 +0200 Subject: [PATCH 3/6] Create extracting and validation funcs for content types and write unit tests --- .../Parser/validateDoc.swift | 55 +++++++++++++ .../Parser/Test_validateDoc.swift | 82 +++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift b/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift index 5b057f04..e0357e00 100644 --- a/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift +++ b/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift @@ -13,6 +13,58 @@ //===----------------------------------------------------------------------===// import OpenAPIKit +import Foundation + +/// Extracts content types from a ParsedOpenAPIRepresentation. +/// +/// - Parameter doc: The OpenAPI document representation. +/// - Returns: An array of strings representing content types extracted from requests and responses. +func extractContentTypes(from doc: ParsedOpenAPIRepresentation) -> [String] { + let contentTypes: [String] = doc.paths.values.flatMap { pathValue -> [OpenAPI.ContentType.RawValue] in + guard case .b(let pathItem) = pathValue else { return [] } + + let requestBodyContentTypes: [String] = pathItem.endpoints.map { $0.operation.requestBody } + .compactMap { (eitherRequest: Either, OpenAPI.Request>?) in + guard case .b(let actualRequest) = eitherRequest else { return nil } + return actualRequest.content.keys.first?.rawValue + } + + let responseContentTypes: [String] = pathItem.endpoints.map { $0.operation.responses.values } + .flatMap { (response: [Either, OpenAPI.Response>]) in + response.compactMap { (eitherResponse: Either, OpenAPI.Response>) in + guard case .b(let actualResponse) = eitherResponse else { return nil } + return actualResponse.content.keys.first?.rawValue + } + } + + return requestBodyContentTypes + responseContentTypes + } + + return contentTypes +} + +/// Validates an array of content types. +/// +/// - Parameter contentTypes: An array of strings representing content types. +/// - Throws: A Diagnostic error if any content type is invalid. +func validateContentTypes(_ contentTypes: [String]) throws { + let mediaTypePattern = "^[a-zA-Z]+/[a-zA-Z]+$" + let regex = try! NSRegularExpression(pattern: mediaTypePattern) + + func isValidContentType(_ contentType: String) -> Bool { + let range = NSRange(location: 0, length: contentType.utf16.count) + return regex.firstMatch(in: contentType, range: range) != nil + } + + for contentType in contentTypes { + guard isValidContentType(contentType) else { + throw Diagnostic.error( + message: + "Invalid content type string: '\(contentType)' must have 2 components separated by a slash '/'.\n" + ) + } + } +} /// Runs validation steps on the incoming OpenAPI document. /// - Parameters: @@ -30,6 +82,9 @@ 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. + let contentTypes = extractContentTypes(from: doc) + try validateContentTypes(contentTypes) + 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..5f9245c1 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift @@ -56,4 +56,86 @@ final class Test_validateDoc: Test_Core { XCTAssertThrowsError(try validateDoc(doc, config: .init(mode: .types, access: Config.defaultAccessModifier))) } + func testExtractContentTypes() throws { + let doc = OpenAPI.Document( + info: .init(title: "Test", version: "1.0.0"), + servers: [], + paths: [ + "/path1": .b( + OpenAPI.PathItem( + get: .init( + requestBody: .b(OpenAPI.Request(content: [.init(rawValue: "")!: .init(schema: .string)])), + responses: [ + .init(integerLiteral: 200): .b( + OpenAPI.Response( + description: "Test description 1", + content: [ + OpenAPI.ContentType(rawValue: "application/json")!: .init(schema: .string) + ] + ) + ) + ] + ) + ) + ), + "/path2": .b( + OpenAPI.PathItem( + get: .init( + requestBody: .b(OpenAPI.Request(content: [.init(rawValue: " ")!: .init(schema: .string)])), + responses: [ + .init(integerLiteral: 200): .b( + OpenAPI.Response( + description: "Test description 2", + content: [OpenAPI.ContentType(rawValue: "text/plain")!: .init(schema: .string)] + ) + ) + ] + ) + ) + ), + ], + components: .noComponents + ) + XCTAssertEqual(extractContentTypes(from: doc), ["", "application/json", " ", "text/plain"]) + } + + func testValidContentTypes() throws { + let validContentTypes = ["application/json", "text/html"] + XCTAssertNoThrow(try validateContentTypes(validContentTypes)) + } + + func testContentTypes_emptyArray() { XCTAssertNoThrow(try validateContentTypes([])) } + + func testInvalidContentTypes_spaceBetweenComponents() { + let invalidContentTypes = ["application/json", "text / html"] + XCTAssertThrowsError(try validateContentTypes(invalidContentTypes)) { error in + XCTAssertTrue(error is Diagnostic) + XCTAssertEqual( + error.localizedDescription, + "error: Invalid content type string: 'text / html' must have 2 components separated by a slash '/'.\n" + ) + } + } + + func testInvalidContentTypes_missingComponent() { + let invalidContentTypes = ["/json", "text/html"] + XCTAssertThrowsError(try validateContentTypes(invalidContentTypes)) { error in + XCTAssertTrue(error is Diagnostic) + XCTAssertEqual( + error.localizedDescription, + "error: Invalid content type string: '/json' must have 2 components separated by a slash '/'.\n" + ) + } + } + func testInvalidContentTypes_emptyComponent() { + let invalidContentTypes = ["application/json", ""] + XCTAssertThrowsError(try validateContentTypes(invalidContentTypes)) { error in + XCTAssertTrue(error is Diagnostic) + XCTAssertEqual( + error.localizedDescription, + "error: Invalid content type string: '' must have 2 components separated by a slash '/'.\n" + ) + } + } + } From f6433d5165c5f9970a4ea290607bc73d82e7243c Mon Sep 17 00:00:00 2001 From: PARAIPAN SORIN Date: Tue, 19 Dec 2023 00:20:43 +0200 Subject: [PATCH 4/6] Adjust regex pattern to allow - character for subtypes --- Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift | 2 +- Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift b/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift index e0357e00..1db0a023 100644 --- a/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift +++ b/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift @@ -48,7 +48,7 @@ func extractContentTypes(from doc: ParsedOpenAPIRepresentation) -> [String] { /// - Parameter contentTypes: An array of strings representing content types. /// - Throws: A Diagnostic error if any content type is invalid. func validateContentTypes(_ contentTypes: [String]) throws { - let mediaTypePattern = "^[a-zA-Z]+/[a-zA-Z]+$" + let mediaTypePattern = "^[a-zA-Z]+/[a-zA-Z][a-zA-Z-]*$" let regex = try! NSRegularExpression(pattern: mediaTypePattern) func isValidContentType(_ contentType: String) -> Bool { diff --git a/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift b/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift index 5f9245c1..92cd0a02 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift @@ -100,7 +100,7 @@ final class Test_validateDoc: Test_Core { } func testValidContentTypes() throws { - let validContentTypes = ["application/json", "text/html"] + let validContentTypes = ["application/json", "text/html", "application/x-www-form-urlencoded"] XCTAssertNoThrow(try validateContentTypes(validContentTypes)) } From 5011c6d269de289a7d317df3ca9d57cbc55d56f1 Mon Sep 17 00:00:00 2001 From: PARAIPAN SORIN Date: Wed, 20 Dec 2023 14:13:44 +0200 Subject: [PATCH 5/6] Add closure for validation of content types --- .../Parser/validateDoc.swift | 94 ++++---- .../Parser/Test_validateDoc.swift | 215 +++++++++++++++--- 2 files changed, 240 insertions(+), 69 deletions(-) diff --git a/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift b/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift index 1db0a023..61caa6ff 100644 --- a/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift +++ b/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift @@ -13,55 +13,68 @@ //===----------------------------------------------------------------------===// import OpenAPIKit -import Foundation -/// Extracts content types from a ParsedOpenAPIRepresentation. +/// Validates all content types from an OpenAPI document represented by a ParsedOpenAPIRepresentation. /// -/// - Parameter doc: The OpenAPI document representation. -/// - Returns: An array of strings representing content types extracted from requests and responses. -func extractContentTypes(from doc: ParsedOpenAPIRepresentation) -> [String] { - let contentTypes: [String] = doc.paths.values.flatMap { pathValue -> [OpenAPI.ContentType.RawValue] in - guard case .b(let pathItem) = pathValue else { return [] } +/// 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 { - let requestBodyContentTypes: [String] = pathItem.endpoints.map { $0.operation.requestBody } - .compactMap { (eitherRequest: Either, OpenAPI.Request>?) in - guard case .b(let actualRequest) = eitherRequest else { return nil } - return actualRequest.content.keys.first?.rawValue + 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: '\(contentType.rawValue)' found in requestBody at path '\(path.rawValue)'. Must have 2 components separated by a slash '/'.\n" + ) + } + } + } } - let responseContentTypes: [String] = pathItem.endpoints.map { $0.operation.responses.values } - .flatMap { (response: [Either, OpenAPI.Response>]) in - response.compactMap { (eitherResponse: Either, OpenAPI.Response>) in - guard case .b(let actualResponse) = eitherResponse else { return nil } - return actualResponse.content.keys.first?.rawValue + 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: '\(contentType.rawValue)' found in responses at path '\(path.rawValue)'. Must have 2 components separated by a slash '/'.\n" + ) + } + } } } - - return requestBodyContentTypes + responseContentTypes + } } - return contentTypes -} - -/// Validates an array of content types. -/// -/// - Parameter contentTypes: An array of strings representing content types. -/// - Throws: A Diagnostic error if any content type is invalid. -func validateContentTypes(_ contentTypes: [String]) throws { - let mediaTypePattern = "^[a-zA-Z]+/[a-zA-Z][a-zA-Z-]*$" - let regex = try! NSRegularExpression(pattern: mediaTypePattern) - - func isValidContentType(_ contentType: String) -> Bool { - let range = NSRange(location: 0, length: contentType.utf16.count) - return regex.firstMatch(in: contentType, range: range) != nil + for component in doc.components.requestBodies.values { + for contentType in component.content.keys { + if !validate(contentType.rawValue) { + throw Diagnostic.error( + message: + "Invalid content type string: '\(contentType.rawValue)' found in #/components/requestBodies. Must have 2 components separated by a slash '/'.\n" + ) + } + } } - for contentType in contentTypes { - guard isValidContentType(contentType) else { - throw Diagnostic.error( - message: - "Invalid content type string: '\(contentType)' must have 2 components separated by a slash '/'.\n" - ) + for component in doc.components.responses.values { + for contentType in component.content.keys { + if !validate(contentType.rawValue) { + throw Diagnostic.error( + message: + "Invalid content type string: '\(contentType.rawValue)' found in #/components/responses. Must have 2 components separated by a slash '/'.\n" + ) + } } } } @@ -82,8 +95,9 @@ 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. - let contentTypes = extractContentTypes(from: doc) - try validateContentTypes(contentTypes) + 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 diff --git a/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift b/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift index 92cd0a02..fc0cf0a3 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift @@ -56,22 +56,22 @@ final class Test_validateDoc: Test_Core { XCTAssertThrowsError(try validateDoc(doc, config: .init(mode: .types, access: Config.defaultAccessModifier))) } - func testExtractContentTypes() throws { + func testValidateContentTypes_validContentTypes() throws { let doc = OpenAPI.Document( info: .init(title: "Test", version: "1.0.0"), servers: [], paths: [ "/path1": .b( - OpenAPI.PathItem( + .init( get: .init( - requestBody: .b(OpenAPI.Request(content: [.init(rawValue: "")!: .init(schema: .string)])), + requestBody: .b( + .init(content: [.init(rawValue: "application/xml")!: .init(schema: .string)]) + ), responses: [ .init(integerLiteral: 200): .b( - OpenAPI.Response( + .init( description: "Test description 1", - content: [ - OpenAPI.ContentType(rawValue: "application/json")!: .init(schema: .string) - ] + content: [.init(rawValue: "application/json")!: .init(schema: .string)] ) ) ] @@ -79,14 +79,14 @@ final class Test_validateDoc: Test_Core { ) ), "/path2": .b( - OpenAPI.PathItem( + .init( get: .init( - requestBody: .b(OpenAPI.Request(content: [.init(rawValue: " ")!: .init(schema: .string)])), + requestBody: .b(.init(content: [.init(rawValue: "text/html")!: .init(schema: .string)])), responses: [ .init(integerLiteral: 200): .b( - OpenAPI.Response( + .init( description: "Test description 2", - content: [OpenAPI.ContentType(rawValue: "text/plain")!: .init(schema: .string)] + content: [.init(rawValue: "text/plain")!: .init(schema: .string)] ) ) ] @@ -96,44 +96,201 @@ final class Test_validateDoc: Test_Core { ], components: .noComponents ) - XCTAssertEqual(extractContentTypes(from: doc), ["", "application/json", " ", "text/plain"]) + XCTAssertNoThrow( + try validateContentTypes(in: doc) { contentType in + (try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil + } + ) } - func testValidContentTypes() throws { - let validContentTypes = ["application/json", "text/html", "application/x-www-form-urlencoded"] - XCTAssertNoThrow(try validateContentTypes(validContentTypes)) + 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: 'application/' found in requestBody at path '/path1'. Must have 2 components separated by a slash '/'.\n" + ) + } } - func testContentTypes_emptyArray() { XCTAssertNoThrow(try validateContentTypes([])) } - - func testInvalidContentTypes_spaceBetweenComponents() { - let invalidContentTypes = ["application/json", "text / html"] - XCTAssertThrowsError(try validateContentTypes(invalidContentTypes)) { error in + 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: 'text / html' must have 2 components separated by a slash '/'.\n" + "error: Invalid content type string: '/plain' found in responses at path '/path2'. Must have 2 components separated by a slash '/'.\n" ) } } - func testInvalidContentTypes_missingComponent() { - let invalidContentTypes = ["/json", "text/html"] - XCTAssertThrowsError(try validateContentTypes(invalidContentTypes)) { error in + 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: '/json' must have 2 components separated by a slash '/'.\n" + "error: Invalid content type string: 'image/' found in #/components/requestBodies. Must have 2 components separated by a slash '/'.\n" ) } } - func testInvalidContentTypes_emptyComponent() { - let invalidContentTypes = ["application/json", ""] - XCTAssertThrowsError(try validateContentTypes(invalidContentTypes)) { error in + + 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: '' must have 2 components separated by a slash '/'.\n" + "error: Invalid content type string: '' found in #/components/responses. Must have 2 components separated by a slash '/'.\n" ) } } From f66de7d110f011b0209b264bf9db72cac58ba658 Mon Sep 17 00:00:00 2001 From: PARAIPAN SORIN Date: Wed, 20 Dec 2023 15:55:39 +0200 Subject: [PATCH 6/6] Put dynamic data into context dictionary --- .../Parser/validateDoc.swift | 36 +++++++++++++------ .../Parser/Test_validateDoc.swift | 8 ++--- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift b/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift index 61caa6ff..ecc5ef82 100644 --- a/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift +++ b/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift @@ -33,8 +33,13 @@ func validateContentTypes(in doc: ParsedOpenAPIRepresentation, validate: (String for contentType in actualRequest.content.keys { if !validate(contentType.rawValue) { throw Diagnostic.error( - message: - "Invalid content type string: '\(contentType.rawValue)' found in requestBody at path '\(path.rawValue)'. Must have 2 components separated by a slash '/'.\n" + 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 '/'.", + ] ) } } @@ -46,8 +51,13 @@ func validateContentTypes(in doc: ParsedOpenAPIRepresentation, validate: (String for contentType in actualResponse.content.keys { if !validate(contentType.rawValue) { throw Diagnostic.error( - message: - "Invalid content type string: '\(contentType.rawValue)' found in responses at path '\(path.rawValue)'. Must have 2 components separated by a slash '/'.\n" + 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 '/'.", + ] ) } } @@ -56,23 +66,29 @@ func validateContentTypes(in doc: ParsedOpenAPIRepresentation, validate: (String } } - for component in doc.components.requestBodies.values { + 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: '\(contentType.rawValue)' found in #/components/requestBodies. Must have 2 components separated by a slash '/'.\n" + message: "Invalid content type string.", + context: [ + "contentType": contentType.rawValue, "location": "#/components/requestBodies/\(key.rawValue)", + "recoverySuggestion": "Must have 2 components separated by a slash '/'.", + ] ) } } } - for component in doc.components.responses.values { + 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: '\(contentType.rawValue)' found in #/components/responses. Must have 2 components separated by a slash '/'.\n" + message: "Invalid content type string.", + context: [ + "contentType": contentType.rawValue, "location": "#/components/responses/\(key.rawValue)", + "recoverySuggestion": "Must have 2 components separated by a slash '/'.", + ] ) } } diff --git a/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift b/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift index fc0cf0a3..e88f00ec 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift @@ -149,7 +149,7 @@ final class Test_validateDoc: Test_Core { XCTAssertTrue(error is Diagnostic) XCTAssertEqual( error.localizedDescription, - "error: Invalid content type string: 'application/' found in requestBody at path '/path1'. Must have 2 components separated by a slash '/'.\n" + "error: Invalid content type string. [context: contentType=application/, location=/path1/GET/requestBody, recoverySuggestion=Must have 2 components separated by a slash '/'.]" ) } } @@ -202,7 +202,7 @@ final class Test_validateDoc: Test_Core { XCTAssertTrue(error is Diagnostic) XCTAssertEqual( error.localizedDescription, - "error: Invalid content type string: '/plain' found in responses at path '/path2'. Must have 2 components separated by a slash '/'.\n" + "error: Invalid content type string. [context: contentType=/plain, location=/path2/GET/responses, recoverySuggestion=Must have 2 components separated by a slash '/'.]" ) } } @@ -243,7 +243,7 @@ final class Test_validateDoc: Test_Core { XCTAssertTrue(error is Diagnostic) XCTAssertEqual( error.localizedDescription, - "error: Invalid content type string: 'image/' found in #/components/requestBodies. Must have 2 components separated by a slash '/'.\n" + "error: Invalid content type string. [context: contentType=image/, location=#/components/requestBodies/exampleRequestBody2, recoverySuggestion=Must have 2 components separated by a slash '/'.]" ) } } @@ -290,7 +290,7 @@ final class Test_validateDoc: Test_Core { XCTAssertTrue(error is Diagnostic) XCTAssertEqual( error.localizedDescription, - "error: Invalid content type string: '' found in #/components/responses. Must have 2 components separated by a slash '/'.\n" + "error: Invalid content type string. [context: contentType=, location=#/components/responses/exampleRequestBody2, recoverySuggestion=Must have 2 components separated by a slash '/'.]" ) } }