Skip to content

Commit 85fec92

Browse files
PARAIPAN9czechboy0
andauthored
Validate content type strings in validateDoc (#471)
### Motivation - Fixes #342. ### Modifications - Create extracting and validation functions for content type strings. ### Result - This will help catch invalid content types before code generation. ### Test Plan - Test it on an openapi document to make sure the errors are thrown as intended and wrote additional unit tests. --------- Co-authored-by: Honza Dvorsky <[email protected]>
1 parent 76994bf commit 85fec92

File tree

2 files changed

+324
-0
lines changed

2 files changed

+324
-0
lines changed

Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,87 @@
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 case .b(let pathItem) = pathValue else { continue }
29+
for endpoint in pathItem.endpoints {
30+
31+
if let eitherRequest = endpoint.operation.requestBody {
32+
if case .b(let actualRequest) = eitherRequest {
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 case .b(let actualResponse) = eitherResponse {
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+
for contentType in component.content.keys {
71+
if !validate(contentType.rawValue) {
72+
throw Diagnostic.error(
73+
message: "Invalid content type string.",
74+
context: [
75+
"contentType": contentType.rawValue, "location": "#/components/requestBodies/\(key.rawValue)",
76+
"recoverySuggestion": "Must have 2 components separated by a slash '<type>/<subtype>'.",
77+
]
78+
)
79+
}
80+
}
81+
}
82+
83+
for (key, component) in doc.components.responses {
84+
for contentType in component.content.keys {
85+
if !validate(contentType.rawValue) {
86+
throw Diagnostic.error(
87+
message: "Invalid content type string.",
88+
context: [
89+
"contentType": contentType.rawValue, "location": "#/components/responses/\(key.rawValue)",
90+
"recoverySuggestion": "Must have 2 components separated by a slash '<type>/<subtype>'.",
91+
]
92+
)
93+
}
94+
}
95+
}
96+
}
97+
1798
/// Runs validation steps on the incoming OpenAPI document.
1899
/// - Parameters:
19100
/// - doc: The OpenAPI document to validate.
@@ -30,6 +111,10 @@ func validateDoc(_ doc: ParsedOpenAPIRepresentation, config: Config) throws -> [
30111
// block the generator from running.
31112
// Validation errors continue to be fatal, such as
32113
// structural issues, like non-unique operationIds, etc.
114+
try validateContentTypes(in: doc) { contentType in
115+
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
116+
}
117+
33118
let warnings = try doc.validate(using: Validator().validating(.operationsContainResponses), strict: false)
34119
let diagnostics: [Diagnostic] = warnings.map { warning in
35120
.warning(

Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,243 @@ final class Test_validateDoc: Test_Core {
5656
XCTAssertThrowsError(try validateDoc(doc, config: .init(mode: .types, access: Config.defaultAccessModifier)))
5757
}
5858

59+
func testValidateContentTypes_validContentTypes() throws {
60+
let doc = OpenAPI.Document(
61+
info: .init(title: "Test", version: "1.0.0"),
62+
servers: [],
63+
paths: [
64+
"/path1": .b(
65+
.init(
66+
get: .init(
67+
requestBody: .b(
68+
.init(content: [.init(rawValue: "application/xml")!: .init(schema: .string)])
69+
),
70+
responses: [
71+
.init(integerLiteral: 200): .b(
72+
.init(
73+
description: "Test description 1",
74+
content: [.init(rawValue: "application/json")!: .init(schema: .string)]
75+
)
76+
)
77+
]
78+
)
79+
)
80+
),
81+
"/path2": .b(
82+
.init(
83+
get: .init(
84+
requestBody: .b(.init(content: [.init(rawValue: "text/html")!: .init(schema: .string)])),
85+
responses: [
86+
.init(integerLiteral: 200): .b(
87+
.init(
88+
description: "Test description 2",
89+
content: [.init(rawValue: "text/plain")!: .init(schema: .string)]
90+
)
91+
)
92+
]
93+
)
94+
)
95+
),
96+
],
97+
components: .noComponents
98+
)
99+
XCTAssertNoThrow(
100+
try validateContentTypes(in: doc) { contentType in
101+
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
102+
}
103+
)
104+
}
105+
106+
func testValidateContentTypes_invalidContentTypesInRequestBody() throws {
107+
let doc = OpenAPI.Document(
108+
info: .init(title: "Test", version: "1.0.0"),
109+
servers: [],
110+
paths: [
111+
"/path1": .b(
112+
.init(
113+
get: .init(
114+
requestBody: .b(.init(content: [.init(rawValue: "application/")!: .init(schema: .string)])),
115+
responses: [
116+
.init(integerLiteral: 200): .b(
117+
.init(
118+
description: "Test description 1",
119+
content: [.init(rawValue: "application/json")!: .init(schema: .string)]
120+
)
121+
)
122+
]
123+
)
124+
)
125+
),
126+
"/path2": .b(
127+
.init(
128+
get: .init(
129+
requestBody: .b(.init(content: [.init(rawValue: "text/html")!: .init(schema: .string)])),
130+
responses: [
131+
.init(integerLiteral: 200): .b(
132+
.init(
133+
description: "Test description 2",
134+
content: [.init(rawValue: "text/plain")!: .init(schema: .string)]
135+
)
136+
)
137+
]
138+
)
139+
)
140+
),
141+
],
142+
components: .noComponents
143+
)
144+
XCTAssertThrowsError(
145+
try validateContentTypes(in: doc) { contentType in
146+
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
147+
}
148+
) { error in
149+
XCTAssertTrue(error is Diagnostic)
150+
XCTAssertEqual(
151+
error.localizedDescription,
152+
"error: Invalid content type string. [context: contentType=application/, location=/path1/GET/requestBody, recoverySuggestion=Must have 2 components separated by a slash '<type>/<subtype>'.]"
153+
)
154+
}
155+
}
156+
157+
func testValidateContentTypes_invalidContentTypesInResponses() throws {
158+
let doc = OpenAPI.Document(
159+
info: .init(title: "Test", version: "1.0.0"),
160+
servers: [],
161+
paths: [
162+
"/path1": .b(
163+
.init(
164+
get: .init(
165+
requestBody: .b(
166+
.init(content: [.init(rawValue: "application/xml")!: .init(schema: .string)])
167+
),
168+
responses: [
169+
.init(integerLiteral: 200): .b(
170+
.init(
171+
description: "Test description 1",
172+
content: [.init(rawValue: "application/json")!: .init(schema: .string)]
173+
)
174+
)
175+
]
176+
)
177+
)
178+
),
179+
"/path2": .b(
180+
.init(
181+
get: .init(
182+
requestBody: .b(.init(content: [.init(rawValue: "text/html")!: .init(schema: .string)])),
183+
responses: [
184+
.init(integerLiteral: 200): .b(
185+
.init(
186+
description: "Test description 2",
187+
content: [.init(rawValue: "/plain")!: .init(schema: .string)]
188+
)
189+
)
190+
]
191+
)
192+
)
193+
),
194+
],
195+
components: .noComponents
196+
)
197+
XCTAssertThrowsError(
198+
try validateContentTypes(in: doc) { contentType in
199+
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
200+
}
201+
) { error in
202+
XCTAssertTrue(error is Diagnostic)
203+
XCTAssertEqual(
204+
error.localizedDescription,
205+
"error: Invalid content type string. [context: contentType=/plain, location=/path2/GET/responses, recoverySuggestion=Must have 2 components separated by a slash '<type>/<subtype>'.]"
206+
)
207+
}
208+
}
209+
210+
func testValidateContentTypes_invalidContentTypesInComponentsRequestBodies() throws {
211+
let doc = OpenAPI.Document(
212+
info: .init(title: "Test", version: "1.0.0"),
213+
servers: [],
214+
paths: [
215+
"/path1": .b(
216+
.init(
217+
get: .init(
218+
requestBody: .b(
219+
.init(content: [.init(rawValue: "application/xml")!: .init(schema: .string)])
220+
),
221+
responses: [
222+
.init(integerLiteral: 200): .b(
223+
.init(
224+
description: "Test description 1",
225+
content: [.init(rawValue: "application/json")!: .init(schema: .string)]
226+
)
227+
)
228+
]
229+
)
230+
)
231+
)
232+
],
233+
components: .init(requestBodies: [
234+
"exampleRequestBody1": .init(content: [.init(rawValue: "application/pdf")!: .init(schema: .string)]),
235+
"exampleRequestBody2": .init(content: [.init(rawValue: "image/")!: .init(schema: .string)]),
236+
])
237+
)
238+
XCTAssertThrowsError(
239+
try validateContentTypes(in: doc) { contentType in
240+
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
241+
}
242+
) { error in
243+
XCTAssertTrue(error is Diagnostic)
244+
XCTAssertEqual(
245+
error.localizedDescription,
246+
"error: Invalid content type string. [context: contentType=image/, location=#/components/requestBodies/exampleRequestBody2, recoverySuggestion=Must have 2 components separated by a slash '<type>/<subtype>'.]"
247+
)
248+
}
249+
}
250+
251+
func testValidateContentTypes_invalidContentTypesInComponentsResponses() throws {
252+
let doc = OpenAPI.Document(
253+
info: .init(title: "Test", version: "1.0.0"),
254+
servers: [],
255+
paths: [
256+
"/path1": .b(
257+
.init(
258+
get: .init(
259+
requestBody: .b(
260+
.init(content: [.init(rawValue: "application/xml")!: .init(schema: .string)])
261+
),
262+
responses: [
263+
.init(integerLiteral: 200): .b(
264+
.init(
265+
description: "Test description 1",
266+
content: [.init(rawValue: "application/json")!: .init(schema: .string)]
267+
)
268+
)
269+
]
270+
)
271+
)
272+
)
273+
],
274+
components: .init(responses: [
275+
"exampleRequestBody1": .init(
276+
description: "Test description 1",
277+
content: [.init(rawValue: "application/pdf")!: .init(schema: .string)]
278+
),
279+
"exampleRequestBody2": .init(
280+
description: "Test description 2",
281+
content: [.init(rawValue: "")!: .init(schema: .string)]
282+
),
283+
])
284+
)
285+
XCTAssertThrowsError(
286+
try validateContentTypes(in: doc) { contentType in
287+
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
288+
}
289+
) { error in
290+
XCTAssertTrue(error is Diagnostic)
291+
XCTAssertEqual(
292+
error.localizedDescription,
293+
"error: Invalid content type string. [context: contentType=, location=#/components/responses/exampleRequestBody2, recoverySuggestion=Must have 2 components separated by a slash '<type>/<subtype>'.]"
294+
)
295+
}
296+
}
297+
59298
}

0 commit comments

Comments
 (0)