Skip to content

Commit e6134b4

Browse files
authored
[Bug] Fix multipart schema inference for allOf/anyOf/oneOf of primitive types and non-binary arrays (#391)
[Bug] Fix multipart schema inference for allOf/anyOf/oneOf of primitive types and non-binary arrays ### Motivation As I started testing the multipart generation on real-world projects, I discovered two bugs: - allOf/anyOf/oneOf of primitive types (such as string) were encoded as JSON instead of a raw string (aka HTTPBody), which was wrong - arrays of non-binary and arrays of binary elements were treated inconsistently ### Modifications Fixed the bug by refactoring the inferrence logic a bit. ### Result Now e.g. an anyOf of a string still gets encoded as a primitive type, not JSON. ### Test Plan Added a unit test for this logic with a few test cases, easier to debug this way. Reviewed by: simonjbeaumont Builds: ✔︎ pull request validation (5.10) - Build finished. ✔︎ pull request validation (5.8) - Build finished. ✔︎ pull request validation (5.9) - Build finished. ✔︎ pull request validation (compatibility test) - 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. #391
1 parent ad4060b commit e6134b4

File tree

2 files changed

+134
-23
lines changed

2 files changed

+134
-23
lines changed

Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContentInspector.swift

Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -257,31 +257,52 @@ extension FileTranslator {
257257
default: return .infer(.primitive)
258258
}
259259
}
260-
let repetitionKind: MultipartPartInfo.RepetitionKind
261-
let candidateSource: MultipartPartInfo.ContentTypeSource
262-
switch try schema.dereferenced(in: components) {
263-
case .null, .not: return nil
264-
case .boolean, .number, .integer:
265-
repetitionKind = .single
266-
candidateSource = .infer(.primitive)
267-
case .string(_, let context):
268-
repetitionKind = .single
269-
candidateSource = try inferStringContent(context)
270-
case .object, .all, .one, .any, .fragment:
271-
repetitionKind = .single
272-
candidateSource = .infer(.complex)
273-
case .array(_, let context):
274-
repetitionKind = .array
275-
if let items = context.items {
276-
switch items {
277-
case .null, .not: return nil
278-
case .boolean, .number, .integer: candidateSource = .infer(.primitive)
279-
case .string(_, let context): candidateSource = try inferStringContent(context)
280-
case .object, .all, .one, .any, .fragment, .array: candidateSource = .infer(.complex)
281-
}
282-
} else {
260+
func inferAllOfAnyOfOneOf(_ schemas: [DereferencedJSONSchema]) throws -> MultipartPartInfo.ContentTypeSource? {
261+
// If all schemas are primitive, the allOf/anyOf/oneOf is also primitive.
262+
// These cannot be binary, so only primitive vs complex.
263+
for schema in schemas {
264+
guard let (_, kind) = try inferSchema(schema) else { return nil }
265+
guard case .infer(.primitive) = kind else { return kind }
266+
}
267+
return .infer(.primitive)
268+
}
269+
func inferSchema(_ schema: DereferencedJSONSchema) throws -> (
270+
MultipartPartInfo.RepetitionKind, MultipartPartInfo.ContentTypeSource
271+
)? {
272+
let repetitionKind: MultipartPartInfo.RepetitionKind
273+
let candidateSource: MultipartPartInfo.ContentTypeSource
274+
switch schema {
275+
case .null, .not: return nil
276+
case .boolean, .number, .integer:
277+
repetitionKind = .single
278+
candidateSource = .infer(.primitive)
279+
case .string(_, let context):
280+
repetitionKind = .single
281+
candidateSource = try inferStringContent(context)
282+
case .object, .fragment:
283+
repetitionKind = .single
283284
candidateSource = .infer(.complex)
285+
case .all(of: let schemas, _), .one(of: let schemas, _), .any(of: let schemas, _):
286+
repetitionKind = .single
287+
guard let value = try inferAllOfAnyOfOneOf(schemas) else { return nil }
288+
candidateSource = value
289+
case .array(_, let context):
290+
repetitionKind = .array
291+
if let items = context.items {
292+
switch items {
293+
case .null, .not: return nil
294+
case .boolean, .number, .integer: candidateSource = .infer(.primitive)
295+
case .string(_, let context): candidateSource = try inferStringContent(context)
296+
case .object, .all, .one, .any, .fragment, .array: candidateSource = .infer(.complex)
297+
}
298+
} else {
299+
candidateSource = .infer(.complex)
300+
}
284301
}
302+
return (repetitionKind, candidateSource)
303+
}
304+
guard let (repetitionKind, candidateSource) = try inferSchema(schema.dereferenced(in: components)) else {
305+
return nil
285306
}
286307
let finalContentTypeSource: MultipartPartInfo.ContentTypeSource
287308
if let encoding, let contentType = encoding.contentType {
@@ -301,9 +322,23 @@ extension FileTranslator {
301322
let resolvedSchema: JSONSchema
302323
if isOptional { resolvedSchema = baseSchema.optionalSchemaObject() } else { resolvedSchema = baseSchema }
303324
return (info, resolvedSchema)
325+
} else if repetitionKind == .array {
326+
let isOptional = try typeMatcher.isOptional(schema, components: components)
327+
guard case .array(_, let context) = schema.value else {
328+
preconditionFailure("Array repetition should always use an array schema.")
329+
}
330+
let elementSchema: JSONSchema = context.items ?? .fragment
331+
let resolvedSchema: JSONSchema
332+
if isOptional {
333+
resolvedSchema = elementSchema.optionalSchemaObject()
334+
} else {
335+
resolvedSchema = elementSchema
336+
}
337+
return (info, resolvedSchema)
304338
}
305339
return (info, schema)
306340
}
341+
307342
/// Parses the names of component schemas used by multipart request and response bodies.
308343
///
309344
/// The result is used to inform how a schema is generated.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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 OpenAPIKit
16+
@testable import _OpenAPIGeneratorCore
17+
18+
class Test_MultipartContentInspector: Test_Core {
19+
func testSerializationStrategy() throws {
20+
let translator = makeTypesTranslator()
21+
func _test(
22+
schemaIn: JSONSchema,
23+
encoding: OpenAPI.Content.Encoding? = nil,
24+
source: MultipartPartInfo.ContentTypeSource,
25+
repetition: MultipartPartInfo.RepetitionKind,
26+
schemaOut: JSONSchema,
27+
file: StaticString = #file,
28+
line: UInt = #line
29+
) throws {
30+
let (info, actualSchemaOut) = try XCTUnwrap(
31+
translator.parseMultipartPartInfo(schema: schemaIn, encoding: encoding, foundIn: "")
32+
)
33+
XCTAssertEqual(info.repetition, repetition, file: file, line: line)
34+
XCTAssertEqual(info.contentTypeSource, source, file: file, line: line)
35+
XCTAssertEqual(actualSchemaOut, schemaOut, file: file, line: line)
36+
}
37+
try _test(schemaIn: .object, source: .infer(.complex), repetition: .single, schemaOut: .object)
38+
try _test(schemaIn: .array(items: .object), source: .infer(.complex), repetition: .array, schemaOut: .object)
39+
try _test(
40+
schemaIn: .string,
41+
source: .infer(.primitive),
42+
repetition: .single,
43+
schemaOut: .string(contentEncoding: .binary)
44+
)
45+
try _test(
46+
schemaIn: .integer,
47+
source: .infer(.primitive),
48+
repetition: .single,
49+
schemaOut: .string(contentEncoding: .binary)
50+
)
51+
try _test(
52+
schemaIn: .boolean,
53+
source: .infer(.primitive),
54+
repetition: .single,
55+
schemaOut: .string(contentEncoding: .binary)
56+
)
57+
try _test(
58+
schemaIn: .string(allowedValues: ["foo"]),
59+
source: .infer(.primitive),
60+
repetition: .single,
61+
schemaOut: .string(contentEncoding: .binary)
62+
)
63+
try _test(
64+
schemaIn: .array(items: .string),
65+
source: .infer(.primitive),
66+
repetition: .array,
67+
schemaOut: .string(contentEncoding: .binary)
68+
)
69+
try _test(
70+
schemaIn: .any(of: .string, .string(allowedValues: ["foo"])),
71+
source: .infer(.primitive),
72+
repetition: .single,
73+
schemaOut: .string(contentEncoding: .binary)
74+
)
75+
}
76+
}

0 commit comments

Comments
 (0)