Skip to content

Commit 0d88cf4

Browse files
committed
validate content types in the same OpenAPIKit validation pass as other validations
1 parent 19968ec commit 0d88cf4

2 files changed

Lines changed: 42 additions & 109 deletions

File tree

Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift

Lines changed: 10 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -16,85 +16,13 @@ import OpenAPIKit
1616

1717
/// Validates all content types from an OpenAPI document represented by a ParsedOpenAPIRepresentation.
1818
///
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-
///
2219
/// - Parameters:
23-
/// - doc: The OpenAPI document representation.
2420
/// - 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-
}
21+
func validateContentTypes(_ validate: @escaping (String) -> Bool) -> Validation<OpenAPI.ContentType> {
22+
return .init(
23+
description: "Content type is of form '<type>/<subtype>'.",
24+
check: take(\.rawValue, check: validate)
25+
)
9826
}
9927

10028
/// Validates all type overrides from a Config are present in the components of a ParsedOpenAPIRepresentation.
@@ -126,14 +54,12 @@ func validateTypeOverrides(_ doc: ParsedOpenAPIRepresentation, config: Config) -
12654
/// - Returns: An array of diagnostic messages representing validation warnings.
12755
/// - Throws: An error if a fatal issue is found.
12856
func validateDoc(_ doc: ParsedOpenAPIRepresentation, config: Config) throws -> [Diagnostic] {
129-
try validateContentTypes(in: doc) { contentType in
130-
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
131-
}
13257
let typeOverrideDiagnostics = validateTypeOverrides(doc, config: config)
13358

13459
// Run OpenAPIKit's default built-in validations and additionally check
135-
// that all references point to entries in the Components Object and all
136-
// operations contain responses.
60+
// that all references point to entries in the Components Object, all
61+
// operations contain responses, and all content types parse by this
62+
// library's code.
13763
//
13864
// Pass `false` to `strict`, however, because we don't
13965
// want to turn schema loading warnings into errors.
@@ -143,9 +69,11 @@ func validateDoc(_ doc: ParsedOpenAPIRepresentation, config: Config) throws -> [
14369
// block the generator from running.
14470
// Validation errors continue to be fatal, such as
14571
// structural issues, non-unique operationIds, etc.
72+
let contentTypesValidation = validateContentTypes() { contentType in (try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil }
14673
let validator = Validator()
14774
.validatingAllReferencesFoundInComponents()
14875
.validating(.operationsContainResponses)
76+
.validating(contentTypesValidation)
14977
let warnings = try doc.validate(using: validator, strict: false)
15078
let diagnostics: [Diagnostic] = warnings.map { warning in
15179
.warning(

Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,11 @@ final class Test_validateDoc: Test_Core {
112112
],
113113
components: .noComponents
114114
)
115+
let validator = Validator.blank.validating(validateContentTypes() { contentType in
116+
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
117+
})
115118
XCTAssertNoThrow(
116-
try validateContentTypes(in: doc) { contentType in
117-
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
118-
}
119+
try doc.validate(using: validator, strict: false)
119120
)
120121
}
121122

@@ -157,15 +158,16 @@ final class Test_validateDoc: Test_Core {
157158
],
158159
components: .noComponents
159160
)
161+
let validator = Validator.blank.validating(validateContentTypes() { contentType in
162+
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
163+
})
160164
XCTAssertThrowsError(
161-
try validateContentTypes(in: doc) { contentType in
162-
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
163-
}
165+
try doc.validate(using: validator, strict: false)
164166
) { error in
165-
XCTAssertTrue(error is Diagnostic)
167+
XCTAssertTrue(error is ValidationErrorCollection)
166168
XCTAssertEqual(
167-
error.localizedDescription,
168-
"error: Invalid content type string. [context: contentType=application/, location=/path1/GET/requestBody, recoverySuggestion=Must have 2 components separated by a slash '<type>/<subtype>'.]"
169+
OpenAPI.Error(from: error).localizedDescription,
170+
"Failed to satisfy: Content type is of form '<type>/<subtype>' at path: .paths['/path1'].get.requestBody.content['application/']"
169171
)
170172
}
171173
}
@@ -210,15 +212,16 @@ final class Test_validateDoc: Test_Core {
210212
],
211213
components: .noComponents
212214
)
215+
let validator = Validator.blank.validating(validateContentTypes() { contentType in
216+
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
217+
})
213218
XCTAssertThrowsError(
214-
try validateContentTypes(in: doc) { contentType in
215-
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
216-
}
219+
try doc.validate(using: validator, strict: true)
217220
) { error in
218-
XCTAssertTrue(error is Diagnostic)
221+
XCTAssertTrue(error is ValidationErrorCollection)
219222
XCTAssertEqual(
220-
error.localizedDescription,
221-
"error: Invalid content type string. [context: contentType=/plain, location=/path2/GET/responses, recoverySuggestion=Must have 2 components separated by a slash '<type>/<subtype>'.]"
223+
OpenAPI.Error(from: error).localizedDescription,
224+
"Failed to satisfy: Content type is of form '<type>/<subtype>' at path: .paths['/path2'].get.responses.200.content['/plain']"
222225
)
223226
}
224227
}
@@ -251,15 +254,16 @@ final class Test_validateDoc: Test_Core {
251254
"exampleRequestBody2": .init(content: [.init(rawValue: "image/")!: .content(.init(schema: .string))]),
252255
])
253256
)
257+
let validator = Validator.blank.validating(validateContentTypes() { contentType in
258+
return (try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
259+
})
254260
XCTAssertThrowsError(
255-
try validateContentTypes(in: doc) { contentType in
256-
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
257-
}
261+
try doc.validate(using: validator, strict: false)
258262
) { error in
259-
XCTAssertTrue(error is Diagnostic)
263+
XCTAssertTrue(error is ValidationErrorCollection)
260264
XCTAssertEqual(
261-
error.localizedDescription,
262-
"error: Invalid content type string. [context: contentType=image/, location=#/components/requestBodies/exampleRequestBody2, recoverySuggestion=Must have 2 components separated by a slash '<type>/<subtype>'.]"
265+
OpenAPI.Error(from: error).localizedDescription,
266+
"Failed to satisfy: Content type is of form '<type>/<subtype>' at path: .components.requestBodies.exampleRequestBody2.content['image/']"
263267
)
264268
}
265269
}
@@ -298,15 +302,16 @@ final class Test_validateDoc: Test_Core {
298302
),
299303
])
300304
)
305+
let validator = Validator.blank.validating(validateContentTypes() { contentType in
306+
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
307+
})
301308
XCTAssertThrowsError(
302-
try validateContentTypes(in: doc) { contentType in
303-
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
304-
}
309+
try doc.validate(using: validator, strict: false)
305310
) { error in
306-
XCTAssertTrue(error is Diagnostic)
311+
XCTAssertTrue(error is ValidationErrorCollection)
307312
XCTAssertEqual(
308-
error.localizedDescription,
309-
"error: Invalid content type string. [context: contentType=, location=#/components/responses/exampleRequestBody2, recoverySuggestion=Must have 2 components separated by a slash '<type>/<subtype>'.]"
313+
OpenAPI.Error(from: error).localizedDescription,
314+
"Failed to satisfy: Content type is of form '<type>/<subtype>' at path: .components.responses.exampleRequestBody2.content."
310315
)
311316
}
312317
}

0 commit comments

Comments
 (0)