Skip to content
98 changes: 96 additions & 2 deletions Sources/OpenAPIRuntime/Conversion/Converter+Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ extension Converter {
/// - Throws: An error if setting the request body as binary fails.
public func setOptionalRequestBodyAsBinary(_ value: HTTPBody?, headerFields: inout HTTPFields, contentType: String)
throws -> HTTPBody?
{ try setOptionalRequestBody(value, headerFields: &headerFields, contentType: contentType, convert: { $0 }) }
{ setOptionalRequestBody(value, headerFields: &headerFields, contentType: contentType, convert: { $0 }) }

/// Sets a required request body as binary in the specified header fields and returns an `HTTPBody`.
///
Expand All @@ -154,7 +154,7 @@ extension Converter {
/// - Throws: An error if setting the request body as binary fails.
public func setRequiredRequestBodyAsBinary(_ value: HTTPBody, headerFields: inout HTTPFields, contentType: String)
throws -> HTTPBody
{ try setRequiredRequestBody(value, headerFields: &headerFields, contentType: contentType, convert: { $0 }) }
{ setRequiredRequestBody(value, headerFields: &headerFields, contentType: contentType, convert: { $0 }) }

/// Sets an optional request body as URL-encoded form data in the specified header fields and returns an `HTTPBody`.
///
Expand Down Expand Up @@ -202,6 +202,56 @@ extension Converter {
)
}

/// Sets a required request body as multipart and returns the streaming body.
///
/// - Parameters:
/// - value: The multipart body to be set as the request body.
/// - headerFields: The header fields in which to set the content type.
/// - contentType: The content type to be set in the header fields.
/// - allowsUnknownParts: A Boolean value indicating whether parts with unknown names
/// should be pass through. If `false`, encountering an unknown part throws an error
/// whent the returned body sequence iterates it.
/// - requiredExactlyOncePartNames: The list of part names that are required exactly once.
/// - requiredAtLeastOncePartNames: The list of part names that are required at least once.
/// - atMostOncePartNames: The list of part names that can appear at most once.
/// - zeroOrMoreTimesPartNames: The list of names that can appear any number of times.
/// - encode: A closure that transforms the type-safe part into a raw part.
/// - Returns: A streaming body representing the multipart-encoded request body.
/// - Throws: Currently never, but might in the future.
public func setRequiredRequestBodyAsMultipart<Part: Sendable>(
_ value: MultipartBody<Part>,
headerFields: inout HTTPFields,
contentType: String,
allowsUnknownParts: Bool,
requiredExactlyOncePartNames: Set<String>,
requiredAtLeastOncePartNames: Set<String>,
atMostOncePartNames: Set<String>,
zeroOrMoreTimesPartNames: Set<String>,
encoding encode: @escaping @Sendable (Part) throws -> MultipartRawPart
) throws -> HTTPBody {
let boundary = configuration.multipartBoundaryGenerator.makeBoundary()
let contentTypeWithBoundary = contentType + "; boundary=\(boundary)"
return setRequiredRequestBody(
value,
headerFields: &headerFields,
contentType: contentTypeWithBoundary,
convert: { value in
convertMultipartToBytes(
value,
requirements: .init(
allowsUnknownParts: allowsUnknownParts,
requiredExactlyOncePartNames: requiredExactlyOncePartNames,
requiredAtLeastOncePartNames: requiredAtLeastOncePartNames,
atMostOncePartNames: atMostOncePartNames,
zeroOrMoreTimesPartNames: zeroOrMoreTimesPartNames
),
boundary: boundary,
encode: encode
)
}
)
}

/// Retrieves the response body as JSON and transforms it into a specified type.
///
/// - Parameters:
Expand Down Expand Up @@ -244,4 +294,48 @@ extension Converter {
guard let data else { throw RuntimeError.missingRequiredResponseBody }
return try getResponseBody(type, from: data, transforming: transform, convert: { $0 })
}
/// Returns an async sequence of multipart parts parsed from the provided body stream.
///
/// - Parameters:
/// - type: The type representing the type-safe multipart body.
/// - data: The HTTP body data to transform.
/// - transform: A closure that transforms the multipart body into the output type.
/// - boundary: The multipart boundary string.
/// - allowsUnknownParts: A Boolean value indicating whether parts with unknown names
/// should be pass through. If `false`, encountering an unknown part throws an error
/// whent the returned body sequence iterates it.
/// - requiredExactlyOncePartNames: The list of part names that are required exactly once.
/// - requiredAtLeastOncePartNames: The list of part names that are required at least once.
/// - atMostOncePartNames: The list of part names that can appear at most once.
/// - zeroOrMoreTimesPartNames: The list of names that can appear any number of times.
/// - decoder: A closure that parses a raw part into a type-safe part.
/// - Returns: A value of the output type.
/// - Throws: If the transform closure throws.
public func getResponseBodyAsMultipart<C, Part: Sendable>(
_ type: MultipartBody<Part>.Type,
from data: HTTPBody?,
transforming transform: @escaping @Sendable (MultipartBody<Part>) throws -> C,
boundary: String,
allowsUnknownParts: Bool,
requiredExactlyOncePartNames: Set<String>,
requiredAtLeastOncePartNames: Set<String>,
atMostOncePartNames: Set<String>,
zeroOrMoreTimesPartNames: Set<String>,
decoding decoder: @escaping @Sendable (MultipartRawPart) async throws -> Part
) throws -> C {
guard let data else { throw RuntimeError.missingRequiredResponseBody }
let multipart = convertBytesToMultipart(
data,
boundary: boundary,
requirements: .init(
allowsUnknownParts: allowsUnknownParts,
requiredExactlyOncePartNames: requiredExactlyOncePartNames,
requiredAtLeastOncePartNames: requiredAtLeastOncePartNames,
atMostOncePartNames: atMostOncePartNames,
zeroOrMoreTimesPartNames: zeroOrMoreTimesPartNames
),
transform: decoder
)
return try transform(multipart)
}
}
23 changes: 23 additions & 0 deletions Sources/OpenAPIRuntime/Conversion/Converter+Common.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,29 @@ extension Converter {
return bestContentType
}

