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
Status: Implemented (1.0.0)
Implementation: apple/swift-openapi-runtime#69, apple/swift-openapi-generator#366
Review: (review)
Affected components: generator, runtime
Related links:
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 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.
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.
In OpenAPI 3.1.0, the operation could look like this (irrelevant parts were omitted, see the full OpenAPI document in Appendix A):
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.
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, 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):
/// - 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<metadataPayload>)
struct contentsPayload {
var body: HTTPBody
}
case contents(MultipartPart<contentsPayload>)
case undocumented(MultipartRawPart)
}
/// - Remark: Generated from `#/paths/photos/POST/requestBody/content/multipart\/form-data`.
case multipartForm(MultipartBody<multipartFormPayload>)
}
}
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<Part>
, where Part
is your concrete generated enum:
let multipartBody: OpenAPIRuntime.MultipartBody<Operations.uploadPhoto.Input.Body.multipartFormPayload> = ...
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:
let multipartBody: OpenAPIRuntime.MultipartBody<Operations.uploadPhoto.Input.Body.multipartFormPayload> = [
.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:
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:
let multipartBody: OpenAPIRuntime.MultipartBody<Operations.uploadPhoto.Input.Body.multipartFormPayload> = ...
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 ?? "<nil>")")
print("Cat name: \(metadata.body.objectCatName)")
print("Photographer ID: \(metadata.body.photographerId?.description ?? "<nil>")")
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<UInt8>(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<metadataPayload>)
) and an undocumented one (case undocumented(MultipartRawPart)
).
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<metadataPayload>
(note thatmetadataPayload
is specific to this case only, would be called something else in other documents and properties.)Also generates an
undocumented
case with an associated typeMultipartRawPart
.
OpenAPI:
multipart/form-data:
schema:
type: object
properties:
metadata:
$ref: '#/components/schemas/PhotoMetadata'
Generated Swift enum members:
struct metadataPayload {
var body: Components.Schemas.PhotoMetadata
}
case metadata(MultipartPart<metadataPayload>)
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 typeMultipartRawPart
.Note that while similar to
undocumented
, theother
case uses a different name to communicate the fact that the OpenAPI author deliberately enabledadditionalProperties
and thus any parts with unknown names are expected – so the name “undocumented” would not be appropriate.
OpenAPI:
multipart/form-data:
schema:
type: object
properties:
metadata:
$ref: '#/components/schemas/PhotoMetadata'
additionalProperties: true
Generated Swift enum members:
struct metadataPayload {
var body: Components.Schemas.PhotoMetadata
}
case metadata(MultipartPart<metadataPayload>)
case other(MultipartRawType)
Scenario C: Zero or more documented cases and
additionalProperties: <SCHEMA>
.For each documented case, same as Scenario A.
Also generates an
other
case with an associated typeMultipartDynamicallyNamedPart
.Note that while similar to
MultipartPart
,MultipartDynamicallyNamedPart
adds a read-writename
property, because while the part name is statically known whenMultipartPart
is used, that’s not the case whenMultipartDynamicallyNamedPart
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 anotherPayload
generated struct like for statically documented parts.
OpenAPI:
multipart/form-data:
schema:
type: object
properties:
metadata:
$ref: '#/components/schemas/PhotoMetadata'
additionalProperties:
$ref: '#/components/schemas/OtherInfo'
Generated Swift enum members:
struct metadataPayload {
var body: Components.Schemas.PhotoMetadata
}
case metadata(MultipartPart<metadataPayload>)
case other(MultipartDynamicallyNamedPart<Components.Schemas.OtherInfo>)
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:
multipart/form-data:
schema:
type: object
properties:
metadata:
$ref: '#/components/schemas/PhotoMetadata'
additionalProperties: false
Generated Swift enum members:
struct metadataPayload {
var body: Components.Schemas.PhotoMetadata
}
case metadata(MultipartPart<metadataPayload>)
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 throughif
additionalProperties: true
, the sequence never fails validation when an undocumented part is encounteredif
additionalProperties: false
, the sequence fails validation if an undocumented part is encounteredif
additionalProperties: <SCHEMA>
, the sequence doesn’t fail validation, but will fail later if the provided part can’t be parsed as<SCHEMA>
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:
explicitly specify the content type
image/jpeg
for thecontents
part.define a custom header field
x-sender-id
for themetadata
part.
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:
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 acontentEncoding
, the default Content-Type isapplication/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.
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 generateMultipartBody<Part>
, 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 for helping refine this proposal with thoughtful feedback.
Appendix A: Example OpenAPI document with multipart bodies
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
New API to represent a boundary generator.
/// 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
}
Customizing the boundary generator on
Configuration
.
The below property and initializer added to the Configuration struct, while the existing initializer is deprecated.
/// 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)
/* } */
New multipart part types.
/// 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<Payload> : 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<Payload> : 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)
}
New multipart body async sequence type.
/// 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<MyPartType> = [
/// .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<MyPartType> = ...
/// 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<Part> : @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<Part>, rhs: OpenAPIRuntime.MultipartBody<Part>) -> 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<Input>(_ 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<Part> & 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<OpenAPIRuntime.MultipartBody<Part>.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<OpenAPIRuntime.MultipartBody<Part>.Element>)
}
extension MultipartBody : ExpressibleByArrayLiteral {
public typealias ArrayLiteralElement = OpenAPIRuntime.MultipartBody<Part>.Element
public convenience init(arrayLiteral elements: OpenAPIRuntime.MultipartBody<Part>.Element...)
}
extension MultipartBody : AsyncSequence {
public typealias Element = Part
public typealias AsyncIterator = OpenAPIRuntime.MultipartBody<Part>.Iterator
public func makeAsyncIterator() -> OpenAPIRuntime.MultipartBody<Part>.AsyncIterator
}
extension MultipartBody {
public struct Iterator : AsyncIteratorProtocol {
public mutating func next() async throws -> OpenAPIRuntime.MultipartBody<Part>.Element?
}
}
Move
HTTPBody.IterationBehavior
to the top of the module, asOpenAPIRuntime.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.