Skip to content

Commit 3c0fcab

Browse files
authored
Improve the diagnostic for when a schema is unsupported (#164)
Improve the diagnostic for when a schema is unsupported ### Motivation Previously, when a schema that was, or contained a subschema that was, unsupported, adopter would get a pretty opaque warning diagnostic telling them the "schema is unsupported", without more details of why, and which part of it. ### Modifications This PR moves from a simple Boolean status on the `isSchemaSupported` class of utility functions, and moves to a richer enum, which includes the reason and the schema itself when unsupported, leading to much more informative diagnostics. ### Result Diagnostics emitted for unsupported schemas will be a lot more meaningful. ### Test Plan Adapted the unit tests to make sure all the reasons for unsupported schemas are now tested, and the reason matches. 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. #164
1 parent c181ed7 commit 3c0fcab

File tree

3 files changed

+192
-48
lines changed

3 files changed

+192
-48
lines changed

Sources/_OpenAPIGeneratorCore/Diagnostics.swift

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,32 @@ public struct Diagnostic: Error, Codable {
113113
context: context
114114
)
115115
}
116+
117+
/// Creates a diagnostic for an unsupported schema.
118+
/// - Parameters:
119+
/// - reason: A human-readable reason.
120+
/// - schema: The unsupported JSON schema.
121+
/// - foundIn: A description of the location in which the unsupported
122+
/// schema was detected.
123+
/// - location: Describe the source file that triggered the diagnostic (if known).
124+
/// - context: A set of key-value pairs that help the user understand
125+
/// where the warning occurred.
126+
/// - Returns: A warning diagnostic.
127+
public static func unsupportedSchema(
128+
reason: String,
129+
schema: JSONSchema,
130+
foundIn: String,
131+
location: Location? = nil,
132+
context: [String: String] = [:]
133+
) -> Diagnostic {
134+
var context = context
135+
context["foundIn"] = foundIn
136+
return warning(
137+
message: "Schema \"\(schema.prettyDescription)\" is not supported, reason: \"\(reason)\", skipping",
138+
location: location,
139+
context: context
140+
)
141+
}
116142
}
117143

