Skip to content

Commit f285dab

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

File tree

2 files changed

+83
-6
lines changed

2 files changed

+83
-6
lines changed

Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -247,10 +247,29 @@ struct TypeMatcher {
247247
/// - Throws: An error if there's an issue while checking the schema.
248248
/// - Returns: `true` if the schema is optional, `false` otherwise.
249249
func isOptional(_ schema: JSONSchema, components: OpenAPI.Components) throws -> Bool {
250+
var cache = [JSONReference<JSONSchema>: Bool]()
251+
return try isOptional(schema, components: components, cache: &cache)
252+
}
253+
254+
/// Returns a Boolean value indicating whether the schema is optional.
255+
/// - Parameters:
256+
/// - schema: The schema to check.
257+
/// - components: The OpenAPI components for looking up references.
258+
/// - cache: Memoised optionality by reference.
259+
/// - Throws: An error if there's an issue while checking the schema.
260+
/// - Returns: `true` if the schema is optional, `false` otherwise.
261+
func isOptional(_ schema: JSONSchema, components: OpenAPI.Components, cache: inout [JSONReference<JSONSchema>: Bool]) throws -> Bool {
250262
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)
263+
switch schema.value {
264+
case .null(_):
265+
return true
266+
case .reference(let ref, _):
267+
return try isOptional(ref, components: components, cache: &cache)
268+
case .one(of: let schemas, core: _):
269+
return try schemas.contains(where: { try isOptional($0, components: components, cache: &cache) })
270+
default:
271+
return schema.nullable
272+
}
254273
}
255274

256275
/// Returns a Boolean value indicating whether the schema is optional.
@@ -260,16 +279,56 @@ struct TypeMatcher {
260279
/// - Throws: An error if there's an issue while checking the schema.
261280
/// - Returns: `true` if the schema is optional, `false` otherwise.
262281
func isOptional(_ schema: UnresolvedSchema?, components: OpenAPI.Components) throws -> Bool {
282+
var cache = [JSONReference<JSONSchema>: Bool]()
283+
return try isOptional(schema, components: components, cache: &cache)
284+
}
285+
286+
/// Returns a Boolean value indicating whether the schema is optional.
287+
/// - Parameters:
288+
/// - schema: The schema to check.
289+
/// - components: The OpenAPI components for looking up references.
290+
/// - cache: Memoised optionality by reference.
291+
/// - Throws: An error if there's an issue while checking the schema.
292+
/// - Returns: `true` if the schema is optional, `false` otherwise.
293+
func isOptional(_ schema: UnresolvedSchema?, components: OpenAPI.Components, cache: inout [JSONReference<JSONSchema>: Bool]) throws -> Bool {
263294
guard let schema else {
264295
// A nil unresolved schema represents a non-optional fragment.
265296
return false
266297
}
267298
switch schema {
268299
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)
300+
return try isOptional(ref.jsonReference, components: components, cache: &cache)
301+
case .b(let schema): return try isOptional(schema, components: components, cache: &cache)
302+
}
303+
}
304+
305+
/// Returns a Boolean value indicating whether the referenced schema is optional.
306+
/// - Parameters:
307+
/// - schema: The reference to check.
308+
/// - components: The OpenAPI components for looking up references.
309+
/// - Throws: An error if there's an issue while checking the schema.
310+
/// - Returns: `true` if the schema is optional, `false` otherwise.
311+
func isOptional(_ ref: JSONReference<JSONSchema>, components: OpenAPI.Components) throws -> Bool {
312+
var cache = [JSONReference<JSONSchema>: Bool]()
313+
return try isOptional(ref, components: components, cache: &cache)
314+
}
315+
316+
/// Returns a Boolean value indicating whether the referenced schema is optional.
317+
/// - Parameters:
318+
/// - schema: The reference to check.
319+
/// - components: The OpenAPI components for looking up references.
320+
/// - cache: Memoised optionality by reference.
321+
/// - Throws: An error if there's an issue while checking the schema.
322+
/// - Returns: `true` if the schema is optional, `false` otherwise.
323+
func isOptional(_ ref: JSONReference<JSONSchema>, components: OpenAPI.Components, cache: inout [JSONReference<JSONSchema>: Bool]) throws -> Bool {
324+
if let result = cache[ref] {
325+
return result
272326
}
327+
let targetSchema = try components.lookup(ref)
328+
cache[ref] = false // Pre-cache to treat directly recursive types as non-nullable.
329+
let result = try isOptional(targetSchema, components: components, cache: &cache)
330+
cache[ref] = result
331+
return result
273332
}
274333

275334
// 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)