Skip to content

Commit e2f476d

Browse files
authored
Stop treating schema warnings as errors (#178)
Stop treating schema warnings as errors ### Motivation In #130, we introduced extra validation by calling into OpenAPIKit's `validate` method. (It's hidden behind a feature flag, but I've been testing with it enabled.) It looks for structural issues, such as non-unique operation ids, which would result in us generating non-compiling code anyway, so the validation helps catch these early and emit descriptive errors that adopters can use to fix their doc. However, the method also takes a parameter `strict`, which defaults to `true`, and when enabled, it turns warnings emitted during schema parsing into errors. This part is _too_ strict for us and was rejecting OpenAPI documents that were valid enough for our needs. An example of a schema warning is a schema having `minItems: 1` on a non-array schema. While it's not technically correct, it also doesn't impede our understanding of the non-array schema, as we never actually check what the value of `minItems` is. That's why these are just warnings, not errors, so we should stop promoting them to fatal errors that block an adopter from generating code. ### Modifications This PR flips the `strict` parameter to `false`. This doesn't make us miss out on these warnings, as recently (in #174), we started forwarding these schema warnings into generator diagnostics, so the adopter will see them still, and can address them on their own schedule. ### Result Now documents with only schema warnings aren't rejected by the generator anymore. ### Test Plan Added a unit test of the broken-out `validateDoc` function, ensures that a schema with warnings doesn't trip it up anymore. Reviewed by: simonjbeaumont Builds: ✔︎ pull request validation (5.8) - Build finished. ✔︎ pull request validation (5.9) - Build finished. ✔︎ pull request validation (docc test) - Build finished. ✔︎ pull request validation (integration test) - Build finished. ✔︎ pull request validation (nightly) - Build finished. ✔︎ pull request validation (soundness) - Build finished. #178
1 parent 4cf4891 commit e2f476d

File tree

5 files changed

+163
-38
lines changed

5 files changed

+163
-38
lines changed

Sources/_OpenAPIGeneratorCore/GeneratorPipeline.swift

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ public func runGenerator(
106106
/// ``GeneratorPipeline/run(_:)``.
107107
func makeGeneratorPipeline(
108108
parser: any ParserProtocol = YamsParser(),
109+
validator: @escaping (ParsedOpenAPIRepresentation, Config) throws -> [Diagnostic] = validateDoc,
109110
translator: any TranslatorProtocol = MultiplexTranslator(),
110111
renderer: any RendererProtocol = TextBasedRenderer(),
111112
formatter: @escaping (InMemoryOutputFile) throws -> InMemoryOutputFile = { try $0.swiftFormatted },
@@ -124,36 +125,10 @@ func makeGeneratorPipeline(
124125
},
125126
postTransitionHooks: [
126127
{ doc in
127-
128-
if config.featureFlags.contains(.strictOpenAPIValidation) {
129-
// Run OpenAPIKit's built-in validation.
130-
try doc.validate()
131-
132-
// Validate that the document is dereferenceable, which
133-
// catches reference cycles, which we don't yet support.
134-
_ = try doc.locallyDereferenced()
135-
136-
// Also explicitly dereference the parts of components
137-
// that the generator uses. `locallyDereferenced()` above
138-
// only dereferences paths/operations, but not components.
139-
let components = doc.components
140-
try components.schemas.forEach { schema in
141-
_ = try schema.value.dereferenced(in: components)
142-
}
143-
try components.parameters.forEach { schema in
144-
_ = try schema.value.dereferenced(in: components)
145-
}
146-
try components.headers.forEach { schema in
147-
_ = try schema.value.dereferenced(in: components)
148-
}
149-
try components.requestBodies.forEach { schema in
150-
_ = try schema.value.dereferenced(in: components)
151-
}
152-
try components.responses.forEach { schema in
153-
_ = try schema.value.dereferenced(in: components)
154-
}
128+
let validationDiagnostics = try validator(doc, config)
129+
for diagnostic in validationDiagnostics {
130+
diagnostics.emit(diagnostic)
155131
}
156-
157132
return doc
158133
}
159134
]
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftOpenAPIGenerator open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
/// Runs validation steps on the incoming OpenAPI document.
16+
/// - Parameters:
17+
/// - doc: The OpenAPI document to validate.
18+
/// - config: The generator config.
19+
/// - Throws: An error if a fatal issue is found.
20+
func validateDoc(_ doc: ParsedOpenAPIRepresentation, config: Config) throws -> [Diagnostic] {
21+
guard config.featureFlags.contains(.strictOpenAPIValidation) else {
22+
return []
23+
}
24+
// Run OpenAPIKit's built-in validation.
25+
// Pass `false` to `strict`, however, because we don't
26+
// want to turn schema loading warnings into errors.
27+
// We already propagate the warnings to the generator's
28+
// diagnostics, so they get surfaced to the user.
29+
// But the warnings are often too strict and should not
30+
// block the generator from running.
31+
// Validation errors continue to be fatal, such as
32+
// structural issues, like non-unique operationIds, etc.
33+
let warnings = try doc.validate(strict: false)
34+
let diagnostics: [Diagnostic] = warnings.map { warning in
35+
.warning(
36+
message: "Validation warning: \(warning.description)",
37+
context: [
38+
"codingPath": warning.codingPathString ?? "<none>",
39+
"contextString": warning.contextString ?? "<none>",
40+
"subjectName": warning.subjectName ?? "<none>",
41+
]
42+
)
43+
}
44+
45+
// Validate that the document is dereferenceable, which
46+
// catches reference cycles, which we don't yet support.
47+
_ = try doc.locallyDereferenced()
48+
49+
// Also explicitly dereference the parts of components
50+
// that the generator uses. `locallyDereferenced()` above
51+
// only dereferences paths/operations, but not components.
52+
let components = doc.components
53+
try components.schemas.forEach { schema in
54+
_ = try schema.value.dereferenced(in: components)
55+
}
56+
try components.parameters.forEach { schema in
57+
_ = try schema.value.dereferenced(in: components)
58+
}
59+
try components.headers.forEach { schema in
60+
_ = try schema.value.dereferenced(in: components)
61+
}
62+
try components.requestBodies.forEach { schema in
63+
_ = try schema.value.dereferenced(in: components)
64+
}
65+
try components.responses.forEach { schema in
66+
_ = try schema.value.dereferenced(in: components)
67+
}
68+
return diagnostics
69+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftOpenAPIGenerator open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
import XCTest
15+
import OpenAPIKit30
16+
@testable import _OpenAPIGeneratorCore
17+
18+
final class Test_validateDoc: Test_Core {
19+
20+
func testSchemaWarningIsNotFatal() throws {
21+
let schemaWithWarnings = try loadSchemaFromYAML(
22+
#"""
23+
type: string
24+
items:
25+
type: integer
26+
"""#
27+
)
28+
let doc = OpenAPI.Document(
29+
info: .init(title: "Test", version: "1.0.0"),
30+
servers: [],
31+
paths: [:],
32+
components: .init(schemas: [
33+
"myImperfectSchema": schemaWithWarnings
34+
])
35+
)
36+
let diagnostics = try validateDoc(
37+
doc,
38+
config: .init(
39+
mode: .types,
40+
featureFlags: [
41+
.strictOpenAPIValidation
42+
]
43+
)
44+
)
45+
XCTAssertEqual(diagnostics.count, 1)
46+
}
47+
48+
func testStructuralWarningIsFatal() throws {
49+
let doc = OpenAPI.Document(
50+
info: .init(title: "Test", version: "1.0.0"),
51+
servers: [],
52+
paths: [
53+
"/foo": .b(
54+
.init(
55+
get: .init(
56+
requestBody: nil,
57+
58+
// Fatal error: missing at least one response.
59+
responses: [:]
60+
)
61+
)
62+
)
63+
],
64+
components: .noComponents
65+
)
66+
XCTAssertThrowsError(
67+
try validateDoc(
68+
doc,
69+
config: .init(
70+
mode: .types,
71+
featureFlags: [
72+
.strictOpenAPIValidation
73+
]
74+
)
75+
)
76+
)
77+
}
78+
79+
}

Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ class Test_Core: XCTestCase {
5454
)
5555
}
5656

57+
func loadSchemaFromYAML(_ yamlString: String) throws -> JSONSchema {
58+
try YAMLDecoder().decode(JSONSchema.self, from: yamlString)
59+
}
60+
5761
static var testTypeName: TypeName {
5862
.init(swiftKeyPath: ["Foo"])
5963
}

Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_translateSchemas.swift

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,13 @@ class Test_translateSchemas: Test_Core {
2121
func testSchemaWarningsForwardedToGeneratorDiagnostics() throws {
2222
let typeName = TypeName(swiftKeyPath: ["Foo"])
2323

24-
let schemaWithWarnings = try YAMLDecoder()
25-
.decode(
26-
JSONSchema.self,
27-
from: #"""
28-
type: string
29-
items:
30-
type: integer
31-
"""#
32-
)
24+
let schemaWithWarnings = try loadSchemaFromYAML(
25+
#"""
26+
type: string
27+
items:
28+
type: integer
29+
"""#
30+
)
3331

3432
let cases: [(JSONSchema, [String])] = [
3533
(.string, []),

0 commit comments

Comments
 (0)