/// Verifies the MIME type from the content-type header, if present.
/// - Parameters:
/// - headerFields: The header fields to inspect for the content type header.
/// - match: The content type to verify.
/// - Throws: If the content type is incompatible or malformed.
public func verifyContentTypeIfPresent(in headerFields: HTTPFields, matches match: String) throws {
guard let rawValue = headerFields[.contentType] else { return }
_ = try bestContentType(received: .init(rawValue), options: [match])
}

/// Returns the name and file name parameter values from the `content-disposition` header field, if found.
/// - Parameter headerFields: The header fields to inspect for a `content-disposition` header field.
/// - Returns: A tuple of the name and file name string values.
/// - Throws: Currently doesn't, but might in the future.
public func extractContentDispositionNameAndFilename(in headerFields: HTTPFields) throws -> (
name: String?, filename: String?
) {
guard let rawValue = headerFields[.contentDisposition],
let contentDisposition = ContentDisposition(rawValue: rawValue)
else { return (nil, nil) }
return (contentDisposition.name, contentDisposition.filename)
}

// MARK: - Converter helper methods

/// Sets a header field with an optional value, encoding it as a URI component if not nil.
Expand Down
97 changes: 96 additions & 1 deletion Sources/OpenAPIRuntime/Conversion/Converter+Server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,51 @@ extension Converter {
)
}

