diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStructBlueprint.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStructBlueprint.swift index 9f0bbef2..08af0bc4 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStructBlueprint.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStructBlueprint.swift @@ -28,8 +28,12 @@ extension FileTranslator { let propertyDecls = allProperties.flatMap(translatePropertyBlueprint) var members = propertyDecls - let initDecl = translateStructBlueprintInitializer(typeName: typeName, properties: allProperties) - members.append(initDecl) + let initializers = translateStructBlueprintInitializers( + typeName: typeName, + properties: allProperties, + initializerContext: blueprint.initializerContext + ) + members.append(contentsOf: initializers) if blueprint.shouldGenerateCodingKeys && !serializableProperties.isEmpty { let codingKeysDecl = translateStructBlueprintCodingKeys(properties: serializableProperties) @@ -60,13 +64,46 @@ extension FileTranslator { return .commentable(blueprint.comment, .struct(structDesc).deprecate(if: blueprint.isDeprecated)) } - /// Returns a declaration of an initializer declared in a structure. + /// Returns declarations of initializers for a structure. /// - Parameters: /// - typeName: The type name of the structure. /// - properties: The properties to include in the initializer. - /// - Returns: A `Declaration` representing the translated struct. - func translateStructBlueprintInitializer(typeName: TypeName, properties: [PropertyBlueprint]) -> Declaration { + /// - initializerContext: Context that determines what initializers to generate. + /// - Returns: An array of `Declaration` representing the initializers. + func translateStructBlueprintInitializers( + typeName: TypeName, + properties: [PropertyBlueprint], + initializerContext: StructBlueprint.InitializerContext + ) -> [Declaration] { + var initializers: [Declaration] = [] + + // Always include the memberwise initializer + let memberwiseInit = translateMemberwiseInitializer(typeName: typeName, properties: properties) + initializers.append(memberwiseInit) + + // Add context-specific initializers + switch initializerContext { + case .memberwise: + break // No additional initializers + case .multipartPayload(let originalSchema): + if let valueInit = translateMultipartValueInitializer( + typeName: typeName, + properties: properties, + originalSchema: originalSchema + ) { + initializers.append(valueInit) + } + } + return initializers + } + + /// Returns a declaration of the memberwise initializer for a structure. + /// - Parameters: + /// - typeName: The type name of the structure. + /// - properties: The properties to include in the initializer. + /// - Returns: A `Declaration` representing the memberwise initializer. + func translateMemberwiseInitializer(typeName: TypeName, properties: [PropertyBlueprint]) -> Declaration { let comment: Comment = properties.initializerComment(typeName: typeName.shortSwiftName) let decls: [(ParameterDescription, String)] = properties.map { property in @@ -95,6 +132,110 @@ extension FileTranslator { ) } + /// Returns a value initializer for multipart payload structs with primitive types. + /// - Parameters: + /// - typeName: The type name of the structure. + /// - properties: The properties of the structure. + /// - originalSchema: The original schema before any transformations. + /// - Returns: A value initializer declaration if applicable, nil otherwise. + func translateMultipartValueInitializer( + typeName: TypeName, + properties: [PropertyBlueprint], + originalSchema: JSONSchema + ) -> Declaration? { + let typeMatcher = TypeMatcher(context: context) + guard let matchedType = typeMatcher.tryMatchBuiltinType(for: originalSchema.value) else { + return nil + } + + let valueTypeName = matchedType.typeName + let needsStringConversion: Bool + + switch originalSchema.value { + case .integer, .boolean, .number: + // For these types, if tryMatchBuiltinType succeeded, it means they are + // simple primitive types (e.g., Int, Bool, Double) and not enums. + // They will be converted to String for the HTTPBody. + needsStringConversion = true + case .string: + // For string schemas, we must ensure it's a plain Swift.String. + // Other string-based formats (Date, binary Data, base64 Data) + // are not handled by this specific "value" initializer. + guard valueTypeName == .string else { return nil } + needsStringConversion = false + case .object, .array, .all, .one, .any, .not, .reference, .fragment, .null: + // Other schema types are not supported for this value initializer, + // even if they might be considered "built-in" by the TypeMatcher + // for other purposes. + return nil + } + + // Find headers and body properties + let headersProperty = properties.first { $0.originalName == Constants.Operation.Output.Payload.Headers.variableName } + let bodyProperty = properties.first { $0.originalName == Constants.Operation.Body.variableName } + + // This initializer requires a body part to set the value. + guard bodyProperty != nil else { return nil } + + var parameters: [ParameterDescription] = [] + + // Add headers parameter if a headers property exists + if let headersProperty { + parameters.append(ParameterDescription( + label: headersProperty.swiftSafeName, + type: .init(headersProperty.typeUsage), + defaultValue: nil + )) + } + + // Add the main value parameter + parameters.append(ParameterDescription( + label: "value", + type: .init(valueTypeName.asUsage), + defaultValue: nil + )) + + let bodyContentExpression: Expression + if needsStringConversion { + bodyContentExpression = .functionCall( + calledExpression: .identifierType(TypeName.string), + arguments: [.init(label: nil, expression: .identifierPattern("value"))] + ) + } else { + bodyContentExpression = .identifierPattern("value") + } + + var bodyStatements: [CodeBlock] = [] + + // Assign headers if provided + if let headersProperty { + bodyStatements.append(.expression( + .assignment( + left: .identifierPattern("self").dot(headersProperty.swiftSafeName), + right: .identifierPattern(headersProperty.swiftSafeName) + ) + )) + } + + // Initialize the body property using the value + bodyStatements.append(.expression( + .assignment( + left: .identifierPattern("self").dot(Constants.Operation.Body.variableName), + right: .functionCall( + calledExpression: .identifierType(TypeName.runtime("HTTPBody")), + arguments: [.init(label: nil, expression: bodyContentExpression)] + ) + ) + )) + + return .function( + accessModifier: config.access, + kind: .initializer(failable: false), + parameters: parameters, + body: bodyStatements + ) + } + /// Returns a list of declarations for a specified property blueprint. /// /// May return multiple declarations when the property contains an unnamed diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/StructBlueprint.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/StructBlueprint.swift index 7a3d2260..a3db09b7 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/StructBlueprint.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/StructBlueprint.swift @@ -79,6 +79,18 @@ struct StructBlueprint { /// The properties of the structure. var properties: [PropertyBlueprint] + + /// Context information for generating additional initializers. + enum InitializerContext { + /// Standard struct with only the memberwise initializer. + case memberwise + + /// Multipart payload struct that may need value initializers. + case multipartPayload(originalSchema: JSONSchema) + } + + /// The context for initializer generation. + var initializerContext: InitializerContext = .memberwise } extension StructBlueprint { diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContent.swift b/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContent.swift index 290b3942..ac7a92f4 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContent.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContent.swift @@ -32,26 +32,33 @@ struct MultipartContent { /// A container of information about an individual multipart part. enum MultipartSchemaTypedContent { - /// The associated data with the `documentedTyped` case. - struct DocumentedTypeInfo { + /// A part that has a statically known name and type. + struct DocumentedMultipartPart { - /// The original name of the case from the OpenAPI document. + /// The name of the part as specified in the OpenAPI document. var originalName: String - /// The type name of the part wrapper. + /// The Swift type name that represents the part. var typeName: TypeName - /// Information about the kind of the part. + /// Information about the part's kind, content type, etc. var partInfo: MultipartPartInfo - /// The value schema of the part defined in the OpenAPI document. + /// The schema of the part. + /// + /// This schema might have been transformed by the `MultipartContentInspector` + /// (e.g., an integer might become a string with binary encoding). var schema: JSONSchema - /// The headers defined for the part in the OpenAPI document. + /// The original schema of the part, before any transformation by `MultipartContentInspector`. + var originalSchema: JSONSchema + + /// The headers of the part. var headers: OpenAPI.Header.Map? } - /// A documented part with a name specified in the OpenAPI document. - case documentedTyped(DocumentedTypeInfo) + + /// A documented part with a statically known name and type. + case documentedTyped(DocumentedMultipartPart) /// The associated data with the `otherDynamicallyNamed` case. struct OtherDynamicallyNamedInfo { diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContentInspector.swift b/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContentInspector.swift index b974190b..f74d33a9 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContentInspector.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContentInspector.swift @@ -139,6 +139,7 @@ extension FileTranslator { typeName: typeName, partInfo: info, schema: resolvedSchema, + originalSchema: value, headers: partEncoding?.headers ) ) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Multipart/translateMultipart.swift b/Sources/_OpenAPIGeneratorCore/Translator/Multipart/translateMultipart.swift index 61c839dd..8c945c46 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Multipart/translateMultipart.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Multipart/translateMultipart.swift @@ -37,7 +37,8 @@ extension TypesFileTranslator { typeName: TypeName, headers headerMap: OpenAPI.Header.Map?, contentType: ContentType, - schema: JSONSchema + schema: JSONSchema, + originalSchema: JSONSchema ) throws -> Declaration { let headersTypeName = typeName.appending( swiftComponent: Constants.Operation.Output.Payload.Headers.typeName, @@ -98,7 +99,8 @@ extension TypesFileTranslator { access: config.access, typeName: typeName, conformances: Constants.Operation.Output.Payload.conformances, - properties: [headersProperty, bodyProperty].compactMap { $0 } + properties: [headersProperty, bodyProperty].compactMap { $0 }, + initializerContext: .multipartPayload(originalSchema: originalSchema) ) ) return .commentable(typeName.docCommentWithUserDescription(nil), structDecl) @@ -144,7 +146,8 @@ extension TypesFileTranslator { typeName: documentedPart.typeName, headers: documentedPart.headers, contentType: documentedPart.partInfo.contentType, - schema: documentedPart.schema + schema: documentedPart.schema, + originalSchema: documentedPart.originalSchema ) return [decl, caseDecl] case .otherDynamicallyNamed(let dynamicallyNamedPart): diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/Builtins.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/Builtins.swift index ee51fbbd..9e234d5f 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/Builtins.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/Builtins.swift @@ -20,6 +20,12 @@ extension TypeName { /// Returns the type name for the Int type. static var int: Self { .swift("Int") } + /// Returns the type name for the Bool type. + static var bool: Self { .swift("Bool") } + + /// Returns the type name for the Double type. + static var double: Self { .swift("Double") } + /// Returns a type name for a type with the specified name in the /// Swift module. /// - Parameter name: The name of the type. diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift index cdcb3785..49d20168 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift @@ -1626,6 +1626,13 @@ public enum Components { self.headers = headers self.body = body } + public init( + headers: Components.RequestBodies.MultipartUploadTypedRequest.MultipartFormPayload.LogPayload.Headers, + value: Swift.String + ) { + self.headers = headers + self.body = OpenAPIRuntime.HTTPBody(value) + } } case log(OpenAPIRuntime.MultipartPart) /// - Remark: Generated from `#/components/requestBodies/MultipartUploadTypedRequest/multipartForm/metadata`. @@ -1780,6 +1787,13 @@ public enum Components { self.headers = headers self.body = body } + public init( + headers: Components.Responses.MultipartDownloadTypedResponse.Body.MultipartFormPayload.LogPayload.Headers, + value: Swift.String + ) { + self.headers = headers + self.body = OpenAPIRuntime.HTTPBody(value) + } } case log(OpenAPIRuntime.MultipartPart) /// - Remark: Generated from `#/components/responses/MultipartDownloadTypedResponse/content/multipartForm/metadata`. diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index b5b563bd..a7865296 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -2223,6 +2223,13 @@ final class SnippetBasedReferenceTests: XCTestCase { self.headers = headers self.body = body } + public init( + headers: Components.RequestBodies.MultipartUploadTypedRequest.multipartFormPayload.logPayload.Headers, + value: Swift.String + ) { + self.headers = headers + self.body = OpenAPIRuntime.HTTPBody(value) + } } case log(OpenAPIRuntime.MultipartPart) public struct metadataPayload: Sendable, Hashable { @@ -2257,6 +2264,143 @@ final class SnippetBasedReferenceTests: XCTestCase { ) } + func testComponentsRequestBodiesInline_multipart_typesafeInit() throws { + try self.assertRequestBodiesTranslation( + """ + requestBodies: + MultipartIntegerRequest: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + count: + type: integer + score: + type: integer + format: int32 + name: + type: string + items: + type: array + items: + type: integer + required: + - count + """, + #""" + public enum RequestBodies { + @frozen public enum MultipartIntegerRequest: Sendable, Hashable { + @frozen public enum multipartFormPayload: Sendable, Hashable { + public struct countPayload: Sendable, Hashable { + public var body: OpenAPIRuntime.HTTPBody + public init(body: OpenAPIRuntime.HTTPBody) { + self.body = body + } + public init(value: Swift.Int) { + self.body = OpenAPIRuntime.HTTPBody(Swift.String(value)) + } + } + case count(OpenAPIRuntime.MultipartPart) + public struct scorePayload: Sendable, Hashable { + public var body: OpenAPIRuntime.HTTPBody + public init(body: OpenAPIRuntime.HTTPBody) { + self.body = body + } + public init(value: Swift.Int32) { + self.body = OpenAPIRuntime.HTTPBody(Swift.String(value)) + } + } + case score(OpenAPIRuntime.MultipartPart) + public struct namePayload: Sendable, Hashable { + public var body: OpenAPIRuntime.HTTPBody + public init(body: OpenAPIRuntime.HTTPBody) { + self.body = body + } + public init(value: Swift.String) { + self.body = OpenAPIRuntime.HTTPBody(value) + } + } + case name(OpenAPIRuntime.MultipartPart) + public struct itemsPayload: Sendable, Hashable { + public var body: OpenAPIRuntime.HTTPBody + public init(body: OpenAPIRuntime.HTTPBody) { + self.body = body + } + } + case items(OpenAPIRuntime.MultipartPart) + case undocumented(OpenAPIRuntime.MultipartRawPart) + } + case multipartForm(OpenAPIRuntime.MultipartBody) + } + } + """# + ) + } + + func testComponentsRequestBodiesInline_multipart_typesafeInit_boolean() throws { + try self.assertRequestBodiesTranslation( + """ + requestBodies: + MultipartBooleanRequest: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + isActive: + type: boolean + hasFeature: + type: boolean + flags: + type: array + items: + type: boolean + required: + - isActive + """, + #""" + public enum RequestBodies { + @frozen public enum MultipartBooleanRequest: Sendable, Hashable { + @frozen public enum multipartFormPayload: Sendable, Hashable { + public struct isActivePayload: Sendable, Hashable { + public var body: OpenAPIRuntime.HTTPBody + public init(body: OpenAPIRuntime.HTTPBody) { + self.body = body + } + public init(value: Swift.Bool) { + self.body = OpenAPIRuntime.HTTPBody(Swift.String(value)) + } + } + case isActive(OpenAPIRuntime.MultipartPart) + public struct hasFeaturePayload: Sendable, Hashable { + public var body: OpenAPIRuntime.HTTPBody + public init(body: OpenAPIRuntime.HTTPBody) { + self.body = body + } + public init(value: Swift.Bool) { + self.body = OpenAPIRuntime.HTTPBody(Swift.String(value)) + } + } + case hasFeature(OpenAPIRuntime.MultipartPart) + public struct flagsPayload: Sendable, Hashable { + public var body: OpenAPIRuntime.HTTPBody + public init(body: OpenAPIRuntime.HTTPBody) { + self.body = body + } + } + case flags(OpenAPIRuntime.MultipartPart) + case undocumented(OpenAPIRuntime.MultipartRawPart) + } + case multipartForm(OpenAPIRuntime.MultipartBody) + } + } + """# + ) + } + func testPaths() throws { let paths = """ /healthOld: @@ -3498,6 +3642,9 @@ final class SnippetBasedReferenceTests: XCTestCase { public init(body: OpenAPIRuntime.HTTPBody) { self.body = body } + public init(value: Swift.String) { + self.body = OpenAPIRuntime.HTTPBody(value) + } } case log(OpenAPIRuntime.MultipartPart) case undocumented(OpenAPIRuntime.MultipartRawPart) @@ -3643,6 +3790,9 @@ final class SnippetBasedReferenceTests: XCTestCase { public init(body: OpenAPIRuntime.HTTPBody) { self.body = body } + public init(value: Swift.String) { + self.body = OpenAPIRuntime.HTTPBody(value) + } } case log(OpenAPIRuntime.MultipartPart) case undocumented(OpenAPIRuntime.MultipartRawPart) @@ -3963,6 +4113,9 @@ final class SnippetBasedReferenceTests: XCTestCase { public init(body: OpenAPIRuntime.HTTPBody) { self.body = body } + public init(value: Swift.String) { + self.body = OpenAPIRuntime.HTTPBody(value) + } } case log(OpenAPIRuntime.MultipartPart) case undocumented(OpenAPIRuntime.MultipartRawPart) @@ -4130,6 +4283,13 @@ final class SnippetBasedReferenceTests: XCTestCase { self.headers = headers self.body = body } + public init( + headers: Operations.post_sol_foo.Input.Body.multipartFormPayload.logPayload.Headers, + value: Swift.String + ) { + self.headers = headers + self.body = OpenAPIRuntime.HTTPBody(value) + } } case log(OpenAPIRuntime.MultipartPart) case undocumented(OpenAPIRuntime.MultipartRawPart) @@ -4150,6 +4310,9 @@ final class SnippetBasedReferenceTests: XCTestCase { public init(body: OpenAPIRuntime.HTTPBody) { self.body = body } + public init(value: Swift.String) { + self.body = OpenAPIRuntime.HTTPBody(value) + } } case log(OpenAPIRuntime.MultipartPart) case undocumented(OpenAPIRuntime.MultipartRawPart) @@ -4518,6 +4681,9 @@ final class SnippetBasedReferenceTests: XCTestCase { public init(body: OpenAPIRuntime.HTTPBody) { self.body = body } + public init(value: Swift.String) { + self.body = OpenAPIRuntime.HTTPBody(value) + } } case log(OpenAPIRuntime.MultipartPart) } @@ -4671,6 +4837,9 @@ final class SnippetBasedReferenceTests: XCTestCase { public init(body: OpenAPIRuntime.HTTPBody) { self.body = body } + public init(value: Swift.String) { + self.body = OpenAPIRuntime.HTTPBody(value) + } } case log(OpenAPIRuntime.MultipartPart) public struct additionalPropertiesPayload: Codable, Hashable, Sendable { @@ -4868,6 +5037,9 @@ final class SnippetBasedReferenceTests: XCTestCase { public init(body: OpenAPIRuntime.HTTPBody) { self.body = body } + public init(value: Swift.String) { + self.body = OpenAPIRuntime.HTTPBody(value) + } } case log(OpenAPIRuntime.MultipartPart) case additionalProperties(OpenAPIRuntime.MultipartDynamicallyNamedPart) @@ -5201,6 +5373,9 @@ final class SnippetBasedReferenceTests: XCTestCase { public init(body: OpenAPIRuntime.HTTPBody) { self.body = body } + public init(value: Swift.String) { + self.body = OpenAPIRuntime.HTTPBody(value) + } } case log(OpenAPIRuntime.MultipartPart) case undocumented(OpenAPIRuntime.MultipartRawPart) @@ -5372,6 +5547,9 @@ final class SnippetBasedReferenceTests: XCTestCase { public init(body: OpenAPIRuntime.HTTPBody) { self.body = body } + public init(value: Swift.String) { + self.body = OpenAPIRuntime.HTTPBody(value) + } } case log(OpenAPIRuntime.MultipartPart) case undocumented(OpenAPIRuntime.MultipartRawPart) @@ -5532,6 +5710,649 @@ final class SnippetBasedReferenceTests: XCTestCase { ) } + func testRequestMultipartBodyIntegerType() throws { + try self.assertRequestInTypesClientServerTranslation( + """ + /foo: + post: + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + count: + type: integer + required: + - count + additionalProperties: false + responses: + default: + description: Response + """, + input: """ + public struct Input: Sendable, Hashable { + @frozen public enum Body: Sendable, Hashable { + @frozen public enum multipartFormPayload: Sendable, Hashable { + public struct countPayload: Sendable, Hashable { + public var body: OpenAPIRuntime.HTTPBody + public init(body: OpenAPIRuntime.HTTPBody) { + self.body = body + } + public init(value: Swift.Int) { + self.body = OpenAPIRuntime.HTTPBody(Swift.String(value)) + } + } + case count(OpenAPIRuntime.MultipartPart) + } + 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: false, + requiredExactlyOncePartNames: [ + "count" + ], + requiredAtLeastOncePartNames: [], + atMostOncePartNames: [], + zeroOrMoreTimesPartNames: [], + encoding: { part in + switch part { + case let .count(wrapped): + var headerFields: HTTPTypes.HTTPFields = .init() + let value = wrapped.payload + let body = try converter.setRequiredRequestBodyAsBinary( + value.body, + headerFields: &headerFields, + contentType: "text/plain" + ) + return .init( + name: "count", + 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: false, + requiredExactlyOncePartNames: [ + "count" + ], + requiredAtLeastOncePartNames: [], + atMostOncePartNames: [], + zeroOrMoreTimesPartNames: [], + decoding: { part in + let headerFields = part.headerFields + let (name, filename) = try converter.extractContentDispositionNameAndFilename(in: headerFields) + switch name { + case "count": + try converter.verifyContentTypeIfPresent( + in: headerFields, + matches: "text/plain" + ) + let body = try converter.getRequiredRequestBodyAsBinary( + OpenAPIRuntime.HTTPBody.self, + from: part.body, + transforming: { + $0 + } + ) + return .count(.init( + payload: .init(body: body), + filename: filename + )) + default: + preconditionFailure("Unknown part should be rejected by multipart validation.") + } + } + ) + default: + preconditionFailure("bestContentType chose an invalid content type.") + } + return Operations.post_sol_foo.Input(body: body) + } + """ + ) + } + + func testRequestMultipartBodyBooleanType() throws { + try self.assertRequestInTypesClientServerTranslation( + """ + /foo: + post: + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + enabled: + type: boolean + required: + - enabled + additionalProperties: false + responses: + default: + description: Response + """, + input: """ + public struct Input: Sendable, Hashable { + @frozen public enum Body: Sendable, Hashable { + @frozen public enum multipartFormPayload: Sendable, Hashable { + public struct enabledPayload: Sendable, Hashable { + public var body: OpenAPIRuntime.HTTPBody + public init(body: OpenAPIRuntime.HTTPBody) { + self.body = body + } + public init(value: Swift.Bool) { + self.body = OpenAPIRuntime.HTTPBody(Swift.String(value)) + } + } + case enabled(OpenAPIRuntime.MultipartPart) + } + 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: false, + requiredExactlyOncePartNames: [ + "enabled" + ], + requiredAtLeastOncePartNames: [], + atMostOncePartNames: [], + zeroOrMoreTimesPartNames: [], + encoding: { part in + switch part { + case let .enabled(wrapped): + var headerFields: HTTPTypes.HTTPFields = .init() + let value = wrapped.payload + let body = try converter.setRequiredRequestBodyAsBinary( + value.body, + headerFields: &headerFields, + contentType: "text/plain" + ) + return .init( + name: "enabled", + 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: false, + requiredExactlyOncePartNames: [ + "enabled" + ], + requiredAtLeastOncePartNames: [], + atMostOncePartNames: [], + zeroOrMoreTimesPartNames: [], + decoding: { part in + let headerFields = part.headerFields + let (name, filename) = try converter.extractContentDispositionNameAndFilename(in: headerFields) + switch name { + case "enabled": + try converter.verifyContentTypeIfPresent( + in: headerFields, + matches: "text/plain" + ) + let body = try converter.getRequiredRequestBodyAsBinary( + OpenAPIRuntime.HTTPBody.self, + from: part.body, + transforming: { + $0 + } + ) + return .enabled(.init( + payload: .init(body: body), + filename: filename + )) + default: + preconditionFailure("Unknown part should be rejected by multipart validation.") + } + } + ) + default: + preconditionFailure("bestContentType chose an invalid content type.") + } + return Operations.post_sol_foo.Input(body: body) + } + """ + ) + } + + func testRequestMultipartBodyNumberType() throws { + try self.assertRequestInTypesClientServerTranslation( + """ + /foo: + post: + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + price: + type: number + required: + - price + additionalProperties: false + responses: + default: + description: Response + """, + input: """ + public struct Input: Sendable, Hashable { + @frozen public enum Body: Sendable, Hashable { + @frozen public enum multipartFormPayload: Sendable, Hashable { + public struct pricePayload: Sendable, Hashable { + public var body: OpenAPIRuntime.HTTPBody + public init(body: OpenAPIRuntime.HTTPBody) { + self.body = body + } + public init(value: Swift.Double) { + self.body = OpenAPIRuntime.HTTPBody(Swift.String(value)) + } + } + case price(OpenAPIRuntime.MultipartPart) + } + 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: false, + requiredExactlyOncePartNames: [ + "price" + ], + requiredAtLeastOncePartNames: [], + atMostOncePartNames: [], + zeroOrMoreTimesPartNames: [], + encoding: { part in + switch part { + case let .price(wrapped): + var headerFields: HTTPTypes.HTTPFields = .init() + let value = wrapped.payload + let body = try converter.setRequiredRequestBodyAsBinary( + value.body, + headerFields: &headerFields, + contentType: "text/plain" + ) + return .init( + name: "price", + 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: false, + requiredExactlyOncePartNames: [ + "price" + ], + requiredAtLeastOncePartNames: [], + atMostOncePartNames: [], + zeroOrMoreTimesPartNames: [], + decoding: { part in + let headerFields = part.headerFields + let (name, filename) = try converter.extractContentDispositionNameAndFilename(in: headerFields) + switch name { + case "price": + try converter.verifyContentTypeIfPresent( + in: headerFields, + matches: "text/plain" + ) + let body = try converter.getRequiredRequestBodyAsBinary( + OpenAPIRuntime.HTTPBody.self, + from: part.body, + transforming: { + $0 + } + ) + return .price(.init( + payload: .init(body: body), + filename: filename + )) + default: + preconditionFailure("Unknown part should be rejected by multipart validation.") + } + } + ) + default: + preconditionFailure("bestContentType chose an invalid content type.") + } + return Operations.post_sol_foo.Input(body: body) + } + """ + ) + } + + func testRequestMultipartBodyWithHeadersAndTypesafeInit() throws { + try self.assertRequestInTypesClientServerTranslation( + """ + /foo: + post: + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + name: + type: string + required: + - name + additionalProperties: false + encoding: + name: + headers: + X-Custom-Header: + schema: + type: string + required: true + responses: + default: + description: Response + """, + input: """ + public struct Input: Sendable, Hashable { + @frozen public enum Body: Sendable, Hashable { + @frozen public enum multipartFormPayload: Sendable, Hashable { + public struct namePayload: Sendable, Hashable { + public struct Headers: Sendable, Hashable { + public var X_hyphen_Custom_hyphen_Header: Swift.String + public init(X_hyphen_Custom_hyphen_Header: Swift.String) { + self.X_hyphen_Custom_hyphen_Header = X_hyphen_Custom_hyphen_Header + } + } + public var headers: Operations.post_sol_foo.Input.Body.multipartFormPayload.namePayload.Headers + public var body: OpenAPIRuntime.HTTPBody + public init( + headers: Operations.post_sol_foo.Input.Body.multipartFormPayload.namePayload.Headers, + body: OpenAPIRuntime.HTTPBody + ) { + self.headers = headers + self.body = body + } + public init( + headers: Operations.post_sol_foo.Input.Body.multipartFormPayload.namePayload.Headers, + value: Swift.String + ) { + self.headers = headers + self.body = OpenAPIRuntime.HTTPBody(value) + } + } + case name(OpenAPIRuntime.MultipartPart) + } + 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: false, + requiredExactlyOncePartNames: [ + "name" + ], + requiredAtLeastOncePartNames: [], + atMostOncePartNames: [], + zeroOrMoreTimesPartNames: [], + encoding: { part in + switch part { + case let .name(wrapped): + var headerFields: HTTPTypes.HTTPFields = .init() + let value = wrapped.payload + try converter.setHeaderFieldAsURI( + in: &headerFields, + name: "X-Custom-Header", + value: value.headers.X_hyphen_Custom_hyphen_Header + ) + let body = try converter.setRequiredRequestBodyAsBinary( + value.body, + headerFields: &headerFields, + contentType: "text/plain" + ) + return .init( + name: "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: false, + requiredExactlyOncePartNames: [ + "name" + ], + requiredAtLeastOncePartNames: [], + atMostOncePartNames: [], + zeroOrMoreTimesPartNames: [], + decoding: { part in + let headerFields = part.headerFields + let (name, filename) = try converter.extractContentDispositionNameAndFilename(in: headerFields) + switch name { + case "name": + let headers: Operations.post_sol_foo.Input.Body.multipartFormPayload.namePayload.Headers = .init(X_hyphen_Custom_hyphen_Header: try converter.getRequiredHeaderFieldAsURI( + in: headerFields, + name: "X-Custom-Header", + as: Swift.String.self + )) + try converter.verifyContentTypeIfPresent( + in: headerFields, + matches: "text/plain" + ) + let body = try converter.getRequiredRequestBodyAsBinary( + OpenAPIRuntime.HTTPBody.self, + from: part.body, + transforming: { + $0 + } + ) + return .name(.init( + payload: .init( + headers: headers, + body: body + ), + filename: filename + )) + default: + preconditionFailure("Unknown part should be rejected by multipart validation.") + } + } + ) + default: + preconditionFailure("bestContentType chose an invalid content type.") + } + return Operations.post_sol_foo.Input(body: body) + } + """ + ) + } + func testResponseWithExampleWithOnlyValueByte() throws { try self.assertResponsesTranslation( """