From b97f18dda70345312ffcb4915db5a083fd37ee05 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 22 Jul 2024 16:42:42 +0200 Subject: [PATCH] Fix multipart + additionalProperties + string support --- .gitignore | 1 + .../Multipart/MultipartContentInspector.swift | 18 +-- .../StructureHelpers.swift | 2 +- .../FileBasedReferenceTests.swift | 15 +- .../SnippetBasedReferenceTests.swift | 136 +++++++++++++++++- 5 files changed, 155 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index f6f5465e..c01c56a8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ DerivedData/ /Package.resolved .ci/ .docc-build/ +.swiftpm diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContentInspector.swift b/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContentInspector.swift index 804fd3aa..b9edc3b8 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContentInspector.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContentInspector.swift @@ -148,14 +148,6 @@ extension FileTranslator { case .disallowed: break case .allowed: parts.append(.undocumented) case .typed(let schema): - let typeUsage = try typeAssigner.typeUsage( - usingNamingHint: Constants.AdditionalProperties.variableName, - withSchema: .b(schema), - components: components, - inParent: typeName - )! - // The unwrap is safe, the method only returns nil when the input schema is nil. - let typeName = typeUsage.typeName guard let (info, resolvedSchema) = try parseMultipartPartInfo( schema: schema, @@ -167,7 +159,15 @@ extension FileTranslator { message: "Failed to parse multipart info for additionalProperties in \(typeName.description)." ) } - parts.append(.otherDynamicallyNamed(.init(typeName: typeName, partInfo: info, schema: resolvedSchema))) + let partTypeUsage = try typeAssigner.typeUsage( + usingNamingHint: Constants.AdditionalProperties.variableName, + withSchema: .b(resolvedSchema), + components: components, + inParent: typeName + )! + // The unwrap is safe, the method only returns nil when the input schema is nil. + let partTypeName = partTypeUsage.typeName + parts.append(.otherDynamicallyNamed(.init(typeName: partTypeName, partInfo: info, schema: resolvedSchema))) case .any: parts.append(.otherRaw) } let requirements = try parseMultipartRequirements( diff --git a/Tests/OpenAPIGeneratorCoreTests/StructureHelpers.swift b/Tests/OpenAPIGeneratorCoreTests/StructureHelpers.swift index c25697b7..fc40833b 100644 --- a/Tests/OpenAPIGeneratorCoreTests/StructureHelpers.swift +++ b/Tests/OpenAPIGeneratorCoreTests/StructureHelpers.swift @@ -154,7 +154,7 @@ extension BindingKind { } } -extension Expression { +extension _OpenAPIGeneratorCore.Expression { var info: ExprInfo { switch self { case .literal(let value): return .init(name: value.name, kind: .literal) diff --git a/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift index 4e25084f..1e9e6786 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift @@ -151,14 +151,17 @@ extension FileBasedReferenceTests { ) } - private func temporaryDirectory(fileManager: FileManager = .default) throws -> URL { - let directoryURL = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) - try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true) + private func temporaryDirectory() throws -> URL { + let directoryURL = FileManager.default.temporaryDirectory.appendingPathComponent( + UUID().uuidString, + isDirectory: true + ) + try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true) addTeardownBlock { do { - if fileManager.fileExists(atPath: directoryURL.path) { - try fileManager.removeItem(at: directoryURL) - XCTAssertFalse(fileManager.fileExists(atPath: directoryURL.path)) + if FileManager.default.fileExists(atPath: directoryURL.path) { + try FileManager.default.removeItem(at: directoryURL) + XCTAssertFalse(FileManager.default.fileExists(atPath: directoryURL.path)) } } catch { // Treat any errors during file deletion as a test failure. diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index 5d454d96..2117e527 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -4457,6 +4457,140 @@ final class SnippetBasedReferenceTests: XCTestCase { ) } + func testRequestMultipartBodyAdditionalPropertiesSchemaBuiltin() throws { + try self.assertRequestInTypesClientServerTranslation( + """ + /foo: + post: + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + additionalProperties: + type: string + responses: + default: + description: Response + """, + types: """ + public struct Input: Sendable, Hashable { + @frozen public enum Body: Sendable, Hashable { + @frozen public enum multipartFormPayload: Sendable, Hashable { + case additionalProperties(OpenAPIRuntime.MultipartDynamicallyNamedPart) + } + case multipartForm(OpenAPIRuntime.MultipartBody) + } + public var body: Operations.post_sol_foo.Input.Body + public init(body: Operations.post_sol_foo.Input.Body) { + self.body = body + } + } + """, + client: """ + { input in + let path = try converter.renderedPath( + template: "/foo", + parameters: [] + ) + var request: HTTPTypes.HTTPRequest = .init( + soar_path: path, + method: .post + ) + suppressMutabilityWarning(&request) + let body: OpenAPIRuntime.HTTPBody? + switch input.body { + case let .multipartForm(value): + body = try converter.setRequiredRequestBodyAsMultipart( + value, + headerFields: &request.headerFields, + contentType: "multipart/form-data", + allowsUnknownParts: true, + requiredExactlyOncePartNames: [], + requiredAtLeastOncePartNames: [], + atMostOncePartNames: [], + zeroOrMoreTimesPartNames: [], + encoding: { part in + switch part { + case let .additionalProperties(wrapped): + var headerFields: HTTPTypes.HTTPFields = .init() + let value = wrapped.payload + let body = try converter.setRequiredRequestBodyAsBinary( + value, + headerFields: &headerFields, + contentType: "text/plain" + ) + return .init( + name: wrapped.name, + filename: wrapped.filename, + headerFields: headerFields, + body: body + ) + } + } + ) + } + return (request, body) + } + """, + server: """ + { request, requestBody, metadata in + let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) + let body: Operations.post_sol_foo.Input.Body + let chosenContentType = try converter.bestContentType( + received: contentType, + options: [ + "multipart/form-data" + ] + ) + switch chosenContentType { + case "multipart/form-data": + body = try converter.getRequiredRequestBodyAsMultipart( + OpenAPIRuntime.MultipartBody.self, + from: requestBody, + transforming: { value in + .multipartForm(value) + }, + boundary: contentType.requiredBoundary(), + allowsUnknownParts: true, + requiredExactlyOncePartNames: [], + requiredAtLeastOncePartNames: [], + atMostOncePartNames: [], + zeroOrMoreTimesPartNames: [], + decoding: { part in + let headerFields = part.headerFields + let (name, filename) = try converter.extractContentDispositionNameAndFilename(in: headerFields) + switch name { + default: + try converter.verifyContentTypeIfPresent( + in: headerFields, + matches: "text/plain" + ) + let body = try converter.getRequiredRequestBodyAsBinary( + OpenAPIRuntime.HTTPBody.self, + from: part.body, + transforming: { + $0 + } + ) + return .additionalProperties(.init( + payload: body, + filename: filename, + name: name + )) + } + } + ) + default: + preconditionFailure("bestContentType chose an invalid content type.") + } + return Operations.post_sol_foo.Input(body: body) + } + """ + ) + } + func testResponseMultipartReferencedResponse() throws { try self.assertResponseInTypesClientServerTranslation( """ @@ -5211,7 +5345,7 @@ private func XCTAssertSwiftEquivalent( } private func XCTAssertSwiftEquivalent( - _ expression: Expression, + _ expression: _OpenAPIGeneratorCore.Expression, _ expectedSwift: String, file: StaticString = #filePath, line: UInt = #line