Skip to content

Commit f58f60d

Browse files
authored
Validate references in validateDoc (#500)
### 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.
1 parent 85fec92 commit f58f60d

File tree

2 files changed

+304
-7
lines changed

2 files changed

+304
-7
lines changed

Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift

Lines changed: 165 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ import OpenAPIKit
2525
/// - Throws: An error with diagnostic information if any invalid content types are found.
2626
func validateContentTypes(in doc: ParsedOpenAPIRepresentation, validate: (String) -> Bool) throws {
2727
for (path, pathValue) in doc.paths {
28-
guard case .b(let pathItem) = pathValue else { continue }
28+
guard let pathItem = pathValue.pathItemValue else { continue }
2929
for endpoint in pathItem.endpoints {
3030

3131
if let eitherRequest = endpoint.operation.requestBody {
32-
if case .b(let actualRequest) = eitherRequest {
32+
if let actualRequest = eitherRequest.requestValue {
3333
for contentType in actualRequest.content.keys {
3434
if !validate(contentType.rawValue) {
3535
throw Diagnostic.error(
@@ -47,7 +47,7 @@ func validateContentTypes(in doc: ParsedOpenAPIRepresentation, validate: (String
4747
}
4848

4949
for eitherResponse in endpoint.operation.responses.values {
50-
if case .b(let actualResponse) = eitherResponse {
50+
if let actualResponse = eitherResponse.responseValue {
5151
for contentType in actualResponse.content.keys {
5252
if !validate(contentType.rawValue) {
5353
throw Diagnostic.error(
@@ -95,13 +95,175 @@ func validateContentTypes(in doc: ParsedOpenAPIRepresentation, validate: (String
9595
}
9696
}
9797

98+
/// Validates all references from an OpenAPI document represented by a ParsedOpenAPIRepresentation against its components.
99+
///
100+
/// This method traverses the OpenAPI document to ensure that all references
101+
/// within the document are valid and point to existing components.
102+
///
103+
/// - Parameter doc: The OpenAPI document to validate.
104+
/// - Throws: `Diagnostic.error` if an external reference is found or a reference is not found in components.
105+
func validateReferences(in doc: ParsedOpenAPIRepresentation) throws {
106+
func validateReference<ReferenceType: ComponentDictionaryLocatable>(
107+
_ reference: OpenAPI.Reference<ReferenceType>,
108+
in components: OpenAPI.Components,
109+
location: String
110+
) throws {
111+
if reference.isExternal {
112+
throw Diagnostic.error(
113+
message: "External references are not suppported.",
114+
context: ["reference": reference.absoluteString, "location": location]
115+
)
116+
}
117+
if components[reference] == nil {
118+
throw Diagnostic.error(
119+
message: "Reference not found in components.",
120+
context: ["reference": reference.absoluteString, "location": location]
121+
)
122+
}
123+
}
124+
125+
func validateReferencesInContentTypes(_ content: OpenAPI.Content.Map, location: String) throws {
126+
for (contentKey, contentType) in content {
127+
if let reference = contentType.schema?.reference {
128+
try validateReference(
129+
reference,
130+
in: doc.components,
131+
location: location + "/content/\(contentKey.rawValue)/schema"
132+
)
133+
}
134+
if let eitherExamples = contentType.examples?.values {
135+
for example in eitherExamples {
136+
if let reference = example.reference {
137+
try validateReference(
138+
reference,
139+
in: doc.components,
140+
location: location + "/content/\(contentKey.rawValue)/examples"
141+
)
142+
}
143+
}
144+
}
145+
}
146+
}
147+
148+
for (key, value) in doc.webhooks {
149+
if let reference = value.reference { try validateReference(reference, in: doc.components, location: key) }
150+
}
151+
152+
for (path, pathValue) in doc.paths {
153+
if let reference = pathValue.reference {
154+
try validateReference(reference, in: doc.components, location: path.rawValue)
155+
} else if let pathItem = pathValue.pathItemValue {
156+
157+
for endpoint in pathItem.endpoints {
158+
for (endpointKey, endpointValue) in endpoint.operation.callbacks {
159+
if let reference = endpointValue.reference {
160+
try validateReference(
161+
reference,
162+
in: doc.components,
163+
location: "\(path.rawValue)/\(endpoint.method.rawValue)/callbacks/\(endpointKey)"
164+
)
165+
}
166+
}
167+
168+
for eitherParameter in endpoint.operation.parameters {
169+
if let reference = eitherParameter.reference {
170+
try validateReference(
171+
reference,
172+
in: doc.components,
173+
location: "\(path.rawValue)/\(endpoint.method.rawValue)/parameters"
174+
)
175+
} else if let parameter = eitherParameter.parameterValue {
176+
if let reference = parameter.schemaOrContent.schemaReference {
177+
try validateReference(
178+
reference,
179+
in: doc.components,
180+
location: "\(path.rawValue)/\(endpoint.method.rawValue)/parameters/\(parameter.name)"
181+
)
182+
} else if let content = parameter.schemaOrContent.contentValue {
183+
try validateReferencesInContentTypes(
184+
content,
185+
location: "\(path.rawValue)/\(endpoint.method.rawValue)/parameters/\(parameter.name)"
186+
)
187+
}
188+
}
189+
}
190+
if let reference = endpoint.operation.requestBody?.reference {
191+
try validateReference(
192+
reference,
193+
in: doc.components,
194+
location: "\(path.rawValue)/\(endpoint.method.rawValue)/requestBody"
195+
)
196+
} else if let requestBodyValue = endpoint.operation.requestBody?.requestValue {
197+
try validateReferencesInContentTypes(
198+
requestBodyValue.content,
199+
location: "\(path.rawValue)/\(endpoint.method.rawValue)/requestBody"
200+
)
201+
}
202+
203+
for (statusCode, eitherResponse) in endpoint.operation.responses {
204+
if let reference = eitherResponse.reference {
205+
try validateReference(
206+
reference,
207+
in: doc.components,
208+
location: "\(path.rawValue)/\(endpoint.method.rawValue)/responses/\(statusCode.rawValue)"
209+
)
210+
} else if let responseValue = eitherResponse.responseValue {
211+
try validateReferencesInContentTypes(
212+
responseValue.content,
213+
location: "\(path.rawValue)/\(endpoint.method.rawValue)/responses/\(statusCode.rawValue)"
214+
)
215+
}
216+
if let headers = eitherResponse.responseValue?.headers {
217+
for (headerKey, eitherHeader) in headers {
218+
if let reference = eitherHeader.reference {
219+
try validateReference(
220+
reference,
221+
in: doc.components,
222+
location:
223+
"\(path.rawValue)/\(endpoint.method.rawValue)/responses/\(statusCode.rawValue)/headers/\(headerKey)"
224+
)
225+
} else if let headerValue = eitherHeader.headerValue {
226+
if let schemaReference = headerValue.schemaOrContent.schemaReference {
227+
try validateReference(
228+
schemaReference,
229+
in: doc.components,
230+
location:
231+
"\(path.rawValue)/\(endpoint.method.rawValue)/responses/\(statusCode.rawValue)/headers/\(headerKey)"
232+
)
233+
} else if let contentValue = headerValue.schemaOrContent.contentValue {
234+
try validateReferencesInContentTypes(
235+
contentValue,
236+
location:
237+
"\(path.rawValue)/\(endpoint.method.rawValue)/responses/\(statusCode.rawValue)/headers/\(headerKey)"
238+
)
239+
}
240+
}
241+
}
242+
}
243+
}
244+
}
245+
246+
for eitherParameter in pathItem.parameters {
247+
if let reference = eitherParameter.reference {
248+
try validateReference(reference, in: doc.components, location: "\(path.rawValue)/parameters")
249+
}
250+
}
251+
}
252+
}
253+
}
254+
98255
/// Runs validation steps on the incoming OpenAPI document.
99256
/// - Parameters:
100257
/// - doc: The OpenAPI document to validate.
101258
/// - config: The generator config.
102259
/// - Returns: An array of diagnostic messages representing validation warnings.
103260
/// - Throws: An error if a fatal issue is found.
104261
func validateDoc(_ doc: ParsedOpenAPIRepresentation, config: Config) throws -> [Diagnostic] {
262+
try validateReferences(in: doc)
263+
try validateContentTypes(in: doc) { contentType in
264+
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
265+
}
266+
105267
// Run OpenAPIKit's built-in validation.
106268
// Pass `false` to `strict`, however, because we don't
107269
// want to turn schema loading warnings into errors.
@@ -111,10 +273,6 @@ func validateDoc(_ doc: ParsedOpenAPIRepresentation, config: Config) throws -> [
111273
// block the generator from running.
112274
// Validation errors continue to be fatal, such as
113275
// structural issues, like non-unique operationIds, etc.
114-
try validateContentTypes(in: doc) { contentType in
115-
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
116-
}
117-
118276
let warnings = try doc.validate(using: Validator().validating(.operationsContainResponses), strict: false)
119277
let diagnostics: [Diagnostic] = warnings.map { warning in
120278
.warning(

Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,4 +295,143 @@ final class Test_validateDoc: Test_Core {
295295
}
296296
}
297297

298+
func testValidateReferences_validReferences() throws {
299+
let doc = OpenAPI.Document(
300+
info: .init(title: "Test", version: "1.0.0"),
301+
servers: [],
302+
paths: [
303+
"/path1": .b(
304+
.init(
305+
get: .init(
306+
parameters: .init(
307+
arrayLiteral: .b(
308+
.init(
309+
name: "ID",
310+
context: .path,
311+
content: [
312+
.init(rawValue: "text/plain")!: .init(
313+
schema: .a(.component(named: "Path1ParametersContentSchemaReference"))
314+
)
315+
]
316+
)
317+
),
318+
.init(.component(named: "Path1ParametersReference"))
319+
),
320+
requestBody: .reference(.component(named: "RequestBodyReference")),
321+
responses: [
322+
.init(integerLiteral: 200): .reference(.component(named: "ResponsesReference")),
323+
.init(integerLiteral: 202): .response(
324+
.init(
325+
description: "ResponseDescription",
326+
content: [
327+
.init(rawValue: "text/plain")!: .init(
328+
schema: .a(.component(named: "ResponsesContentSchemaReference"))
329+
)
330+
]
331+
)
332+
),
333+
.init(integerLiteral: 204): .response(
334+
description: "Response Description",
335+
headers: ["Header": .a(.component(named: "ResponsesHeaderReference"))]
336+
),
337+
]
338+
)
339+
)
340+
), "/path2": .a(.component(named: "Path2Reference")),
341+
"/path3": .b(
342+
.init(
343+
get: .init(
344+
parameters: .init(arrayLiteral: .a(.component(named: "Path3ExampleID"))),
345+
requestBody: .b(
346+
.init(content: [
347+
.init(rawValue: "text/html")!: .init(
348+
schema: .a(.component(named: "RequestBodyContentSchemaReference"))
349+
)
350+
])
351+
),
352+
responses: [:],
353+
callbacks: [.init("Callback"): .a(.component(named: "CallbackReference"))]
354+
)
355+
)
356+
),
357+
],
358+
components: .init(
359+
schemas: [
360+
"ResponsesContentSchemaReference": .init(schema: .string(.init(), .init())),
361+
"RequestBodyContentSchemaReference": .init(schema: .integer(.init(), .init())),
362+
"Path1ParametersContentSchemaReference": .init(schema: .string(.init(), .init())),
363+
],
364+
responses: ["ResponsesReference": .init(description: "Description")],
365+
parameters: [
366+
"Path3ExampleID": .init(name: "ID", context: .path, content: .init()),
367+
"Path1ParametersReference": .init(name: "Schema", context: .path, schema: .array),
368+
],
369+
requestBodies: [
370+
"RequestBodyReference": .init(content: .init())
371+
372+
],
373+
headers: ["ResponsesHeaderReference": .init(schema: .array)],
374+
callbacks: ["CallbackReference": .init()],
375+
pathItems: ["Path2Reference": .init()]
376+
)
377+
)
378+
XCTAssertNoThrow(try validateReferences(in: doc))
379+
}
380+
381+
func testValidateReferences_referenceNotFoundInComponents() throws {
382+
let doc = OpenAPI.Document(
383+
info: .init(title: "Test", version: "1.0.0"),
384+
servers: [],
385+
paths: [
386+
"/path": .b(
387+
.init(
388+
get: .init(
389+
requestBody: .b(
390+
.init(content: [
391+
.init(rawValue: "text/html")!: .init(
392+
schema: .a(.component(named: "RequestBodyContentSchemaReference"))
393+
)
394+
])
395+
),
396+
responses: [:]
397+
)
398+
)
399+
)
400+
],
401+
components: .init(schemas: ["RequestBodyContentSchema": .init(schema: .integer(.init(), .init()))])
402+
)
403+
XCTAssertThrowsError(try validateReferences(in: doc)) { error in
404+
XCTAssertTrue(error is Diagnostic)
405+
XCTAssertEqual(
406+
error.localizedDescription,
407+
"error: Reference not found in components. [context: location=/path/GET/requestBody/content/text/html/schema, reference=#/components/schemas/RequestBodyContentSchemaReference]"
408+
)
409+
}
410+
}
411+
412+
func testValidateReferences_foundExternalReference() throws {
413+
let doc = OpenAPI.Document(
414+
info: .init(title: "Test", version: "1.0.0"),
415+
servers: [],
416+
paths: [
417+
"/path": .b(
418+
.init(
419+
get: .init(
420+
requestBody: .b(.init(content: .init())),
421+
responses: [.init(integerLiteral: 200): .reference(.external(URL(string: "ExternalURL")!))]
422+
)
423+
)
424+
)
425+
],
426+
components: .noComponents
427+
)
428+
XCTAssertThrowsError(try validateReferences(in: doc)) { error in
429+
XCTAssertTrue(error is Diagnostic)
430+
XCTAssertEqual(
431+
error.localizedDescription,
432+
"error: External references are not suppported. [context: location=/path/GET/responses/200, reference=ExternalURL]"
433+
)
434+
}
435+
}
436+
298437
}

0 commit comments

Comments
 (0)