Skip to content

Commit

Permalink
Validate references in validateDoc (#500)
Browse files Browse the repository at this point in the history
### 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.
  • Loading branch information
PARAIPAN9 authored Jan 8, 2024
1 parent 85fec92 commit f58f60d
Show file tree
Hide file tree
Showing 2 changed files with 304 additions and 7 deletions.
172 changes: 165 additions & 7 deletions Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -95,13 +95,175 @@ 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<ReferenceType: ComponentDictionaryLocatable>(
_ reference: OpenAPI.Reference<ReferenceType>,
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.
/// - config: The generator config.
/// - 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.
Expand All @@ -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(
Expand Down
139 changes: 139 additions & 0 deletions Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]"
)
}
}

}

0 comments on commit f58f60d

Please sign in to comment.