Skip to content

Commit 92deec8

Browse files
authored
Use OpenAPIKit for more validation logic (#880)
### Motivation OpenAPIKit provides some validation help in its latest versions that can be used here to reduce the amount of logic this project must maintain. For example, the generator need not crawl the document to validate all content types, this is the behavior of a `Validation<OpenAPIKit.ContentType>`. ### Modifications - I've removed the reference validations entirely because they are now available in exactly the desired form upstream. - I replaced the existing implementation of `ContentType` check with one using OpenAPIKit's validation DSL. This mostly serves to avoid the need to maintain logic in this library that OpenAPIKit must maintain anyway (namely, there's no need to find the `ContentType` values to validate them, OpenAPIKit will validate all values of that type anywhere in the Document). ### Result Removed code, slightly different error messages. ### Test Plan Tests have been updated to reflect the new (though comparable) errors.
1 parent 8725c22 commit 92deec8

2 files changed

Lines changed: 84 additions & 307 deletions

File tree

Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift

Lines changed: 13 additions & 249 deletions
Original file line numberDiff line numberDiff line change
@@ -14,249 +14,12 @@
1414

1515
import OpenAPIKit
1616

17-
/// Validates all content types from an OpenAPI document represented by a ParsedOpenAPIRepresentation.
18-
///
19-
/// This function iterates through the paths, endpoints, and components of the OpenAPI document,
20-
/// checking and reporting any invalid content types using the provided validation closure.
21-
///
22-
/// - Parameters:
23-
/// - doc: The OpenAPI document representation.
24-
/// - validate: A closure to validate each content type.
25-
/// - Throws: An error with diagnostic information if any invalid content types are found.
26-
func validateContentTypes(in doc: ParsedOpenAPIRepresentation, validate: (String) -> Bool) throws {
27-
for (path, pathValue) in doc.paths {
28-
guard let pathItem = pathValue.pathItemValue else { continue }
29-
for endpoint in pathItem.endpoints {
30-
31-
if let eitherRequest = endpoint.operation.requestBody {
32-
if let actualRequest = eitherRequest.requestValue {
33-
for contentType in actualRequest.content.keys {
34-
if !validate(contentType.rawValue) {
35-
throw Diagnostic.error(
36-
message: "Invalid content type string.",
37-
context: [
38-
"contentType": contentType.rawValue,
39-
"location": "\(path.rawValue)/\(endpoint.method.rawValue)/requestBody",
40-
"recoverySuggestion":
41-
"Must have 2 components separated by a slash '<type>/<subtype>'.",
42-
]
43-
)
44-
}
45-
}
46-
}
47-
}
48-
49-
for eitherResponse in endpoint.operation.responses.values {
50-
if let actualResponse = eitherResponse.responseValue {
51-
for contentType in actualResponse.content.keys {
52-
if !validate(contentType.rawValue) {
53-
throw Diagnostic.error(
54-
message: "Invalid content type string.",
55-
context: [
56-
"contentType": contentType.rawValue,
57-
"location": "\(path.rawValue)/\(endpoint.method.rawValue)/responses",
58-
"recoverySuggestion":
59-
"Must have 2 components separated by a slash '<type>/<subtype>'.",
60-
]
61-
)
62-
}
63-
}
64-
}
65-
}
66-
}
67-
}
68-
69-
for (key, component) in doc.components.requestBodies {
70-
let component = try doc.components.assumeLookupOnce(component)
71-
for contentType in component.content.keys {
72-
if !validate(contentType.rawValue) {
73-
throw Diagnostic.error(
74-
message: "Invalid content type string.",
75-
context: [
76-
"contentType": contentType.rawValue, "location": "#/components/requestBodies/\(key.rawValue)",
77-
"recoverySuggestion": "Must have 2 components separated by a slash '<type>/<subtype>'.",
78-
]
79-
)
80-
}
81-
}
82-
}
83-
84-
for (key, component) in doc.components.responses {
85-
let component = try doc.components.assumeLookupOnce(component)
86-
for contentType in component.content.keys {
87-
if !validate(contentType.rawValue) {
88-
throw Diagnostic.error(
89-
message: "Invalid content type string.",
90-
context: [
91-
"contentType": contentType.rawValue, "location": "#/components/responses/\(key.rawValue)",
92-
"recoverySuggestion": "Must have 2 components separated by a slash '<type>/<subtype>'.",
93-
]
94-
)
95-
}
96-
}
97-
}
98-
}
99-
100-
/// Validates all references from an OpenAPI document represented by a ParsedOpenAPIRepresentation against its components.
101-
///
102-
/// This method traverses the OpenAPI document to ensure that all references
103-
/// within the document are valid and point to existing components.
104-
///
105-
/// - Parameter doc: The OpenAPI document to validate.
106-
/// - Throws: `Diagnostic.error` if an external reference is found or a reference is not found in components.
107-
func validateReferences(in doc: ParsedOpenAPIRepresentation) throws {
108-
func validateReference<ReferenceType: ComponentDictionaryLocatable>(
109-
_ reference: OpenAPI.Reference<ReferenceType>,
110-
in components: OpenAPI.Components,
111-
location: String
112-
) throws {
113-
if reference.isExternal {
114-
throw Diagnostic.error(
115-
message: "External references are not suppported.",
116-
context: ["reference": reference.absoluteString, "location": location]
117-
)
118-
}
119-
if components[reference] == nil {
120-
throw Diagnostic.error(
121-
message: "Reference not found in components.",
122-
context: ["reference": reference.absoluteString, "location": location]
123-
)
124-
}
125-
}
126-
127-
func validateReferencesInContentTypes(_ content: OpenAPI.Content.Map, location: String) throws {
128-
for (contentKey, contentType) in content {
129-
switch contentType {
130-
case .a(let ref):
131-
try validateReference(ref, in: doc.components, location: location + "/content/\(contentKey.rawValue)")
132-
case .b(let contentType):
133-
if let reference: JSONReference<JSONSchema> = contentType.schema?.reference {
134-
try validateReference(
135-
.init(reference),
136-
in: doc.components,
137-
location: location + "/content/\(contentKey.rawValue)/schema"
138-
)
139-
}
140-
if let eitherExamples = contentType.examples?.values {
141-
for example in eitherExamples {
142-
if let reference = example.reference {
143-
try validateReference(
144-
reference,
145-
in: doc.components,
146-
location: location + "/content/\(contentKey.rawValue)/examples"
147-
)
148-
}
149-
}
150-
}
151-
}
152-
}
153-
}
154-
155-
for (key, value) in doc.webhooks {
156-
if let reference = value.reference { try validateReference(reference, in: doc.components, location: key) }
157-
}
158-
159-
for (path, pathValue) in doc.paths {
160-
if let reference = pathValue.reference {
161-
try validateReference(reference, in: doc.components, location: path.rawValue)
162-
} else if let pathItem = pathValue.pathItemValue {
163-
164-
for endpoint in pathItem.endpoints {
165-
for (endpointKey, endpointValue) in endpoint.operation.callbacks {
166-
if let reference = endpointValue.reference {
167-
try validateReference(
168-
reference,
169-
in: doc.components,
170-
location: "\(path.rawValue)/\(endpoint.method.rawValue)/callbacks/\(endpointKey)"
171-
)
172-
}
173-
}
174-
175-
for eitherParameter in endpoint.operation.parameters {
176-
if let reference = eitherParameter.reference {
177-
try validateReference(
178-
reference,
179-
in: doc.components,
180-
location: "\(path.rawValue)/\(endpoint.method.rawValue)/parameters"
181-
)
182-
} else if let parameter = eitherParameter.parameterValue {
183-
if let reference = parameter.schemaOrContent.schemaReference {
184-
try validateReference(
185-
reference,
186-
in: doc.components,
187-
location: "\(path.rawValue)/\(endpoint.method.rawValue)/parameters/\(parameter.name)"
188-
)
189-
} else if let content = parameter.schemaOrContent.contentValue {
190-
try validateReferencesInContentTypes(
191-
content,
192-
location: "\(path.rawValue)/\(endpoint.method.rawValue)/parameters/\(parameter.name)"
193-
)
194-
}
195-
}
196-
}
197-
if let reference = endpoint.operation.requestBody?.reference {
198-
try validateReference(
199-
reference,
200-
in: doc.components,
201-
location: "\(path.rawValue)/\(endpoint.method.rawValue)/requestBody"
202-
)
203-
} else if let requestBodyValue = endpoint.operation.requestBody?.requestValue {
204-
try validateReferencesInContentTypes(
205-
requestBodyValue.content,
206-
location: "\(path.rawValue)/\(endpoint.method.rawValue)/requestBody"
207-
)
208-
}
209-
210-
for (statusCode, eitherResponse) in endpoint.operation.responses {
211-
if let reference = eitherResponse.reference {
212-
try validateReference(
213-
reference,
214-
in: doc.components,
215-
location: "\(path.rawValue)/\(endpoint.method.rawValue)/responses/\(statusCode.rawValue)"
216-
)
217-
} else if let responseValue = eitherResponse.responseValue {
218-
try validateReferencesInContentTypes(
219-
responseValue.content,
220-
location: "\(path.rawValue)/\(endpoint.method.rawValue)/responses/\(statusCode.rawValue)"
221-
)
222-
}
223-
if let headers = eitherResponse.responseValue?.headers {
224-
for (headerKey, eitherHeader) in headers {
225-
if let reference = eitherHeader.reference {
226-
try validateReference(
227-
reference,
228-
in: doc.components,
229-
location:
230-
"\(path.rawValue)/\(endpoint.method.rawValue)/responses/\(statusCode.rawValue)/headers/\(headerKey)"
231-
)
232-
} else if let headerValue = eitherHeader.headerValue {
233-
if let schemaReference = headerValue.schemaOrContent.schemaReference {
234-
try validateReference(
235-
schemaReference,
236-
in: doc.components,
237-
location:
238-
"\(path.rawValue)/\(endpoint.method.rawValue)/responses/\(statusCode.rawValue)/headers/\(headerKey)"
239-
)
240-
} else if let contentValue = headerValue.schemaOrContent.contentValue {
241-
try validateReferencesInContentTypes(
242-
contentValue,
243-
location:
244-
"\(path.rawValue)/\(endpoint.method.rawValue)/responses/\(statusCode.rawValue)/headers/\(headerKey)"
245-
)
246-
}
247-
}
248-
}
249-
}
250-
}
251-
}
252-
253-
for eitherParameter in pathItem.parameters {
254-
if let reference = eitherParameter.reference {
255-
try validateReference(reference, in: doc.components, location: "\(path.rawValue)/parameters")
256-
}
257-
}
258-
}
259-
}
17+
/// Validates all content types from an OpenAPI document can be parsed as a ContentType.
18+
var contentTypesValidation: Validation<OpenAPI.ContentType> {
19+
.init(
20+
description: "Content type is of form '<type>/<subtype>'.",
21+
check: { context in (try? _OpenAPIGeneratorCore.ContentType(string: context.subject.rawValue)) != nil }
22+
)
26023
}
26124

26225
/// Validates all type overrides from a Config are present in the components of a ParsedOpenAPIRepresentation.
@@ -288,13 +51,13 @@ func validateTypeOverrides(_ doc: ParsedOpenAPIRepresentation, config: Config) -
28851
/// - Returns: An array of diagnostic messages representing validation warnings.
28952
/// - Throws: An error if a fatal issue is found.
29053
func validateDoc(_ doc: ParsedOpenAPIRepresentation, config: Config) throws -> [Diagnostic] {
291-
try validateReferences(in: doc)
292-
try validateContentTypes(in: doc) { contentType in
293-
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
294-
}
29554
let typeOverrideDiagnostics = validateTypeOverrides(doc, config: config)
29655

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

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

0 commit comments

Comments
 (0)