@@ -73,16 +73,23 @@ public final class RESTClient: Sendable {
7373 case json( Encodable & Sendable )
7474 case string( String )
7575 case form( [ URLQueryItem ] )
76+ case multipart( [ MultipartItem ] , requestID: UUID )
7677
7778 var contentType : String {
7879 switch self {
79- case . binary: return " application/octet-stream "
80- case . json: return " application/json "
81- case . string: return " text/plain "
82- case . form: return " application/x-www-form-urlencoded "
80+ case . binary: " application/octet-stream "
81+ case . json: " application/json "
82+ case . string: " text/plain "
83+ case . form: " application/x-www-form-urlencoded "
84+ case . multipart( _, let requestID) : " multipart/form-data; boundary=handy-swift-boundary- \( requestID. uuidString) "
8385 }
8486 }
8587
88+ /// Creates a multipart body with a generated UUID for request identification.
89+ public static func multipart( _ items: [ MultipartItem ] ) -> Body {
90+ . multipart( items, requestID: UUID ( ) )
91+ }
92+
8693 func httpData( jsonEncoder: JSONEncoder ) throws -> Data {
8794 switch self {
8895 case . binary( let data) :
@@ -98,10 +105,98 @@ public final class RESTClient: Sendable {
98105 var urlComponents = URLComponents ( string: " https://example.com " ) !
99106 urlComponents. queryItems = queryItems
100107 return Data ( urlComponents. percentEncodedQuery!. utf8)
108+
109+ case . multipart( let items, let requestID) :
110+ let boundaryString = " handy-swift-boundary- \( requestID. uuidString) "
111+ var body = Data ( )
112+
113+ for item in items {
114+ // Add boundary separator
115+ body. append ( Data ( " -- \( boundaryString) \r \n " . utf8) )
116+
117+ // Add Content-Disposition header
118+ var contentDisposition = " Content-Disposition: form-data; name= \" \( item. name) \" "
119+
120+ switch item. value {
121+ case . text( let text) :
122+ body. append ( Data ( " \( contentDisposition) \r \n \r \n " . utf8) )
123+ body. append ( Data ( text. utf8) )
124+
125+ case . data( let data, let fileName, let mimeType) :
126+ if let fileName = fileName {
127+ contentDisposition += " ; filename= \" \( fileName) \" "
128+ }
129+ body. append ( Data ( " \( contentDisposition) \r \n " . utf8) )
130+
131+ if let mimeType = mimeType {
132+ body. append ( Data ( " Content-Type: \( mimeType) \r \n " . utf8) )
133+ }
134+
135+ body. append ( Data ( " \r \n " . utf8) )
136+ body. append ( data)
137+
138+ case . json( let json) :
139+ body. append ( Data ( " \( contentDisposition) \r \n " . utf8) )
140+ body. append ( Data ( " Content-Type: application/json \r \n \r \n " . utf8) )
141+ let jsonData = try jsonEncoder. encode ( json)
142+ body. append ( jsonData)
143+ }
144+
145+ body. append ( Data ( " \r \n " . utf8) )
146+ }
147+
148+ // Add final boundary
149+ body. append ( Data ( " -- \( boundaryString) -- \r \n " . utf8) )
150+
151+ return body
101152 }
102153 }
103154 }
104155
156+ /// A name-value pair for multipart/form-data requests, following the URLQueryItem pattern.
157+ /// Used to construct multipart request bodies in a type-safe, explorable way.
158+ ///
159+ /// Example usage:
160+ /// ```swift
161+ /// let multipartItems: [RESTClient.MultipartItem] = [
162+ /// .init(name: "model", value: .text("gpt-image-1")),
163+ /// .init(name: "prompt", value: .text("Generate a liquid glass icon")),
164+ /// .init(name: "image", value: .data(imageData, fileName: "input.png", mimeType: "image/png"))
165+ /// ]
166+ /// ```
167+ public struct MultipartItem : Sendable {
168+ /// The name of the form field.
169+ public let name : String
170+
171+ /// The value of the form field, which can be text, binary data, or JSON.
172+ public let value : MultipartValue
173+
174+ /// Creates a new multipart item with the specified name and value.
175+ /// - Parameters:
176+ /// - name: The name of the form field
177+ /// - value: The value of the form field
178+ public init ( name: String , value: MultipartValue ) {
179+ self . name = name
180+ self . value = value
181+ }
182+ }
183+
184+ /// The value types supported in multipart/form-data requests.
185+ public enum MultipartValue : Sendable {
186+ /// A plain text value.
187+ case text( String )
188+
189+ /// Binary data with optional filename and MIME type.
190+ /// - Parameters:
191+ /// - data: The binary data to include
192+ /// - fileName: Optional filename for the uploaded file
193+ /// - mimeType: Optional MIME type (e.g., "image/png", "application/json")
194+ case data( Data , fileName: String ? , mimeType: String ? )
195+
196+ /// A JSON-encodable value that will be serialized to JSON.
197+ case json( Encodable & Sendable )
198+ }
199+
105200 public protocol RequestPlugin : Sendable {
106201 func apply( to request: inout URLRequest )
107202 }
0 commit comments