118144
extension Diagnostic.Severity: CustomStringConvertible {
@@ -174,6 +200,31 @@ extension DiagnosticCollector {
174200
emit(Diagnostic.unsupported(feature, foundIn: foundIn, context: context))
175201
}
176202

203+
/// Emits a diagnostic for an unsupported schema found in the specified
204+
/// string location.
205+
/// - Parameters:
206+
/// - reason: A human-readable reason.
207+
/// - schema: The unsupported JSON schema.
208+
/// - foundIn: A description of the location in which the unsupported
209+
/// schema was detected.
210+
/// - context: A set of key-value pairs that help the user understand
211+
/// where the warning occurred.
212+
func emitUnsupportedSchema(
213+
reason: String,
214+
schema: JSONSchema,
215+
foundIn: String,
216+
context: [String: String] = [:]
217+
) {
218+
emit(
219+
Diagnostic.unsupportedSchema(
220+
reason: reason,
221+
schema: schema,
222+
foundIn: foundIn,
223+
context: context
224+
)
225+
)
226+
}
227+
177228
/// Emits a diagnostic for an unsupported feature found in the specified
178229
/// type name.
179230
///

Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/isSchemaSupported.swift

Lines changed: 123 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,46 @@
1313
//===----------------------------------------------------------------------===//
1414
import OpenAPIKit30
1515

16+
/// A result of checking whether a schema is supported.
17+
enum IsSchemaSupportedResult: Equatable {
18+
19+
/// The schema is supported and can be generated.
20+
case supported
21+
22+
/// The reason a schema is unsupported.
23+
enum UnsupportedReason: Equatable, CustomStringConvertible {
24+
25+
/// Describes when no subschemas are found in an allOf, oneOf, or anyOf.
26+
case noSubschemas
27+
28+
/// Describes when the schema is not object-ish, in other words isn't
29+
/// an object, a ref, or an allOf.
30+
case notObjectish
31+
32+
/// Describes when the schema is not a reference.
33+
case notRef
34+
35+
/// Describes when the schema is of an unsupported schema type.
36+
case schemaType
37+
38+
var description: String {
39+
switch self {
40+
case .noSubschemas:
41+
return "no subschemas"
42+
case .notObjectish:
43+
return "not an object-ish schema (object, ref, allOf)"
44+
case .notRef:
45+
return "not a reference"
46+
case .schemaType:
47+
return "schema type"
48+
}
49+
}
50+
}
51+
52+
/// The schema is unsupported for the provided reason.
53+
case unsupported(reason: UnsupportedReason, schema: JSONSchema)
54+
}
55+
1656
extension FileTranslator {
1757

1858
/// Validates that the schema is supported by the generator.
@@ -26,11 +66,17 @@ extension FileTranslator {
2666
_ schema: JSONSchema,
2767
foundIn: String
2868
) throws -> Bool {
29-
guard try isSchemaSupported(schema) else {
30-
diagnostics.emitUnsupported("Schema", foundIn: foundIn)
69+
switch try isSchemaSupported(schema) {
70+
case .supported:
71+
return true
72+
case .unsupported(reason: let reason, schema: let schema):
73+
diagnostics.emitUnsupportedSchema(
74+
reason: reason.description,
75+
schema: schema,
76+
foundIn: foundIn
77+
)
3178
return false
3279
}
33-
return true
3480
}
3581

3682
/// Validates that the schema is supported by the generator.
@@ -44,22 +90,27 @@ extension FileTranslator {
4490
_ schema: UnresolvedSchema?,
4591
foundIn: String
4692
) throws -> Bool {
47-
guard try isSchemaSupported(schema) else {
48-
diagnostics.emitUnsupported("Schema", foundIn: foundIn)
93+
switch try isSchemaSupported(schema) {
94+
case .supported:
95+
return true
96+
case .unsupported(reason: let reason, schema: let schema):
97+
diagnostics.emitUnsupportedSchema(
98+
reason: reason.description,
99+
schema: schema,
100+
foundIn: foundIn
101+
)
49102
return false
50103
}
51-
return true
52104
}
53105

54-
/// Returns a Boolean value that indicates whether the schema is supported.
106+
/// Returns whether the schema is supported.
55107
///
56108
/// If a schema is not supported, no references to it should be emitted.
57109
/// - Parameters:
58110
/// - schema: The schema to validate.
59-
/// - Returns: `true` if the schema is supported; `false` otherwise.
60111
func isSchemaSupported(
61112
_ schema: JSONSchema
62-
) throws -> Bool {
113+
) throws -> IsSchemaSupportedResult {
63114
switch schema.value {
64115
case .string,
65116
.integer,
@@ -71,31 +122,40 @@ extension FileTranslator {
71122
// responsible for picking only supported properties.
72123
.object,
73124
.fragment:
74-
return true
125+
return .supported
75126
case .reference(let ref, _):
76127
// reference is supported iff the existing type is supported
77128
let existingSchema = try components.lookup(ref)
78129
return try isSchemaSupported(existingSchema)
79130
case .array(_, let array):
80131
guard let items = array.items else {
81132
// an array of fragments is supported
82-
return true
133+
return .supported
83134
}
84135
// an array is supported iff its element schema is supported
85136
return try isSchemaSupported(items)
86137
case .all(of: let schemas, _):
87138
guard !schemas.isEmpty else {
88-
return false
139+
return .unsupported(
140+
reason: .noSubschemas,
141+
schema: schema
142+
)
89143
}
90144
return try areObjectishSchemasAndSupported(schemas)
91145
case .any(of: let schemas, _):
92146
guard !schemas.isEmpty else {
93-
return false
147+
return .unsupported(
148+
reason: .noSubschemas,
149+
schema: schema
150+
)
94151
}
95152
return try areObjectishSchemasAndSupported(schemas)
96153
case .one(of: let schemas, let context):
97154
guard !schemas.isEmpty else {
98-
return false
155+
return .unsupported(
156+
reason: .noSubschemas,
157+
schema: schema
158+
)
99159
}
100160
// If a discriminator is provided, only refs to object/allOf of
101161
// object schemas are allowed.
@@ -105,81 +165,104 @@ extension FileTranslator {
105165
}
106166
return try areRefsToObjectishSchemaAndSupported(schemas)
107167
case .not:
108-
return false
168+
return .unsupported(
169+
reason: .schemaType,
170+
schema: schema
171+
)
109172
}
110173
}
111174

112-
/// Returns a Boolean value that indicates whether the schema is supported.
175+
/// Returns a result indicating whether the schema is supported.
113176
///
114177
/// If a schema is not supported, no references to it should be emitted.
115178
/// - Parameters:
116179
/// - schema: The schema to validate.
117-
/// - Returns: `true` if the schema is supported; `false` otherwise.
118180
func isSchemaSupported(
119181
_ schema: UnresolvedSchema?
120-
) throws -> Bool {
182+
) throws -> IsSchemaSupportedResult {
121183
guard let schema else {
122184
// fragment type is supported
123-
return true
185+
return .supported
124186
}
125187
switch schema {
126188
case .a:
127189
// references are supported
128-
return true
190+
return .supported
129191
case let .b(schema):
130192
return try isSchemaSupported(schema)
131193
}
132194
}
133195

134-
/// Returns a Boolean value that indicates whether the provided schemas
196+
/// Returns a result indicating whether the provided schemas
135197
/// are supported.
136198
/// - Parameter schemas: Schemas to check.
137-
/// - Returns: `true` if all schemas are supported; `false` otherwise.
138-
func areSchemasSupported(_ schemas: [JSONSchema]) throws -> Bool {
139-
try schemas.allSatisfy(isSchemaSupported)
199+
func areSchemasSupported(_ schemas: [JSONSchema]) throws -> IsSchemaSupportedResult {
200+
for schema in schemas {
201+
let result = try isSchemaSupported(schema)
202+
guard result == .supported else {
203+
return result
204+
}
205+
}
206+
return .supported
140207
}
141208

142-
/// Returns a Boolean value that indicates whether the provided schemas
209+
/// Returns a result indicating whether the provided schemas
143210
/// are reference, object, or allOf schemas and supported.
144211
/// - Parameter schemas: Schemas to check.
145-
/// - Returns: `true` if all schemas match; `false` otherwise.
146-
func areObjectishSchemasAndSupported(_ schemas: [JSONSchema]) throws -> Bool {
147-
try schemas.allSatisfy(isObjectishSchemaAndSupported)
212+
/// - Returns: `.supported` if all schemas match; `.unsupported` otherwise.
213+
func areObjectishSchemasAndSupported(_ schemas: [JSONSchema]) throws -> IsSchemaSupportedResult {
214+
for schema in schemas {
215+
let result = try isObjectishSchemaAndSupported(schema)
216+
guard result == .supported else {
217+
return result
218+
}
219+
}
220+
return .supported
148221
}
149222

150-
/// Returns a Boolean value that indicates whether the provided schema
223+
/// Returns a result indicating whether the provided schema
151224
/// is an reference, object, or allOf (object-ish) schema and is supported.
152225
/// - Parameter schema: A schemas to check.
153-
/// - Returns: `true` if the schema matches; `false` otherwise.
154-
func isObjectishSchemaAndSupported(_ schema: JSONSchema) throws -> Bool {
226+
func isObjectishSchemaAndSupported(_ schema: JSONSchema) throws -> IsSchemaSupportedResult {
155227
switch schema.value {
156228
case .object, .reference:
157229
return try isSchemaSupported(schema)
158230
case .all(of: let schemas, _):
159231
return try areObjectishSchemasAndSupported(schemas)
160232
default:
161-
return false
233+
return .unsupported(
234+
reason: .notObjectish,
235+
schema: schema
236+
)
162237
}
163238
}
164239

165-
/// Returns a Boolean value that indicates whether the provided schemas
240+
/// Returns a result indicating whether the provided schemas
166241
/// are reference schemas that point to object-ish schemas and supported.
167242
/// - Parameter schemas: Schemas to check.
168-
/// - Returns: `true` if all schemas match; `false` otherwise.
169-
func areRefsToObjectishSchemaAndSupported(_ schemas: [JSONSchema]) throws -> Bool {
170-
try schemas.allSatisfy(isRefToObjectishSchemaAndSupported)
243+
/// - Returns: `.supported` if all schemas match; `.unsupported` otherwise.
244+
func areRefsToObjectishSchemaAndSupported(_ schemas: [JSONSchema]) throws -> IsSchemaSupportedResult {
245+
for schema in schemas {
246+
let result = try isRefToObjectishSchemaAndSupported(schema)
247+
guard result == .supported else {
248+
return result
249+
}
250+
}
251+
return .supported
171252
}
172253

173-
/// Returns a Boolean value that indicates whether the provided schema
254+
/// Returns a result indicating whether the provided schema
174255
/// is a reference schema that points to an object-ish schema and is supported.
175256
/// - Parameter schema: A schema to check.
176-
/// - Returns: `true` if the schema matches; `false` otherwise.
177-
func isRefToObjectishSchemaAndSupported(_ schema: JSONSchema) throws -> Bool {
257+
func isRefToObjectishSchemaAndSupported(_ schema: JSONSchema) throws -> IsSchemaSupportedResult {
178258
switch schema.value {
179259
case .reference:
180260
return try isObjectishSchemaAndSupported(schema)
181261
default:
182-
return false
262+
return .unsupported(
263+
reason: .notRef,
264+
schema: schema
265+
)
183266
}
184267
}
185268
}

Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_isSchemaSupported.swift

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,23 +88,33 @@ class Test_isSchemaSupported: XCTestCase {
8888
let translator = self.translator
8989
for schema in Self.supportedTypes {
9090
XCTAssertTrue(
91-
try translator.isSchemaSupported(schema),
91+
try translator.isSchemaSupported(schema) == .supported,
9292
"Expected schema to be supported: \(schema)"
9393
)
9494
}
9595
}
9696

97-
static let unsupportedTypes: [JSONSchema] = [
97+
static let unsupportedTypes: [(JSONSchema, IsSchemaSupportedResult.UnsupportedReason)] = [
9898
// a not
99-
.not(.string)
99+
(.not(.string), .schemaType),
100+
101+
// an allOf without any subschemas
102+
(.all(of: []), .noSubschemas),
103+
104+
// an allOf with non-object-ish schemas
105+
(.all(of: [.string, .integer]), .notObjectish),
106+
107+
// a oneOf with a discriminator with an inline subschema
108+
(.one(of: .object, discriminator: .init(propertyName: "foo")), .notRef),
100109
]
101110
func testUnsupportedTypes() throws {
102111
let translator = self.translator
103-
for schema in Self.unsupportedTypes {
104-
XCTAssertFalse(
105-
try translator.isSchemaSupported(schema),
106-
"Expected schema to be unsupported: \(schema)"
107-
)
112+
for (schema, expectedReason) in Self.unsupportedTypes {
113+
guard case let .unsupported(reason, _) = try translator.isSchemaSupported(schema) else {
114+
XCTFail("Expected schema to be unsupported: \(schema)")
115+
return
116+
}
117+
XCTAssertEqual(reason, expectedReason)
108118
}
109119
}
110120
}

0 commit comments

Comments
 (0)