diff --git a/Sources/RemoveBg/RemoveBgClient.swift b/Sources/RemoveBg/RemoveBgClient.swift index 57ec6e5..c5fc03f 100644 --- a/Sources/RemoveBg/RemoveBgClient.swift +++ b/Sources/RemoveBg/RemoveBgClient.swift @@ -57,24 +57,25 @@ public class RemoveBgClient { return try await fetch(body: body) } - func createRequestBody(options: ApiOptions, configuration: (inout ApiOptions) -> Void) throws -> Data { - let encoder = FormDataEncoder() + func createRequestBody(options: ApiOptions, configuration: (inout ApiOptions) -> Void) throws -> MultipartFormData { + var multipart = MultipartFormData(boundary: boundary) + var options = options options.imageUrl = nil options.imageFileB64 = nil options.imageFile = nil configuration(&options) - - let body = try encoder.encode(options, boundary: boundary) - return body.data(using: .utf8) ?? .init() + + if let data = options.imageFile { + multipart.addField(named: "image_file", filename: "image", data: data) + } + + return multipart } - func fetch(body: Data) async throws -> ApiResult { - var httpRequest = URLRequest(url: RemoveBgClient.endpoint) - httpRequest.httpBody = body - httpRequest.httpMethod = "POST" - httpRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + func fetch(body: MultipartFormData) async throws -> ApiResult { + let httpRequest = URLRequest(url: RemoveBgClient.endpoint, formData: body) let (data, response) = try await urlSession.data(for: httpRequest) if let httpResponse = response as? HTTPURLResponse { @@ -122,3 +123,67 @@ extension ApiResult.Meta { foregroundHeight = parseHeader("X-Foreground-Height", Int.init) } } + + +public struct MultipartFormData { + fileprivate let boundary: String + private var formData = Data() + + init(boundary: String) { + self.boundary = boundary + } + + fileprivate var httpBody: Data { + var data = formData + data.append("--\(boundary)--") + return data + } + + public mutating func addField(named name: String, value: String) { + formData.addField("--\(boundary)") + formData.addField("Content-Disposition: form-data; name=\"\(name)\"") + formData.addField() + formData.addField(value) + } + + public mutating func addField(named name: String, filename: String, data: Data) { + formData.addField("--\(boundary)") + formData.addField("Content-Disposition: form-data; name=\"\(name)\"; filename=\"\(filename)\"") + formData.addField() + formData.addField(data) + } +} + +public extension URLRequest { + init(url: URL, formData: MultipartFormData) { + self.init(url: url) + httpMethod = "POST" + setValue("multipart/form-data; boundary=\(formData.boundary)", forHTTPHeaderField: "Content-Type") + setValue("*/*", forHTTPHeaderField: "Accept") + httpBody = formData.httpBody + } +} + +fileprivate extension Data { + mutating func append(_ string: String) { + append(Data(string.utf8)) + } + + mutating func addField() { + append(.httpFieldDelimiter) + } + + mutating func addField(_ string: String) { + append(string) + append(.httpFieldDelimiter) + } + + mutating func addField(_ data: Data) { + append(data) + append(.httpFieldDelimiter) + } +} + +fileprivate extension String { + static let httpFieldDelimiter = "\r\n" +} diff --git a/Tests/RemoveBgTests/RemoveBgTests.swift b/Tests/RemoveBgTests/RemoveBgTests.swift index dd82df4..2cbd08f 100644 --- a/Tests/RemoveBgTests/RemoveBgTests.swift +++ b/Tests/RemoveBgTests/RemoveBgTests.swift @@ -9,6 +9,8 @@ final class RemoveBgParametersTests: XCTestCase { let sessionConfiguration = URLSessionConfiguration.ephemeral sessionConfiguration.protocolClasses = [MockURLProtocol.self] client = RemoveBgClient(urlSessionConfiguration: sessionConfiguration, apiKey: apiKey) + + addMockResponse(response: .init(url: RemoveBgClient.endpoint, statusCode: 200, httpVersion: nil, headerFields: nil)!, data: "IMAGE".data(using: .utf8)) super.setUp() }