Skip to content

Commit c22e88d

Browse files
committed
support references in TypeMatcher isOptional
1 parent 5ed951f commit c22e88d

File tree

2 files changed

+53
-8
lines changed

2 files changed

+53
-8
lines changed

Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -244,32 +244,59 @@ struct TypeMatcher {
244244
/// - Parameters:
245245
/// - schema: The schema to check.
246246
/// - components: The OpenAPI components for looking up references.
247+
/// - cache: Memoised optionality by reference.
247248
/// - Throws: An error if there's an issue while checking the schema.
248249
/// - Returns: `true` if the schema is optional, `false` otherwise.
249-
func isOptional(_ schema: JSONSchema, components: OpenAPI.Components) throws -> Bool {
250+
func isOptional(_ schema: JSONSchema, components: OpenAPI.Components, cache: [JSONReference<JSONSchema>: Bool] = [:]) throws -> Bool {
250251
if schema.nullable || !schema.required { return true }
251-
guard case .reference(let ref, _) = schema.value else { return false }
252-
let targetSchema = try components.lookup(ref)
253-
return try isOptional(targetSchema, components: components)
252+
switch schema.value {
253+
case .null(_):
254+
return true
255+
case .reference(let ref, _):
256+
return try isOptional(ref, components: components, cache: cache)
257+
case .one(of: let schemas, core: _):
258+
return try schemas.contains(where: { try isOptional($0, components: components, cache: cache) })
259+
default:
260+
return schema.nullable
261+
}
254262
}
255263

256264
/// Returns a Boolean value indicating whether the schema is optional.
257265
/// - Parameters:
258266
/// - schema: The schema to check.
259267
/// - components: The OpenAPI components for looking up references.
268+
/// - cache: Memoised optionality by reference.
260269
/// - Throws: An error if there's an issue while checking the schema.
261270
/// - Returns: `true` if the schema is optional, `false` otherwise.
262-
func isOptional(_ schema: UnresolvedSchema?, components: OpenAPI.Components) throws -> Bool {
271+
func isOptional(_ schema: UnresolvedSchema?, components: OpenAPI.Components, cache: [JSONReference<JSONSchema>: Bool] = [:]) throws -> Bool {
263272
guard let schema else {
264273
// A nil unresolved schema represents a non-optional fragment.
265274
return false
266275
}
267276
switch schema {
268277
case .a(let ref):
269-
let targetSchema = try components.lookup(ref)
270-
return try isOptional(targetSchema, components: components)
271-
case .b(let schema): return try isOptional(schema, components: components)
278+
return try isOptional(ref.jsonReference, components: components, cache: cache)
279+
case .b(let schema): return try isOptional(schema, components: components, cache: cache)
280+
}
281+
}
282+
283+
/// Returns a Boolean value indicating whether the referenced schema is optional.
284+
/// - Parameters:
285+
/// - schema: The reference to check.
286+
/// - components: The OpenAPI components for looking up references.
287+
/// - cache: Memoised optionality by reference.
288+
/// - Throws: An error if there's an issue while checking the schema.
289+
/// - Returns: `true` if the schema is optional, `false` otherwise.
290+
func isOptional(_ ref: JSONReference<JSONSchema>, components: OpenAPI.Components, cache: [JSONReference<JSONSchema>: Bool] = [:]) throws -> Bool {
291+
if let result = cache[ref] {
292+
return result
272293
}
294+
let targetSchema = try components.lookup(ref)
295+
var cache = cache
296+
cache[ref] = false // Pre-cache to treat directly recursive types as non-nullable.
297+
let result = try isOptional(targetSchema, components: components, cache: cache)
298+
cache[ref] = result
299+
return result
273300
}
274301

275302
// MARK: - Private

Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ final class Test_TypeMatcher: Test_Core {
216216
}
217217

218218
static let optionalTestCases: [(JSONSchema, Bool)] = [
219+
// Explicit null.
220+
(.null(), true),
219221

220222
// A required string.
221223
(.string, false), (.string(required: true, nullable: false), false),
@@ -227,10 +229,26 @@ final class Test_TypeMatcher: Test_Core {
227229
// A reference pointing to a required schema.
228230
(.reference(.component(named: "RequiredString")), false),
229231
(.reference(.component(named: "NullableString")), true),
232+
233+
// Unknown type.
234+
(.fragment(), false),
235+
(.fragment(nullable: true), true),
236+
237+
// References.
238+
(.reference(.component(named: "List")), true),
239+
(.reference(.component(named: "Loop")), false),
230240
]
231241
func testOptionalSchemas() throws {
232242
let components = OpenAPI.Components(schemas: [
233243
"RequiredString": .string, "NullableString": .string(nullable: true),
244+
// Singlely linked list where null is an empty list.
245+
"List": .one(of: [
246+
.null(),
247+
.object(properties: ["next": .reference(.component(named: "List"),
248+
required: true)])]),
249+
// A non-empty circular linked list.
250+
"Loop": .object(properties: ["next": .reference(.component(named: "Loop"),
251+
required: true)]),
234252
])
235253
for (schema, expectedIsOptional) in Self.optionalTestCases {
236254
let actualIsOptional = try typeMatcher.isOptional(schema, components: components)

0 commit comments

Comments
 (0)