Skip to content

Commit db5d1ea

Browse files
authored
Fix multipart + additionalProperties + string support (#597)
1 parent 23d93f1 commit db5d1ea

File tree

5 files changed

+155
-17
lines changed

5 files changed

+155
-17
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ DerivedData/
99
/Package.resolved
1010
.ci/
1111
.docc-build/
12+
.swiftpm

Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContentInspector.swift

+9-9
Original file line numberDiff line numberDiff line change
@@ -148,14 +148,6 @@ extension FileTranslator {
148148
case .disallowed: break
149149
case .allowed: parts.append(.undocumented)
150150
case .typed(let schema):
151-
let typeUsage = try typeAssigner.typeUsage(
152-
usingNamingHint: Constants.AdditionalProperties.variableName,
153-
withSchema: .b(schema),
154-
components: components,
155-
inParent: typeName
156-
)!
157-
// The unwrap is safe, the method only returns nil when the input schema is nil.
158-
let typeName = typeUsage.typeName
159151
guard
160152
let (info, resolvedSchema) = try parseMultipartPartInfo(
161153
schema: schema,
@@ -167,7 +159,15 @@ extension FileTranslator {
167159
message: "Failed to parse multipart info for additionalProperties in \(typeName.description)."
168160
)
169161
}
170-
parts.append(.otherDynamicallyNamed(.init(typeName: typeName, partInfo: info, schema: resolvedSchema)))
162+
let partTypeUsage = try typeAssigner.typeUsage(
163+
usingNamingHint: Constants.AdditionalProperties.variableName,
164+
withSchema: .b(resolvedSchema),
165+
components: components,
166+
inParent: typeName
167+
)!
168+
// The unwrap is safe, the method only returns nil when the input schema is nil.
169+
let partTypeName = partTypeUsage.typeName
170+
parts.append(.otherDynamicallyNamed(.init(typeName: partTypeName, partInfo: info, schema: resolvedSchema)))
171171
case .any: parts.append(.otherRaw)
172172
}
173173
let requirements = try parseMultipartRequirements(

Tests/OpenAPIGeneratorCoreTests/StructureHelpers.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ extension BindingKind {
154154
}
155155
}
156156

157-
extension Expression {
157+
extension _OpenAPIGeneratorCore.Expression {
158158
var info: ExprInfo {
159159
switch self {
160160
case .literal(let value): return .init(name: value.name, kind: .literal)

Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift

+9-6
Original file line numberDiff line numberDiff line change
@@ -151,14 +151,17 @@ extension FileBasedReferenceTests {
151151
)
152152
}
153153

154-
private func temporaryDirectory(fileManager: FileManager = .default) throws -> URL {
155-
let directoryURL = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
156-
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
154+
private func temporaryDirectory() throws -> URL {
155+
let directoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(
156+
UUID().uuidString,
157+
isDirectory: true
158+
)
159+
try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true)
157160
addTeardownBlock {
158161
do {
159-
if fileManager.fileExists(atPath: directoryURL.path) {
160-
try fileManager.removeItem(at: directoryURL)
161-
XCTAssertFalse(fileManager.fileExists(atPath: directoryURL.path))
162+
if FileManager.default.fileExists(atPath: directoryURL.path) {
163+
try FileManager.default.removeItem(at: directoryURL)
164+
XCTAssertFalse(FileManager.default.fileExists(atPath: directoryURL.path))
162165
}
163166
} catch {
164167
// Treat any errors during file deletion as a test failure.

Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift

+135-1
Original file line numberDiff line numberDiff line change
@@ -4567,6 +4567,140 @@ final class SnippetBasedReferenceTests: XCTestCase {
45674567
)
45684568
}
45694569

4570+
func testRequestMultipartBodyAdditionalPropertiesSchemaBuiltin() throws {
4571+
try self.assertRequestInTypesClientServerTranslation(
4572+
"""
4573+
/foo:
4574+
post:
4575+
requestBody:
4576+
required: true
4577+
content:
4578+
multipart/form-data:
4579+
schema:
4580+
type: object
4581+
additionalProperties:
4582+
type: string
4583+
responses:
4584+
default:
4585+
description: Response
4586+
""",
4587+
types: """
4588+
public struct Input: Sendable, Hashable {
4589+
@frozen public enum Body: Sendable, Hashable {
4590+
@frozen public enum multipartFormPayload: Sendable, Hashable {
4591+
case additionalProperties(OpenAPIRuntime.MultipartDynamicallyNamedPart<OpenAPIRuntime.HTTPBody>)
4592+
}
4593+
case multipartForm(OpenAPIRuntime.MultipartBody<Operations.post_sol_foo.Input.Body.multipartFormPayload>)
4594+
}
4595+
public var body: Operations.post_sol_foo.Input.Body
4596+
public init(body: Operations.post_sol_foo.Input.Body) {
4597+
self.body = body
4598+
}
4599+
}
4600+
""",
4601+
client: """
4602+
{ input in
4603+
let path = try converter.renderedPath(
4604+
template: "/foo",
4605+
parameters: []
4606+
)
4607+
var request: HTTPTypes.HTTPRequest = .init(
4608+
soar_path: path,
4609+
method: .post
4610+
)
4611+
suppressMutabilityWarning(&request)
4612+
let body: OpenAPIRuntime.HTTPBody?
4613+
switch input.body {
4614+
case let .multipartForm(value):
4615+
body = try converter.setRequiredRequestBodyAsMultipart(
4616+
value,
4617+
headerFields: &request.headerFields,
4618+
contentType: "multipart/form-data",
4619+
allowsUnknownParts: true,
4620+
requiredExactlyOncePartNames: [],
4621+
requiredAtLeastOncePartNames: [],
4622+
atMostOncePartNames: [],
4623+
zeroOrMoreTimesPartNames: [],
4624+
encoding: { part in
4625+
switch part {
4626+
case let .additionalProperties(wrapped):
4627+
var headerFields: HTTPTypes.HTTPFields = .init()
4628+
let value = wrapped.payload
4629+
let body = try converter.setRequiredRequestBodyAsBinary(
4630+
value,
4631+
headerFields: &headerFields,
4632+
contentType: "text/plain"
4633+
)
4634+
return .init(
4635+
name: wrapped.name,
4636+
filename: wrapped.filename,
4637+
headerFields: headerFields,
4638+
body: body
4639+
)
4640+
}
4641+
}
4642+
)
4643+
}
4644+
return (request, body)
4645+
}
4646+
""",
4647+
server: """
4648+
{ request, requestBody, metadata in
4649+
let contentType = converter.extractContentTypeIfPresent(in: request.headerFields)
4650+
let body: Operations.post_sol_foo.Input.Body
4651+
let chosenContentType = try converter.bestContentType(
4652+
received: contentType,
4653+
options: [
4654+
"multipart/form-data"
4655+
]
4656+
)
4657+
switch chosenContentType {
4658+
case "multipart/form-data":
4659+
body = try converter.getRequiredRequestBodyAsMultipart(
4660+
OpenAPIRuntime.MultipartBody<Operations.post_sol_foo.Input.Body.multipartFormPayload>.self,
4661+
from: requestBody,
4662+
transforming: { value in
4663+
.multipartForm(value)
4664+
},
4665+
boundary: contentType.requiredBoundary(),
4666+
allowsUnknownParts: true,
4667+
requiredExactlyOncePartNames: [],
4668+
requiredAtLeastOncePartNames: [],
4669+
atMostOncePartNames: [],
4670+
zeroOrMoreTimesPartNames: [],
4671+
decoding: { part in
4672+
let headerFields = part.headerFields
4673+
let (name, filename) = try converter.extractContentDispositionNameAndFilename(in: headerFields)
4674+
switch name {
4675+
default:
4676+
try converter.verifyContentTypeIfPresent(
4677+
in: headerFields,
4678+
matches: "text/plain"
4679+
)
4680+
let body = try converter.getRequiredRequestBodyAsBinary(
4681+
OpenAPIRuntime.HTTPBody.self,
4682+
from: part.body,
4683+
transforming: {
4684+
$0
4685+
}
4686+
)
4687+
return .additionalProperties(.init(
4688+
payload: body,
4689+
filename: filename,
4690+
name: name
4691+
))
4692+
}
4693+
}
4694+
)
4695+
default:
4696+
preconditionFailure("bestContentType chose an invalid content type.")
4697+
}
4698+
return Operations.post_sol_foo.Input(body: body)
4699+
}
4700+
"""
4701+
)
4702+
}
4703+
45704704
func testResponseMultipartReferencedResponse() throws {
45714705
try self.assertResponseInTypesClientServerTranslation(
45724706
"""
@@ -5323,7 +5457,7 @@ private func XCTAssertSwiftEquivalent(
53235457
}
53245458

53255459
private func XCTAssertSwiftEquivalent(
5326-
_ expression: Expression,
5460+
_ expression: _OpenAPIGeneratorCore.Expression,
53275461
_ expectedSwift: String,
53285462
file: StaticString = #filePath,
53295463
line: UInt = #line

0 commit comments

Comments
 (0)