From f58f60d07be55142561571c0526b79ca2700b4a8 Mon Sep 17 00:00:00 2001 From: PARAIPAN SORIN <51127880+PARAIPAN9@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:21:01 +0200 Subject: [PATCH] Validate references in validateDoc (#500) ### Motivation - Fixes #341. ### Modifications - Develop a function responsible for iterating through and validating the references within the OpenAPI document. If external references are present or if they are not found in the 'components' section, the function should throw errors. ### Result - This will help catch invalid references before code generation. ### Test Plan - Make sure that tests pass and wrote additional unit tests. --- .../Parser/validateDoc.swift | 172 +++++++++++++++++- .../Parser/Test_validateDoc.swift | 139 ++++++++++++++ 2 files changed, 304 insertions(+), 7 deletions(-) diff --git a/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift b/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift index ecc5ef82..416704ad 100644 --- a/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift +++ b/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift @@ -25,11 +25,11 @@ import OpenAPIKit /// - 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 } + guard let pathItem = pathValue.pathItemValue else { continue } for endpoint in pathItem.endpoints { if let eitherRequest = endpoint.operation.requestBody { - if case .b(let actualRequest) = eitherRequest { + if let actualRequest = eitherRequest.requestValue { for contentType in actualRequest.content.keys { if !validate(contentType.rawValue) { throw Diagnostic.error( @@ -47,7 +47,7 @@ func validateContentTypes(in doc: ParsedOpenAPIRepresentation, validate: (String } for eitherResponse in endpoint.operation.responses.values { - if case .b(let actualResponse) = eitherResponse { + if let actualResponse = eitherResponse.responseValue { for contentType in actualResponse.content.keys { if !validate(contentType.rawValue) { throw Diagnostic.error( @@ -95,6 +95,163 @@ func validateContentTypes(in doc: ParsedOpenAPIRepresentation, validate: (String } } +/// Validates all references from an OpenAPI document represented by a ParsedOpenAPIRepresentation against its components. +/// +/// This method traverses the OpenAPI document to ensure that all references +/// within the document are valid and point to existing components. +/// +/// - Parameter doc: The OpenAPI document to validate. +/// - Throws: `Diagnostic.error` if an external reference is found or a reference is not found in components. +func validateReferences(in doc: ParsedOpenAPIRepresentation) throws { + func validateReference( + _ reference: OpenAPI.Reference, + in components: OpenAPI.Components, + location: String + ) throws { + if reference.isExternal { + throw Diagnostic.error( + message: "External references are not suppported.", + context: ["reference": reference.absoluteString, "location": location] + ) + } + if components[reference] == nil { + throw Diagnostic.error( + message: "Reference not found in components.", + context: ["reference": reference.absoluteString, "location": location] + ) + } + } + + func validateReferencesInContentTypes(_ content: OpenAPI.Content.Map, location: String) throws { + for (contentKey, contentType) in content { + if let reference = contentType.schema?.reference { + try validateReference( + reference, + in: doc.components, + location: location + "/content/\(contentKey.rawValue)/schema" + ) + } + if let eitherExamples = contentType.examples?.values { + for example in eitherExamples { + if let reference = example.reference { + try validateReference( + reference, + in: doc.components, + location: location + "/content/\(contentKey.rawValue)/examples" + ) + } + } + } + } + } + + for (key, value) in doc.webhooks { + if let reference = value.reference { try validateReference(reference, in: doc.components, location: key) } + } + + for (path, pathValue) in doc.paths { + if let reference = pathValue.reference { + try validateReference(reference, in: doc.components, location: path.rawValue) + } else if let pathItem = pathValue.pathItemValue { + + for endpoint in pathItem.endpoints { + for (endpointKey, endpointValue) in endpoint.operation.callbacks { + if let reference = endpointValue.reference { + try validateReference( + reference, + in: doc.components, + location: "\(path.rawValue)/\(endpoint.method.rawValue)/callbacks/\(endpointKey)" + ) + } + } + + for eitherParameter in endpoint.operation.parameters { + if let reference = eitherParameter.reference { + try validateReference( + reference, + in: doc.components, + location: "\(path.rawValue)/\(endpoint.method.rawValue)/parameters" + ) + } else if let parameter = eitherParameter.parameterValue { + if let reference = parameter.schemaOrContent.schemaReference { + try validateReference( + reference, + in: doc.components, + location: "\(path.rawValue)/\(endpoint.method.rawValue)/parameters/\(parameter.name)" + ) + } else if let content = parameter.schemaOrContent.contentValue { + try validateReferencesInContentTypes( + content, + location: "\(path.rawValue)/\(endpoint.method.rawValue)/parameters/\(parameter.name)" + ) + } + } + } + if let reference = endpoint.operation.requestBody?.reference { + try validateReference( + reference, + in: doc.components, + location: "\(path.rawValue)/\(endpoint.method.rawValue)/requestBody" + ) + } else if let requestBodyValue = endpoint.operation.requestBody?.requestValue { + try validateReferencesInContentTypes( + requestBodyValue.content, + location: "\(path.rawValue)/\(endpoint.method.rawValue)/requestBody" + ) + } + + for (statusCode, eitherResponse) in endpoint.operation.responses { + if let reference = eitherResponse.reference { + try validateReference( + reference, + in: doc.components, + location: "\(path.rawValue)/\(endpoint.method.rawValue)/responses/\(statusCode.rawValue)" + ) + } else if let responseValue = eitherResponse.responseValue { + try validateReferencesInContentTypes( + responseValue.content, + location: "\(path.rawValue)/\(endpoint.method.rawValue)/responses/\(statusCode.rawValue)" + ) + } + if let headers = eitherResponse.responseValue?.headers { + for (headerKey, eitherHeader) in headers { + if let reference = eitherHeader.reference { + try validateReference( + reference, + in: doc.components, + location: + "\(path.rawValue)/\(endpoint.method.rawValue)/responses/\(statusCode.rawValue)/headers/\(headerKey)" + ) + } else if let headerValue = eitherHeader.headerValue { + if let schemaReference = headerValue.schemaOrContent.schemaReference { + try validateReference( + schemaReference, + in: doc.components, + location: + "\(path.rawValue)/\(endpoint.method.rawValue)/responses/\(statusCode.rawValue)/headers/\(headerKey)" + ) + } else if let contentValue = headerValue.schemaOrContent.contentValue { + try validateReferencesInContentTypes( + contentValue, + location: + "\(path.rawValue)/\(endpoint.method.rawValue)/responses/\(statusCode.rawValue)/headers/\(headerKey)" + ) + } + } + } + } + } + } + + for eitherParameter in pathItem.parameters { + if let reference = eitherParameter.reference { + try validateReference(reference, in: doc.components, location: "\(path.rawValue)/parameters") + } + } + } + } +} + /// Runs validation steps on the incoming OpenAPI document. /// - Parameters: /// - doc: The OpenAPI document to validate. @@ -102,6 +259,11 @@ func validateContentTypes(in doc: ParsedOpenAPIRepresentation, validate: (String /// - Returns: An array of diagnostic messages representing validation warnings. /// - Throws: An error if a fatal issue is found. func validateDoc(_ doc: ParsedOpenAPIRepresentation, config: Config) throws -> [Diagnostic] { + try validateReferences(in: doc) + try validateContentTypes(in: doc) { contentType in + (try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil + } + // Run OpenAPIKit's built-in validation. // Pass `false` to `strict`, however, because we don't // want to turn schema loading warnings into errors. @@ -111,10 +273,6 @@ 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 e88f00ec..c1e92cd3 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift @@ -295,4 +295,143 @@ final class Test_validateDoc: Test_Core { } } + func testValidateReferences_validReferences() throws { + let doc = OpenAPI.Document( + info: .init(title: "Test", version: "1.0.0"), + servers: [], + paths: [ + "/path1": .b( + .init( + get: .init( + parameters: .init( + arrayLiteral: .b( + .init( + name: "ID", + context: .path, + content: [ + .init(rawValue: "text/plain")!: .init( + schema: .a(.component(named: "Path1ParametersContentSchemaReference")) + ) + ] + ) + ), + .init(.component(named: "Path1ParametersReference")) + ), + requestBody: .reference(.component(named: "RequestBodyReference")), + responses: [ + .init(integerLiteral: 200): .reference(.component(named: "ResponsesReference")), + .init(integerLiteral: 202): .response( + .init( + description: "ResponseDescription", + content: [ + .init(rawValue: "text/plain")!: .init( + schema: .a(.component(named: "ResponsesContentSchemaReference")) + ) + ] + ) + ), + .init(integerLiteral: 204): .response( + description: "Response Description", + headers: ["Header": .a(.component(named: "ResponsesHeaderReference"))] + ), + ] + ) + ) + ), "/path2": .a(.component(named: "Path2Reference")), + "/path3": .b( + .init( + get: .init( + parameters: .init(arrayLiteral: .a(.component(named: "Path3ExampleID"))), + requestBody: .b( + .init(content: [ + .init(rawValue: "text/html")!: .init( + schema: .a(.component(named: "RequestBodyContentSchemaReference")) + ) + ]) + ), + responses: [:], + callbacks: [.init("Callback"): .a(.component(named: "CallbackReference"))] + ) + ) + ), + ], + components: .init( + schemas: [ + "ResponsesContentSchemaReference": .init(schema: .string(.init(), .init())), + "RequestBodyContentSchemaReference": .init(schema: .integer(.init(), .init())), + "Path1ParametersContentSchemaReference": .init(schema: .string(.init(), .init())), + ], + responses: ["ResponsesReference": .init(description: "Description")], + parameters: [ + "Path3ExampleID": .init(name: "ID", context: .path, content: .init()), + "Path1ParametersReference": .init(name: "Schema", context: .path, schema: .array), + ], + requestBodies: [ + "RequestBodyReference": .init(content: .init()) + + ], + headers: ["ResponsesHeaderReference": .init(schema: .array)], + callbacks: ["CallbackReference": .init()], + pathItems: ["Path2Reference": .init()] + ) + ) + XCTAssertNoThrow(try validateReferences(in: doc)) + } + + func testValidateReferences_referenceNotFoundInComponents() throws { + let doc = OpenAPI.Document( + info: .init(title: "Test", version: "1.0.0"), + servers: [], + paths: [ + "/path": .b( + .init( + get: .init( + requestBody: .b( + .init(content: [ + .init(rawValue: "text/html")!: .init( + schema: .a(.component(named: "RequestBodyContentSchemaReference")) + ) + ]) + ), + responses: [:] + ) + ) + ) + ], + components: .init(schemas: ["RequestBodyContentSchema": .init(schema: .integer(.init(), .init()))]) + ) + XCTAssertThrowsError(try validateReferences(in: doc)) { error in + XCTAssertTrue(error is Diagnostic) + XCTAssertEqual( + error.localizedDescription, + "error: Reference not found in components. [context: location=/path/GET/requestBody/content/text/html/schema, reference=#/components/schemas/RequestBodyContentSchemaReference]" + ) + } + } + + func testValidateReferences_foundExternalReference() throws { + let doc = OpenAPI.Document( + info: .init(title: "Test", version: "1.0.0"), + servers: [], + paths: [ + "/path": .b( + .init( + get: .init( + requestBody: .b(.init(content: .init())), + responses: [.init(integerLiteral: 200): .reference(.external(URL(string: "ExternalURL")!))] + ) + ) + ) + ], + components: .noComponents + ) + XCTAssertThrowsError(try validateReferences(in: doc)) { error in + XCTAssertTrue(error is Diagnostic) + XCTAssertEqual( + error.localizedDescription, + "error: External references are not suppported. [context: location=/path/GET/responses/200, reference=ExternalURL]" + ) + } + } + }