/// Returns an async sequence of multipart parts parsed from the provided body stream.
///
/// - Parameters:
/// - type: The type representing the type-safe multipart body.
/// - data: The HTTP body data to transform.
/// - transform: A closure that transforms the multipart body into the output type.
/// - boundary: The multipart boundary string.
/// - allowsUnknownParts: A Boolean value indicating whether parts with unknown names
/// should be pass through. If `false`, encountering an unknown part throws an error
/// whent the returned body sequence iterates it.
/// - requiredExactlyOncePartNames: The list of part names that are required exactly once.
/// - requiredAtLeastOncePartNames: The list of part names that are required at least once.
/// - atMostOncePartNames: The list of part names that can appear at most once.
/// - zeroOrMoreTimesPartNames: The list of names that can appear any number of times.
/// - decoder: A closure that parses a raw part into a type-safe part.
/// - Returns: A value of the output type.
/// - Throws: If the transform closure throws.
public func getRequiredRequestBodyAsMultipart<C, Part: Sendable>(
_ type: MultipartBody<Part>.Type,
from data: HTTPBody?,
transforming transform: @escaping @Sendable (MultipartBody<Part>) throws -> C,
boundary: String,
allowsUnknownParts: Bool,
requiredExactlyOncePartNames: Set<String>,
requiredAtLeastOncePartNames: Set<String>,
atMostOncePartNames: Set<String>,
zeroOrMoreTimesPartNames: Set<String>,
decoding decoder: @escaping @Sendable (MultipartRawPart) async throws -> Part
) throws -> C {
guard let data else { throw RuntimeError.missingRequiredRequestBody }
let multipart = convertBytesToMultipart(
data,
boundary: boundary,
requirements: .init(
allowsUnknownParts: allowsUnknownParts,
requiredExactlyOncePartNames: requiredExactlyOncePartNames,
requiredAtLeastOncePartNames: requiredAtLeastOncePartNames,
atMostOncePartNames: atMostOncePartNames,
zeroOrMoreTimesPartNames: zeroOrMoreTimesPartNames
),
transform: decoder
)
return try transform(multipart)
}

/// Sets the response body as JSON data, serializing the provided value.
///
/// - Parameters:
Expand Down Expand Up @@ -313,5 +358,55 @@ extension Converter {
/// - Throws: An error if there are issues setting the response body or updating the header fields.
public func setResponseBodyAsBinary(_ value: HTTPBody, headerFields: inout HTTPFields, contentType: String) throws
-> HTTPBody
{ try setResponseBody(value, headerFields: &headerFields, contentType: contentType, convert: { $0 }) }
{ setResponseBody(value, headerFields: &headerFields, contentType: contentType, convert: { $0 }) }

/// Sets a response body as multipart and returns the streaming body.
///
/// - Parameters:
/// - value: The multipart body to be set as the response body.
/// - headerFields: The header fields in which to set the content type.
/// - contentType: The content type to be set in the header fields.
/// - allowsUnknownParts: A Boolean value indicating whether parts with unknown names
/// should be pass through. If `false`, encountering an unknown part throws an error
/// whent the returned body sequence iterates it.
/// - requiredExactlyOncePartNames: The list of part names that are required exactly once.
/// - requiredAtLeastOncePartNames: The list of part names that are required at least once.
/// - atMostOncePartNames: The list of part names that can appear at most once.
/// - zeroOrMoreTimesPartNames: The list of names that can appear any number of times.
/// - encode: A closure that transforms the type-safe part into a raw part.
/// - Returns: A streaming body representing the multipart-encoded response body.
/// - Throws: Currently never, but might in the future.
public func setResponseBodyAsMultipart<Part: Sendable>(
_ value: MultipartBody<Part>,
headerFields: inout HTTPFields,
contentType: String,
allowsUnknownParts: Bool,
requiredExactlyOncePartNames: Set<String>,
requiredAtLeastOncePartNames: Set<String>,
atMostOncePartNames: Set<String>,
zeroOrMoreTimesPartNames: Set<String>,
encoding encode: @escaping @Sendable (Part) throws -> MultipartRawPart
) throws -> HTTPBody {
let boundary = configuration.multipartBoundaryGenerator.makeBoundary()
let contentTypeWithBoundary = contentType + "; boundary=\(boundary)"
return setResponseBody(
value,
headerFields: &headerFields,
contentType: contentTypeWithBoundary,
convert: { value in
convertMultipartToBytes(
value,
requirements: .init(
allowsUnknownParts: allowsUnknownParts,
requiredExactlyOncePartNames: requiredExactlyOncePartNames,
requiredAtLeastOncePartNames: requiredAtLeastOncePartNames,
atMostOncePartNames: atMostOncePartNames,
zeroOrMoreTimesPartNames: zeroOrMoreTimesPartNames
),
boundary: boundary,
encode: encode
)
}
)
}
}
55 changes: 49 additions & 6 deletions Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,52 @@ extension Converter {
return HTTPBody(encodedString)
}

