Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
262 changes: 13 additions & 249 deletions Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,249 +14,12 @@

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 let pathItem = pathValue.pathItemValue else { continue }
for endpoint in pathItem.endpoints {

if let eitherRequest = endpoint.operation.requestBody {
if let actualRequest = eitherRequest.requestValue {
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 let actualResponse = eitherResponse.responseValue {
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 {
let component = try doc.components.assumeLookupOnce(component)
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 {
let component = try doc.components.assumeLookupOnce(component)
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>'.",
]
)
}
}
}
}

/// 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 {
switch contentType {
case .a(let ref):
try validateReference(ref, in: doc.components, location: location + "/content/\(contentKey.rawValue)")
case .b(let contentType):
if let reference: JSONReference<JSONSchema> = contentType.schema?.reference {
try validateReference(
.init(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")
}
}
}
}
/// Validates all content types from an OpenAPI document can be parsed as a ContentType.
var contentTypesValidation: Validation<OpenAPI.ContentType> {
.init(
description: "Content type is of form '<type>/<subtype>'.",
check: { context in (try? _OpenAPIGeneratorCore.ContentType(string: context.subject.rawValue)) != nil }
)
}

/// Validates all type overrides from a Config are present in the components of a ParsedOpenAPIRepresentation.
Expand Down Expand Up @@ -288,13 +51,13 @@ func validateTypeOverrides(_ doc: ParsedOpenAPIRepresentation, config: 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
}
let typeOverrideDiagnostics = validateTypeOverrides(doc, config: config)

// Run OpenAPIKit's built-in validation.
// Run OpenAPIKit's default built-in validations and additionally check
// that all references point to entries in the Components Object, all
// operations contain responses, and all content types parse by this
// library's code.
//
// Pass `false` to `strict`, however, because we don't
// want to turn schema loading warnings into errors.
// We already propagate the warnings to the generator's
Expand All @@ -318,7 +81,8 @@ func validateDoc(_ doc: ParsedOpenAPIRepresentation, config: Config) throws -> [

extension OpenAPIKit.Validator {
static var swiftOpenAPICustomValidator: Validator {
Validator().validating(\.operationsContainResponses)
Validator().validatingAllReferencesFoundInComponents().validating(\.operationsContainResponses)
.validating(contentTypesValidation)
// Skip this one to be backwards compatible with previous versions of Swift OpenAPI Generator.
// Even when run with strict=false, this one will cause OpenAPIKit to throw an error. Previous verions were more
// lenient and Swift OpenAPI Generator would later emit a warning that it's unsupported.
Expand Down
Loading