apple / swift-openapi-generator

Generate Swift client and server code from an OpenAPI document.
https://swiftpackageindex.com/apple/swift-openapi-generator/documentation
Apache License 2.0
1.21k stars 87 forks source link

Multipart content not properly generated? #550

Closed agattringer closed 1 month ago

agattringer commented 1 month ago

Question

Hello! I do have a problem with code generation it seems.

My open API spec file contains this section:

/api/payment/purchase:
    post:
      tags:
        - Payment
      summary: Processes a purchase
      description: "Some description"
      operationId: ProcessPurchase
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                Payload:
                  type: string
                  format: binary
                App:
                  type: string
            encoding:
              Payload:
                style: form
              App:
                style: form

For some reason the generated client method processPurchase() does not have a body input parameter. So I cannot pass the data properly into the POST method. Code generation works as expected for all other parts of the spec.

Am I doing something wrong here or is this an issue with the generator?

czechboy0 commented 1 month ago

Hi @agattringer, I'd like to be able to reproduce it - can you provide the full operation, so including the responses key, please?

czechboy0 commented 1 month ago

Oh I see the issue - your body is not marked as required: true, so it will be skipped. The generator emits a warning, which you should see in Xcode/in build output. After you add the missing required key, it'll show up.

The specification explicitly states that a request multipart body must be required, and never optional. Can you check if that fixes your issue?

agattringer commented 1 month ago

Thank you very much for your quick answer! required: true actually fixes the issue.

czechboy0 commented 1 month ago

Great! 🙏

erikhric commented 1 month ago

Hey @agattringer, how did you initialize body for your multipart request? Can you check if my schema and generated code satisfies everything neccessary for upload? I'm going in circles and obiously missing something.
schema:

      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  description: 'The image file. Max size: 64 MB, Min dimensions: 640x640'
                  format: binary
                  required: true

code:

@frozen internal enum Body: Sendable, Hashable {
                /// - Remark: Generated from `#/paths/accounts/{account}/content/basic/image/upload/POST/requestBody/multipartForm`.
                @frozen internal enum multipartFormPayload: Sendable, Hashable {
                    /// - Remark: Generated from `#/paths/accounts/{account}/content/basic/image/upload/POST/requestBody/multipartForm/file`.
                    internal struct filePayload: Sendable, Hashable {
                        internal var body: OpenAPIRuntime.HTTPBody
                        /// Creates a new `filePayload`.
                        ///
                        /// - Parameters:
                        ///   - body:
                        internal init(body: OpenAPIRuntime.HTTPBody) {
                            self.body = body
                        }
                    }
                    case file(OpenAPIRuntime.MultipartPart<Operations.post_sol_accounts_sol__lcub_account_rcub__sol_content_sol_basic_sol_image_sol_upload.Input.Body.multipartFormPayload.filePayload>)
                    case undocumented(OpenAPIRuntime.MultipartRawPart)
                }
                /// - Remark: Generated from `#/paths/accounts/{account}/content/basic/image/upload/POST/requestBody/content/multipart\/form-data`.
                case multipartForm(OpenAPIRuntime.MultipartBody<Operations.post_sol_accounts_sol__lcub_account_rcub__sol_content_sol_basic_sol_image_sol_upload.Input.Body.multipartFormPayload>)
            }
czechboy0 commented 1 month ago

@erikhric It looks correct to me.

You should be able to initialize the Body with the multipartBody case. What error are you hitting?

erikhric commented 1 month ago

Problem is obviously me, not the generator - I failed to find example for uploading an image. I don't know how to initialize Input for upload operation:

func uploadImage(accountId: Int, imageData: Data) {
        //this gives error Initializer 'init(_:)' requires the types 'Operations.post_sol_accounts_sol__lcub_account_rcub__sol_content_sol_basic_sol_image_sol_upload.Input.Body.multipartFormPayload' and 'Data.Element' (aka 'UInt8') be equivalent
        let multi: MultipartBody<Operations.post_sol_accounts_sol__lcub_account_rcub__sol_content_sol_basic_sol_image_sol_upload.Input.Body.multipartFormPayload> = .init(imageData)

        // Cannot convert value of type 'MultipartBody<Operations.post_sol_accounts_sol__lcub_account_rcub__sol_content_sol_basic_sol_image_sol_upload.Input.Body.multipartFormPayload>.Type' to expected argument type 'MultipartBody<Operations.post_sol_accounts_sol__lcub_account_rcub__sol_content_sol_basic_sol_image_sol_upload.Input.Body.multipartFormPayload>'
        let input: Operations.post_sol_accounts_sol__lcub_account_rcub__sol_content_sol_basic_sol_image_sol_upload
            .Input = .init(path: .init(account: accountId),
                           body: .multipartForm(MultipartBody<Operations.post_sol_accounts_sol__lcub_account_rcub__sol_content_sol_basic_sol_image_sol_upload.Input.Body.multipartFormPayload>))
}
czechboy0 commented 1 month ago

@erikhric check out the last example here: https://github.com/apple/swift-openapi-generator/blob/main/Examples/various-content-types-client-example/Sources/ContentTypesClient/ContentTypesClient.swift

A multipart body can be initialized using an array of parts. Each of the parts is named after the part name in the OpenAPI document. The part value can then be the image data itself.

erikhric commented 1 month ago

I get Cannot convert value of type 'Data' to expected argument type 'HTTPBody' error when passing image data.
If I convert it to string

let stringData = String(decoding: imageData, as: UTF8.self)
let multi: MultipartBody<Operations.post_sol_accounts_sol__lcub_account_rcub__sol_content_sol_basic_sol_image_sol_upload.Input.Body.multipartFormPayload> = [
        .file(.init(payload: .init(body: stringData), filename: "image")) //Cannot convert value of type 'String' to expected argument type 'HTTPBody'
]

It only works directly with string in place

.file(.init(payload: .init(body: "this works, or at least  it compiles"), filename: "image"))
czechboy0 commented 1 month ago

You can create an HTTPBody value from data using let httpBody = HTTPBody(data).

erikhric commented 1 month ago

@czechboy0 thank you! I'll be in Prague next month, can I buy you a beer? 🍺 😅