diff --git a/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md b/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md index 2e505a09..49d1e93e 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md +++ b/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md @@ -50,3 +50,4 @@ If you have any questions, tag [Honza Dvorsky](https://github.com/czechboy0) or - - - +- diff --git a/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0009.md b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0009.md new file mode 100644 index 00000000..adcef02c --- /dev/null +++ b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0009.md @@ -0,0 +1,934 @@ +# SOAR-0009: Type-safe streaming multipart support + +Provide a type-safe streaming API to produce and consume multipart bodies. + +## Overview + +- Proposal: SOAR-0009 +- Author(s): [Honza Dvorsky](https://github.com/czechboy0) +- Status: **In Review** +- Issue: [apple/swift-openapi-generator#36](https://github.com/apple/swift-openapi-generator/issues/36) +- Implementation: + - [apple/swift-openapi-runtime#69](https://github.com/apple/swift-openapi-runtime/pull/69) + - [apple/swift-openapi-generator#366](https://github.com/apple/swift-openapi-generator/pull/366) +- Feature flag: `multipart` +- Affected components: + - generator + - runtime +- Links: + - [OpenAPI 3.0.3 specification][openapi303] + - [OpenAPI 3.1.0 specification][openapi310] + - [Swagger.io documentation on multipart support in OpenAPI 3.x][swaggerio-multipart] + - [RFC 7578: Returning Values from Forms: multipart/form-data][rfc7578] + - [RFC 2046: Multipurpose Internet Mail Extensions (MIME) Part Two: Media Types][rfc2046] +- Versions: + - v1.0 (2023-11-08): Initial version + - v1.1 (2023-11-16): Replace the prefix "Randomized" with "Random" in the boundary generator name. + +### Introduction + +Support multipart requests and responses by providing a streaming way to produce and consume type-safe parts. + +### Motivation + +Since its first version, Swift OpenAPI Generator has supported OpenAPI operations that represent the most common HTTP request/response pairs. + +For example, posting JSON data to a server, which can look like this: + +``` +> POST /cat-photo-metadata HTTP/1.1 +> content-type: application/json +> x-sender-id: zoom123 +> +> {"objectCatName":"Waffles","photographerId":24} +--- +< HTTP/1.1 204 No Content +``` + +Or uploading a raw file, such as a photo, to a server: + +``` +> POST /cat-photo HTTP/1.1 +> content-type: image/jpeg +> +> ... +--- +< HTTP/1.1 204 No Content +``` + +In both of these examples, the HTTP message (a request or a response) has a single content type that describes the format of the body payload. + +However, there are use cases where the client wants to send multiple different payloads, each of a different content type, in a single HTTP message. That's what the [multipart][rfc7578] content type is for, and this proposal describes how Swift OpenAPI Generator can add support for it, providing both type safety while retaining a fully streaming API. + +> Note: In this proposal, we mostly discuss an example of making a multipart request, but all the proposed features apply to responses as well. + +With multipart support, uploading both a JSON object and a raw file to the server in one request could look something like: + +``` +> POST /photos HTTP/1.1 +> content-type: multipart/form-data; boundary=___MY_BOUNDARY_1234__ +> +> --___MY_BOUNDARY_1234__ +> content-disposition: form-data; name="metadata" +> content-type: application/json +> x-sender-id: zoom123 +> +> {"objectCatName":"Waffles","photographerId":24} +> --___MY_BOUNDARY_1234__ +> content-disposition: form-data; name="contents" +> content-type: image/jpeg +> +> ... +> --___MY_BOUNDARY_1234__-- +--- +< HTTP/1.1 204 No Content +``` + +While we'll discuss the structure of a multipart message in detail below, the TL;DR is: +- This is still a regular HTTP message, just with a different content type and body. +- The body uses a _boundary_ string to separate individual _parts_. +- Each part has its own header fields and body. + +Extra requirements to keep in mind: +- A multipart message must have at least one part (an empty multipart body is invalid). +- But, a part can have no headers or an empty body. +- So, the least you can send is a single part with no headers and no body bytes, but it'd still have the boundary strings around it, making it a valid multipart body consisting of one part. + +### Proposed solution + +As an example, let's consider a service that allows uploading cat photos together with additional JSON metadata in a single request, as seen in the previous section. + +#### Describing a multipart request in OpenAPI + +Let's define a `POST` request on the `/photos` path that accepts a `multipart/form-data` body containing 2 parts, one JSON part with the name "metadata", and another called "contents" that contains the raw JPEG bytes of the cat photo. + +> Note: Parts do not have a predefined order, they can arrive in any order the sender chooses. So let's think of a multipart body as a _stream_ of parts. + +In OpenAPI 3.1.0, the operation could look like this (irrelevant parts were omitted, see the full OpenAPI document in [Appendix A][appendix-a]): + +```yaml +paths: + /photos: + post: + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + metadata: + $ref: '#/components/schemas/PhotoMetadata' + contents: + type: string + contentEncoding: binary + required: + - metadata + - contents + encoding: + metadata: + headers: + x-sender-id: + schema: + type: string + contents: + contentType: image/jpeg +``` + +In OpenAPI, the schema for the multipart message is defined using JSON Schema, even though the top level schema is never actually serialized as JSON - it only serves as a way to define the individual parts. + +The top level schema is always an object (or an object-ish type, such as allOf or anyOf of objects), and each property describes one part. Top level properties that describe an array schema are interpreted as a way to say that there might be more than one part of the provided name (matching the property name). The `required` property of the schema is used just like in regular JSON Schema – to communicate which properties (in this case, parts) are required and which are optional. + +Finally, a sibling field of the `schema` is called `encoding` and mirrors the `schema` structure. For each part, you can override the content type and add custom header fields for each part. + +#### Generating a multipart request in Swift + +As with other Swift OpenAPI Generator features, the goal of generating code for multipart is to maximize type safety for adopters without compromising their ability to stream HTTP bodies. + +With that in mind, multiple different strategies were considered for how to best represent a multipart body in Swift – for details, see the "Future directions" and "Alternatives considered" sections of this proposal. + +To that end, we propose to represent a multipart body as an _async sequence of type-safe parts_ (spelled as `OpenAPIRuntime.MultipartBody` in this proposal). This is motivated by the fact that multipart bodies can be gigabytes in size and contain hundreds of parts, so any Swift representation that forces buffering immediately prevents advanced use cases. + +> Note: That said, a proposal in the future might introduce an opt-in buffered variant of each type-safe body that builds on the foundation of the async sequence. If this is something you'd like to use, please open an issue on GitHub and start a thread on the Swift Forums, describing your use case. + +In addition to `MultipartBody`, we are proposing a few new public types (`MultipartPart` and `MultipartRawPart`) in the runtime library that are used in the Swift snippets below. The full proposed runtime library API diff can be found in [Appendix B][appendix-b], and the details of each type will be discussed in "Detailed design" section. + +Getting back to the cat photo service and our multipart request, the body definition would look something like (omitted irrelevant parts of code for brevity): + +```swift +/// - Remark: Generated from `#/paths/photos/POST/requestBody/content`. +/* nested in Operations.uploadPhoto.Input */ { + enum Body { + enum multipartFormPayload { + struct metadataPayload { + struct Headers { + var x_dash_sender_dash_id: String? + } + var headers: Headers + var body: Components.Schemas.PhotoMetadata + } + case metadata(MultipartPart) + struct contentsPayload { + var body: HTTPBody + } + case contents(MultipartPart) + case undocumented(MultipartRawPart) + } + /// - Remark: Generated from `#/paths/photos/POST/requestBody/content/multipart\/form-data`. + case multipartForm(MultipartBody) + } +} +``` + +The generated type `multipartFormPayload` is an enum with associated value, where each case is one of the parts documented in the OpenAPI document. By default, undocumented parts are also collected, and this behavior is controlled by the `additionalProperties` schema field - more on that in the "Detailed design" section below. + +#### Producing a multipart body sequence + +As a client sending this multipart request (or a server sending a multipart response), you are expected to provide a value of type `OpenAPIRuntime.MultipartBody`, where `Part` is your concrete generated enum: + +```swift +let multipartBody: OpenAPIRuntime.MultipartBody = ... +let response = try await client.uploadPhoto(body: multipartBody) +// ... +``` + +Similarly to `OpenAPIRuntime.HTTPBody`, the `OpenAPIRuntime.MultipartBody` async sequence has several convenience initializers, making it easy to construct both from buffered and streaming sources. + +For a buffered example, just provide an array of the part values, such as: + +```swift +let multipartBody: OpenAPIRuntime.MultipartBody = [ + .metadata(.init( + payload: .init( + headers: .init(x_dash_sender_dash_id: "zoom123"), + body: .init(objectCatName: "Waffles", photographerId: 24) + ) + )), + .contents(.init( + payload: .init( + body: .init(try Data(contentsOf: URL(fileURLWithPath: "/tmp/waffles-summer-2023.jpg"))) + ), + filename: "cat.jpg" + )) +] +let response = try await client.uploadPhoto(body: multipartBody) +// ... +``` + +However, you can also stream the parts and their bodies: + +```swift +let (stream, continuation) = AsyncStream.makeStream(of: Operations.uploadPhoto.Input.Body.multipartFormPayload.self) +// Pass `continuation` to another task to start producing parts by calling `continuation.yield(...)` and at the end, `continuation.finish()`. +let response = try await client.uploadPhoto(body: .init(stream)) +// ... +``` + +#### Consuming a multipart body sequence + +When consuming a multipart body sequence, for example as a client consuming a multipart response, or a server consuming a multipart request, you are provided with the multipart body async sequence and are responsible for iterating it to completion. + +Additionally, for received parts that have their own streaming bodies, you _must_ consume those bodies before requesting the next part, as the underlying async sequence never does any buffering for you, so you can't "skip" any parts or chunks of bytes within a part without explicitly consuming it. + +Consuming a multipart body, where you print the metadata fields, and write the photo to disk, could look something like this: + +```swift +let multipartBody: OpenAPIRuntime.MultipartBody = ... +for try await part in multipartBody { + switch part { + case .metadata(let metadataPart): + let metadata = metadataPart.payload + print("x-sender-id: \(metadata.headers.x_dash_sender_dash_id ?? "")") + print("Cat name: \(metadata.body.objectCatName)") + print("Photographer ID: \(metadata.body.photographerId?.description ?? "")") + case .contents(let contentsPart): + // Ensure the incoming filepath doesn't try to escape to a parent directory, and so on, before using it. + let fileName = contentsPart.filename ?? "\(UUID().uuidString).jpg" + guard let outputStream = OutputStream(toFileAtPath: "/tmp/received-cat-photos/\(fileName)", shouldAppend: false) else { + // failed to open a stream + } + outputStream.open() + defer { + outputStream.close() + } + // Consume the body before moving to the next part. + for try await chunk in contentsPart.body { + chunk.withUnsafeBufferPointer { _ = outputStream.write($0.baseAddress!, maxLength: $0.count) } + } + case .undocumented(let rawPart): + print("Received an undocumented part with header fields: \(rawPart.headerFields)") + // Consume the body before moving to the next part. + _ = try await ArraySlice(collecting: rawPart.body, upTo: 10 * 1024 * 1024) + } +} +``` + +### Detailed design + +This section describes more details of the functionality supporting the kind of examples we saw above. + +#### Different enum case types + +The example in the section "Proposed solution" already showed different case types of the generated enum, namely type-safe ones (`case metadata(MultipartPart)`) and an undocumented one (`case undocumented(MultipartRawPart)`). + +> Note: As a reminder, the full API of the types `MultipartPart`, `MultipartRawPart`, and `MultipartDynamicallyNamedPart` can be found in [Appendix B][appendix-b]. + +In this section, we enumerate the different enum case types and under what circumstances they are generated. + +- **Scenario A**: Zero or more documented cases and `additionalProperties` is not set - a common default. + - For each documented case, generates an associated type of `MultipartPart` (note that `metadataPayload` is specific to this case only, would be called something else in other documents and properties.) + - Also generates an `undocumented` case with an associated type `MultipartRawPart`. + +OpenAPI: + +```yaml +multipart/form-data: + schema: + type: object + properties: + metadata: + $ref: '#/components/schemas/PhotoMetadata' +``` + +Generated Swift enum members: + +```swift +struct metadataPayload { + var body: Components.Schemas.PhotoMetadata +} +case metadata(MultipartPart) +case undocumented(MultipartRawType) +``` + +- **Scenario B**: Zero or more documented cases and `additionalProperties: true`. + - For each documented case, same as Scenario A. + - Also generates an `other` case with an associated type `MultipartRawPart`. + - Note that while similar to `undocumented`, the `other` case uses a different name to communicate the fact that the OpenAPI author deliberately enabled `additionalProperties` and thus any parts with unknown names are expected – so the name "undocumented" would not be appropriate. + +OpenAPI: + +```yaml +multipart/form-data: + schema: + type: object + properties: + metadata: + $ref: '#/components/schemas/PhotoMetadata' + additionalProperties: true +``` + +Generated Swift enum members: + +```swift +struct metadataPayload { + var body: Components.Schemas.PhotoMetadata +} +case metadata(MultipartPart) +case other(MultipartRawType) +``` + +- **Scenario C**: Zero or more documented cases and `additionalProperties: `. + - For each documented case, same as Scenario A. + - Also generates an `other` case with an associated type `MultipartDynamicallyNamedPart`. + - Note that while similar to `MultipartPart`, `MultipartDynamicallyNamedPart` adds a read-write `name` property, because while the part name is statically known when `MultipartPart` is used, that's not the case when `MultipartDynamicallyNamedPart` is used, thus the extra property into which the part name is written is required. + - Also, since there is no way to define custom headers in this case, the generic parameter of `MultipartDynamicallyNamedPart` is the body value itself, instead of being nested in an `otherPayload` generated struct like for statically documented parts. + +OpenAPI: + +```yaml +multipart/form-data: + schema: + type: object + properties: + metadata: + $ref: '#/components/schemas/PhotoMetadata' + additionalProperties: + $ref: '#/components/schemas/OtherInfo' +``` + +Generated Swift enum members: + +```swift +struct metadataPayload { + var body: Components.Schemas.PhotoMetadata +} +case metadata(MultipartPart) +case other(MultipartDynamicallyNamedPart) +``` + +- **Scenario D**: Zero or more documented cases and `additionalProperties: false`. + - For each documented case, same as Scenario A. + - No other cases are generated, and the runtime validation logic ensures that no undocumented part is allowed through. + +OpenAPI: + +```yaml +multipart/form-data: + schema: + type: object + properties: + metadata: + $ref: '#/components/schemas/PhotoMetadata' + additionalProperties: false +``` + +Generated Swift enum members: + +```swift +struct metadataPayload { + var body: Components.Schemas.PhotoMetadata +} +case metadata(MultipartPart) +``` + +#### Validation + +Since the OpenAPI document can describe some parts as single value properties, and others as array; and some as required, while others as optional, the generator will emit code that enforces these semantics in the internals of the `MultipartBody` sequence. + +The following will be enforced: +- if a property is a required single value, the sequence fails validation with an error if a part of that name is not seen before the sequence is finished, or if it appears multiple times +- if a property is an optional single value, the sequence fails validation with an error if a part of that name is seen multiple times +- if a property is a required array value, the sequence fails validation with an error if a part of that name is not seen at least once +- if a property is an optional array value, the sequence never fails validation, as any number, from 0 up, are a valid number of occurrences +- if `additionalProperties` is not specified, the default behavior is to allow undocumented parts through +- if `additionalProperties: true`, the sequence never fails validation when an undocumented part is encountered +- if `additionalProperties: false`, the sequence fails validation if an undocumented part is encountered +- if `additionalProperties: `, the sequence doesn't fail validation, but will fail later if the provided part can't be parsed as `` + +This validation is implemented as a private async sequence inserted into the chain with the names of parts that need the specific requirements enforced. This affords the adopter the same amount of type safety as the rest of the generated code, such as `Codable` generated types that parse from JSON. + +Optionality of parts is not reflected as an optional type (`Type?`) here, instead the _absence_ of a part in the async sequence represents it being nil. + +#### Boundary customization + +When sending a multipart message, the sender needs to choose a boundary string (for example, `___MY_BOUNDARY_1234__`) that is used to separate the individual parts. The boundary string must not appear in any of the parts themselves. + +With this proposal, we introduce a protocol called `MultipartBoundaryGenerator` with a single method `func makeBoundary() -> String`, which returns the boundary string. The method is called once per multipart message, so it's encouraged for implementations of `MultipartBoundaryGenerator` to return a somewhat random output every time the `makeBoundary` method is called. + +The runtime library comes with two implementations, `ConstantMultipartBoundaryGenerator` and `RandomMultipartBoundaryGenerator`. + +`ConstantMultipartBoundaryGenerator` returns the same boundary every time and is useful for testing and in cases where stable output for stable inputs is desired, for example for caching. +`RandomMultipartBoundaryGenerator` uses a constant prefix and appends a random suffix made out of `0-9` digits, returning a different output every time. + +By default, the updated `Configuration` uses the random boundary generator, but the adopter can switch to the constant one, or provide a completely custom implementation. + +#### The OpenAPI Encoding object + +In the initial example (again listed below), we saw how to use the `encoding` object in the OpenAPI document to: +1. explicitly specify the content type `image/jpeg` for the `contents` part. +2. define a custom header field `x-sender-id` for the `metadata` part. + +```yaml +multipart/form-data: + schema: + type: object + properties: + metadata: + $ref: '#/components/schemas/PhotoMetadata' + contents: + type: string + contentEncoding: binary + required: + - metadata + - contents + encoding: + metadata: + headers: + x-sender-id: + schema: + type: string + contents: + contentType: image/jpeg +``` + +Adopters only need to explicitly specify the content type if the inferred content type doesn't match what they need. + +The inferred content type uses the following logic, copied from the [OpenAPI 3.1.0 specification][openapi310-multipart]: + +> - If the property is a primitive, or an array of primitive values, the default Content-Type is `text/plain` +> - If the property is complex, or an array of complex values, the default Content-Type is `application/json` +> - If the property is a `type: string` with a `contentEncoding`, the default Content-Type is `application/octet-stream` + +The generator follows these rules and once a serialization method is chosen, treats the payloads the same way as bodies in regular HTTP requests and responses. + +Custom headers are also optional, so if the default content type is correctly inferred, and the adopter doesn't need any custom headers, the `encoding` object can be omitted from the OpenAPI document. + +> Important: Note that the generated code depends on a pair of a `schema` + `encoding`, so in theory the generator would need to generate as many variants as there are unique combinations of `schema` + `encoding`. However, we choose not to do that, and instead will emit a diagnostic if a `schema` is associated with more than one `encoding` value, and always use the first pair encountered. We validated against almost 2000 open-source OpenAPI documents that not a single document used a `schema` with more than one `encoding`, so we are confident this is unlikely to limit any legitimate use cases. + +#### Optional multipart request bodies + +While the OpenAPI specification allows a request body to be optional, in multipart the rule is that at least one part must be sent, so a nil or empty multipart sequence is not valid. For that reason, when the generator encounters an optional multipart request body, it will emit a warning diagnostic and treat it as a required one (as we assume that the OpenAPI author just forgot to mark the body as required). + +### API stability + +- Runtime API: + - All of the runtime API changes are purely additive, so this feature does not require a new API-breaking release of the runtime library. +- Generated API: + - Since before this feature, multipart bodies were treated as generic raw bodies, represented by the `HTTPBody` type, and now we will generate `MultipartBody`, this is a breaking change. + - However, we will stage it into the 0.3.x release behind a feature flag, and enable it in the next API-breaking release of the generator. + +### Future directions + +As this proposal already is of a considerable size and complexity, we chose to defer some additional ideas to future proposals. Those will be considered based on feedback from real-world usage of this initial multipart support. + +#### A buffered representation of the full body + +While we believe that offering a fully streaming representation of the multipart parts, and even their individual bodies, is the correct choice at the lowest type-safe layer, some adopters might not take advantage of the streaming nature, and the streaming API might not be ergonomic for them. This might especially be the case when the individual parts are small and were sent in one data chunk from the client anyway, for example from an HTML form in a web browser. + +For such adopters, it might make sense to generate an extra convenience type that has a property for each part, and is only delivered to the receiver once all the data has come in. This type would represent optional values as optional properties, and array values as array properties, closer to the generated `Codable` types. + +This change should be purely additive, and would build on top of the multipart async sequence form this proposal. The generated code should simply accumulate all the parts, and then assign them to the properties on this generated type. + +The feature needs to be balanced against the cost of generating another variant of the code we will already generate with this proposal, and it's also important not to let it overshadow the streaming variant, as then even adopters who would benefit from streaming might not use it, because they might see the buffered type first and not realize multipart streaming is even supported. + +#### Other multipart subtypes + +This proposal focuses on the `multipart/form-data` type, but there are other multipart variants, such as `multipart/alternative` and `multipart/mixed`. It might make sense to add support for these in the future as well. + +### Alternatives considered + +This proposal is a product of several months of thinking about how to best represent multipart in a type-safe, yet streaming, way. Below is an incomplete list of other ideas that were considered, and the reasons we ultimately chose not to pursue them. + +#### No action - keep multipart as a raw body + +The first obvious alternative to adding type-safe support for multipart is to _not_ do it. It has the advantage of preserving the streaming nature, and doesn't force buffering on users. + +However, better support for multipart has been the top adopter request in recent months, so it seemed clear that the status quo is not sufficient. On the wire, multipart is not trivial to serialize and parse correctly, so asking every adopter to reimplement it seemed suboptimal. + +It also doesn't align with our goal of maximizing type-safety without compromising on streaming. + +#### Only surfacing raw parts + +The next step on the spectrum is to provide a sequence of parsed raw parts (in other words, the header fields and the raw body), without generating custom code for each part. + +It has the advantage of taking care of the trickiest part of multipart, and that's the serialization and parsing of parts between boundaries, and it retains streaming. However, it drops on the floor the static information the adopter authored in their OpenAPI document, and seems inconsistent with the rest of the generated code, where we do generate custom code for each schema. + +However, in a scenario where we didn't have time to implement the more advanced solution, this still would have been a decent quality-of-life improvement. + +#### No runtime validation of part semantics + +Even with type-safe generated parts, as proposed, we could avoid doing runtime validation of the part semantics defined in the OpenAPI document, such as that a required part did actually arrive before the multipart body sequence was completed, and that only parts described by an array schema are allowed to appear more than once. + +While skipping this work would simplify implementation a little bit, it would again weaken the trust that adopters can have in the type-safe code truly verifying as much information as possible from the OpenAPI document. + +The verification happens in a private async sequence that's inserted into the middle of the serialization/parsing chain, so is mostly implemented in the runtime library, not really affecting the complexity of the generator. + +#### Buffered representation at the bottom layer + +We also could have generated custom code for the schema describing the parts, and only offer a non-streaming, buffered representation of the multipart body. However, that seems to go against the work we did in 0.3.0 to transition the transport and middleware layers to fully streaming mode, unlocking high-performance use cases, and would arbitrarily treat multipart as somewhat different to all the other content types. + +While this is what most other code generators for OpenAPI do today, we didn't just want to follow precedent. We wanted to show how the power of Swift's type safety combined with modern concurrency features allows library authors not to be forced to choose between type-safety and performance – Swift gives us both. It certainly did require more work and several iterations, especially around the layering of `MultipartRawPart`, `MultipartDynamicallyNamedPart`, and `MultipartPart`, but we believe what we propose here is ready for wider feedback. + +### Feedback + +We're looking for feedback from: +- potential adopters of multipart, both on client and server, both with buffering and streaming use cases +- contributors of Swift OpenAPI Generator, about how this fits into the rest of the tool + +And we're especially looking for ideas on the naming of the new types, especially: +- `MultipartRawType` +- `MultipartDynamicallyNamedPart` +- `MultipartPart` +- `MultipartBody` + +That said, any and all feedback is appreciated, especially the kind that can help newcomers pick up the API and easily work with multipart. + +### Acknowledgements + +A special thanks to [Si Beaumont](https://github.com/simonjbeaumont) for helping refine this proposal with thoughtful feedback. + +[rfc7578]: https://www.rfc-editor.org/rfc/rfc7578 +[rfc2046]: https://datatracker.ietf.org/doc/html/rfc2046 +[rfc2046-section5.1]: https://datatracker.ietf.org/doc/html/rfc2046#section-5.1 +[swaggerio-multipart]: https://swagger.io/docs/specification/describing-request-body/multipart-requests +[openapi303]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md +[openapi310]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md +[openapi303-multipart]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#special-considerations-for-multipart-content +[openapi310-multipart]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#special-considerations-for-multipart-content +[appendix-a]: #appendix-a-example-openapi-document-with-multipart-bodies +[appendix-b]: #appendix-b-runtime-library-api-changes + +### Appendix A: Example OpenAPI document with multipart bodies + +```yaml +openapi: '3.1.0' +info: + title: Cat photo service + version: 2.0.0 +paths: + /photos: + post: + operationId: uploadPhoto + description: Uploads the provided photo with metadata to the server. + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + description: The individual parts of the photo upload. + properties: + metadata: + $ref: '#/components/schemas/PhotoMetadata' + description: Extra information about the uploaded photo. + contents: + type: string + contentEncoding: binary + description: The raw contents of the photo. + required: + - metadata + - contents + encoding: + metadata: + # No need to explicitly specify `contents: application/json` because + # it's inferred from the schema itself. + headers: + x-sender-id: + # Note that this serves as an example of a part header. + # But conventionally, you'd include this property in the metadata JSON instead. + description: The identifier of the device sending the photo. + schema: + type: string + contents: + contentType: image/jpeg + responses: + '204': + description: Successfully uploaded the file. +components: + schemas: + PhotoMetadata: + type: object + description: Extra information about a photo. + properties: + objectCatName: + type: string + description: The name of the cat that's in the photo. + photographerId: + type: integer + description: The identifier of the photographer. + required: + - objectCatName + OtherInfo: + type: object + description: Other information. +``` + +### Appendix B: Runtime library API changes + +1. New API to represent a boundary generator. + +```swift +/// A generator of a new boundary string used by multipart messages to separate parts. +public protocol MultipartBoundaryGenerator : Sendable { + + /// Generates a boundary string for a multipart message. + /// - Returns: A boundary string. + func makeBoundary() -> String +} + +extension MultipartBoundaryGenerator where Self == OpenAPIRuntime.ConstantMultipartBoundaryGenerator { + + /// A generator that always returns the same boundary string. + public static var constant: OpenAPIRuntime.ConstantMultipartBoundaryGenerator { get } +} + +extension MultipartBoundaryGenerator where Self == OpenAPIRuntime.RandomMultipartBoundaryGenerator { + + /// A generator that produces a random boundary every time. + public static var random: OpenAPIRuntime.RandomMultipartBoundaryGenerator { get } +} + + +/// A generator that always returns the same constant boundary string. +public struct ConstantMultipartBoundaryGenerator : OpenAPIRuntime.MultipartBoundaryGenerator { + + /// The boundary string to return. + public let boundary: String + + /// Creates a new generator. + /// - Parameter boundary: The boundary string to return every time. + public init(boundary: String = "__X_SWIFT_OPENAPI_GENERATOR_BOUNDARY__") + + /// Generates a boundary string for a multipart message. + /// - Returns: A boundary string. + public func makeBoundary() -> String +} + +/// A generator that returns a boundary containg a constant prefix and a randomized suffix. +public struct RandomMultipartBoundaryGenerator : OpenAPIRuntime.MultipartBoundaryGenerator { + + /// The constant prefix of each boundary. + public let boundaryPrefix: String + + /// The length, in bytes, of the randomized boundary suffix. + public let randomNumberSuffixLength: Int + + /// Create a new generator. + /// - Parameters: + /// - boundaryPrefix: The constant prefix of each boundary. + /// - randomNumberSuffixLength: The length, in bytes, of the randomized boundary suffix. + public init(boundaryPrefix: String = "__X_SWIFT_OPENAPI_", randomNumberSuffixLength: Int = 20) + + /// Generates a boundary string for a multipart message. + /// - Returns: A boundary string. + public func makeBoundary() -> String +} +``` + +2. Customizing the boundary generator on `Configuration`. + +The below property and initializer added to the Configuration struct, while the existing initializer is deprecated. + +```swift +/// A set of configuration values used by the generated client and server types. +/* public struct Configuration : Sendable { */ + + /// The generator to use when creating mutlipart bodies. + public var multipartBoundaryGenerator: OpenAPIRuntime.MultipartBoundaryGenerator + + /// Creates a new configuration with the specified values. + /// + /// - Parameters: + /// - dateTranscoder: The transcoder to use when converting between date + /// and string values. + /// - multipartBoundaryGenerator: The generator to use when creating mutlipart bodies. + public init(dateTranscoder: OpenAPIRuntime.DateTranscoder = .iso8601, multipartBoundaryGenerator: OpenAPIRuntime.MultipartBoundaryGenerator = .random) + + /// Creates a new configuration with the specified values. + /// + /// - Parameter dateTranscoder: The transcoder to use when converting between date + /// and string values. + @available(*, deprecated, renamed: "init(dateTranscoder:multipartBoundaryGenerator:)") + public init(dateTranscoder: OpenAPIRuntime.DateTranscoder) +/* } */ +``` + +3. New multipart part types. + +```swift +/// A raw multipart part containing the header fields and the body stream. +public struct MultipartRawPart : Sendable, Hashable { + + /// The header fields contained in this part, such as `content-disposition`. + public var headerFields: HTTPTypes.HTTPFields + + /// The body stream of this part. + public var body: OpenAPIRuntime.HTTPBody + + /// Creates a new part. + /// - Parameters: + /// - headerFields: The header fields contained in this part, such as `content-disposition`. + /// - body: The body stream of this part. + public init(headerFields: HTTPTypes.HTTPFields, body: OpenAPIRuntime.HTTPBody) +} + +extension MultipartRawPart { + + /// Creates a new raw part by injecting the provided name and filename into + /// the `content-disposition` header field. + /// - Parameters: + /// - name: The name of the part. + /// - filename: The file name of the part. + /// - headerFields: The header fields of the part. + /// - body: The body stream of the part. + public init(name: String?, filename: String? = nil, headerFields: HTTPTypes.HTTPFields, body: OpenAPIRuntime.HTTPBody) + + /// The name of the part stored in the `content-disposition` header field. + public var name: String? + + /// The file name of the part stored in the `content-disposition` header field. + public var filename: String? +} + +/// A wrapper of a typed part with a statically known name that adds other +/// dynamic `content-disposition` parameter values, such as `filename`. +public struct MultipartPart : Sendable, Hashable where Payload : Hashable, Payload : Sendable { + + /// The underlying typed part payload, which has a statically known part name. + public var payload: Payload + + /// A file name parameter provided in the `content-disposition` part header field. + public var filename: String? + + /// Creates a new wrapper. + /// - Parameters: + /// - payload: The underlying typed part payload, which has a statically known part name. + /// - filename: A file name parameter provided in the `content-disposition` part header field. + public init(payload: Payload, filename: String? = nil) +} + +/// A wrapper of a typed part without a statically known name that adds +/// dynamic `content-disposition` parameter values, such as `name` and `filename`. +public struct MultipartDynamicallyNamedPart : Sendable, Hashable where Payload : Hashable, Payload : Sendable { + + /// The underlying typed part payload, which has a statically known part name. + public var payload: Payload + + /// A file name parameter provided in the `content-disposition` part header field. + public var filename: String? + + /// A name parameter provided in the `content-disposition` part header field. + public var name: String? + + /// Creates a new wrapper. + /// - Parameters: + /// - payload: The underlying typed part payload, which has a statically known part name. + /// - filename: A file name parameter provided in the `content-disposition` part header field. + /// - name: A name parameter provided in the `content-disposition` part header field. + public init(payload: Payload, filename: String? = nil, name: String? = nil) +} +``` + +4. New multipart body async sequence type. + +```swift +/// The body of multipart requests and responses. +/// +/// `MultipartBody` represents an async sequence of multipart parts of a specific type. +/// +/// The `Part` generic type parameter is usually a generated enum representing +/// the different values documented for this multipart body. +/// +/// ## Creating a body from buffered parts +/// +/// Create a body from an array of values of type `Part`: +/// +/// ```swift +/// let body: MultipartBody = [ +/// .myCaseA(...), +/// .myCaseB(...), +/// ] +/// ``` +/// +/// ## Creating a body from an async sequence of parts +/// +/// The body type also supports initialization from an async sequence. +/// +/// ```swift +/// let producingSequence = ... // an AsyncSequence of MyPartType +/// let body = MultipartBody( +/// producingSequence, +/// iterationBehavior: .single // or .multiple +/// ) +/// ``` +/// +/// In addition to the async sequence, also specify whether the sequence is safe +/// to be iterated multiple times, or can only be iterated once. +/// +/// Sequences that can be iterated multiple times work better when an HTTP +/// request needs to be retried, or if a redirect is encountered. +/// +/// In addition to providing the async sequence, you can also produce the body +/// using an `AsyncStream` or `AsyncThrowingStream`: +/// +/// ```swift +/// let (stream, continuation) = AsyncStream.makeStream(of: MyPartType.self) +/// // Pass the continuation to another task that produces the parts asynchronously. +/// Task { +/// continuation.yield(.myCaseA(...)) +/// // ... later +/// continuation.yield(.myCaseB(...)) +/// continuation.finish() +/// } +/// let body = MultipartBody(stream) +/// ``` +/// +/// ## Consuming a body as an async sequence +/// +/// The `MultipartBody` type conforms to `AsyncSequence` and uses a generic element type, +/// so it can be consumed in a streaming fashion, without ever buffering the whole body +/// in your process. +/// +/// ```swift +/// let multipartBody: MultipartBody = ... +/// for try await part in multipartBody { +/// switch part { +/// case .myCaseA(let myCaseAValue): +/// // Handle myCaseAValue. +/// case .myCaseB(let myCaseBValue): +/// // Handle myCaseBValue, which is a raw type with a streaming part body. +/// // +/// // Option 1: Process the part body bytes in chunks. +/// for try await bodyChunk in myCaseBValue.body { +/// // Handle bodyChunk. +/// } +/// // Option 2: Accumulate the body into a byte array. +/// // (For other convenience initializers, check out ``HTTPBody``. +/// let fullPartBody = try await [UInt8](collecting: myCaseBValue.body, upTo: 1024) +/// // ... +/// } +/// } +/// ``` +/// +/// Multipart parts of different names can arrive in any order, and the order is not significant. +/// +/// Consuming the multipart body should be resilient to parts of different names being reordered. +/// +/// However, multiple parts of the same name, if allowed by the OpenAPI document by defining it as an array, +/// should be treated as an ordered array of values, and those cannot be reordered without changing +/// the message's meaning. +/// +/// > Important: Parts that contain a raw streaming body (of type ``HTTPBody``) must +/// have their bodies fully consumed before the multipart body sequence is asked for +/// the next part. The multipart body sequence does not buffer internally, and since +/// the parts and their bodies arrive in a single stream of bytes, you cannot move on +/// to the next part until the current one is consumed. +final public class MultipartBody : @unchecked Sendable where Part : Sendable { + + /// The iteration behavior, which controls how many times the input sequence can be iterated. + public let iterationBehavior: OpenAPIRuntime.IterationBehavior +} + +extension MultipartBody : Equatable { + public static func == (lhs: OpenAPIRuntime.MultipartBody, rhs: OpenAPIRuntime.MultipartBody) -> Bool +} + +extension MultipartBody : Hashable { + public func hash(into hasher: inout Hasher) +} + +extension MultipartBody { + + /// Creates a new sequence with the provided async sequence of parts. + /// - Parameters: + /// - sequence: An async sequence that provides the parts. + /// - iterationBehavior: The iteration behavior of the sequence, which indicates whether it + /// can be iterated multiple times. + @inlinable public convenience init(_ sequence: Input, iterationBehavior: OpenAPIRuntime.IterationBehavior) where Part == Input.Element, Input : AsyncSequence + + /// Creates a new sequence with the provided collection of parts. + /// - Parameter elements: A collection of parts. + @inlinable public convenience init(_ elements: some Collection & Sendable) + + /// Creates a new sequence with the provided async throwing stream. + /// - Parameter stream: An async throwing stream that provides the parts. + @inlinable public convenience init(_ stream: AsyncThrowingStream.Element, Error>) + + /// Creates a new sequence with the provided async stream. + /// - Parameter stream: An async stream that provides the parts. + @inlinable public convenience init(_ stream: AsyncStream.Element>) +} + +extension MultipartBody : ExpressibleByArrayLiteral { + public typealias ArrayLiteralElement = OpenAPIRuntime.MultipartBody.Element + public convenience init(arrayLiteral elements: OpenAPIRuntime.MultipartBody.Element...) +} + +extension MultipartBody : AsyncSequence { + public typealias Element = Part + public typealias AsyncIterator = OpenAPIRuntime.MultipartBody.Iterator + public func makeAsyncIterator() -> OpenAPIRuntime.MultipartBody.AsyncIterator +} + +extension MultipartBody { + public struct Iterator : AsyncIteratorProtocol { + public mutating func next() async throws -> OpenAPIRuntime.MultipartBody.Element? + } +} +``` + +5. Move `HTTPBody.IterationBehavior` to the top of the module, as `OpenAPIRuntime.IterationBehavior`. + +It is then used by `MultipartBody` as well as `HTTPBody`. + +A deprecated compatibility typealias is added to `HTTPBody` to retain source stability, but it'll be removed on the next API break.