/// Returns a serialized multipart body stream.
/// - Parameters:
/// - multipart: The multipart body.
/// - requirements: The multipart requirements to enforce. When violated, an error is thrown in the sequence.
/// - boundary: The multipart boundary string.
/// - encode: A closure that converts a typed part into a raw part.
/// - Returns: The serialized body stream.
func convertMultipartToBytes<Part: Sendable>(
_ multipart: MultipartBody<Part>,
requirements: MultipartBodyRequirements,
boundary: String,
encode: @escaping @Sendable (Part) throws -> MultipartRawPart
) -> HTTPBody {
let untyped = multipart.map { part in
var untypedPart = try encode(part)
if case .known(let byteCount) = untypedPart.body.length {
untypedPart.headerFields[.contentLength] = String(byteCount)
}
return untypedPart
}
let validated = MultipartValidationSequence(upstream: untyped, requirements: requirements)
let frames = MultipartRawPartsToFramesSequence(upstream: validated)
let bytes = MultipartFramesToBytesSequence(upstream: frames, boundary: boundary)
return HTTPBody(bytes, length: .unknown, iterationBehavior: multipart.iterationBehavior)
}

/// Returns a parsed multipart body.
/// - Parameters:
/// - bytes: The multipart body byte stream.
/// - boundary: The multipart boundary string.
/// - requirements: The multipart requirements to enforce. When violated, an error is thrown in the sequence.
/// - transform: A closure that converts a raw part into a typed part.
/// - Returns: The typed multipart body stream.
func convertBytesToMultipart<Part: Sendable>(
_ bytes: HTTPBody,
boundary: String,
requirements: MultipartBodyRequirements,
transform: @escaping @Sendable (MultipartRawPart) async throws -> Part
) -> MultipartBody<Part> {
let frames = MultipartBytesToFramesSequence(upstream: bytes, boundary: boundary)
let raw = MultipartFramesToRawPartsSequence(upstream: frames)
let validated = MultipartValidationSequence(upstream: raw, requirements: requirements)
let typed = validated.map(transform)
return .init(typed, iterationBehavior: bytes.iterationBehavior)
}

/// Returns a JSON string for the provided encodable value.
/// - Parameter value: The value to encode.
/// - Returns: A JSON string.
Expand Down Expand Up @@ -383,13 +429,12 @@ extension Converter {
/// - contentType: The content type value.
/// - convert: The closure that encodes the value into a raw body.
/// - Returns: The body.
/// - Throws: An error if an issue occurs while encoding the request body or setting the content type.
func setRequiredRequestBody<T>(
_ value: T,
headerFields: inout HTTPFields,
contentType: String,
convert: (T) throws -> HTTPBody
) throws -> HTTPBody {
) rethrows -> HTTPBody {
headerFields[.contentType] = contentType
return try convert(value)
}
Expand All @@ -402,13 +447,12 @@ extension Converter {
/// - contentType: The content type value.
/// - convert: The closure that encodes the value into a raw body.
/// - Returns: The body, if value was not nil.
/// - Throws: An error if an issue occurs while encoding the request body or setting the content type.
func setOptionalRequestBody<T>(
_ value: T?,
headerFields: inout HTTPFields,
contentType: String,
convert: (T) throws -> HTTPBody
) throws -> HTTPBody? {
) rethrows -> HTTPBody? {
guard let value else { return nil }
return try setRequiredRequestBody(
value,
Expand Down Expand Up @@ -547,13 +591,12 @@ extension Converter {
/// - contentType: The content type value.
/// - convert: The closure that encodes the value into a raw body.
/// - Returns: The body, if value was not nil.
/// - Throws: An error if an issue occurs while encoding the request body.
func setResponseBody<T>(
_ value: T,
headerFields: inout HTTPFields,
contentType: String,
convert: (T) throws -> HTTPBody
) throws -> HTTPBody {
) rethrows -> HTTPBody {
headerFields[.contentType] = contentType
return try convert(value)
}
Expand Down
Loading