Skip to content

Commit

Permalink
Merge pull request #6 from jeffbarg/master
Browse files Browse the repository at this point in the history
Allow Multiple Multipart Uploads
  • Loading branch information
s4cha authored Jul 29, 2020
2 parents 430ce76 + a5785d0 commit 99d8118
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 19 deletions.
11 changes: 10 additions & 1 deletion Sources/Networking/Calls/NetworkingClient+Multipart.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,21 @@ import Combine
public extension NetworkingClient {

func post(_ route: String, params: Params = Params(), multipartData: MultipartData) -> AnyPublisher<(Data?, Progress), Error> {
return post(route, params: params, multipartData: [multipartData])
}

func put(_ route: String, params: Params = Params(), multipartData: MultipartData) -> AnyPublisher<(Data?, Progress), Error> {
return put(route, params: params, multipartData: [multipartData])
}

// Allow multiple multipart data
func post(_ route: String, params: Params = Params(), multipartData: [MultipartData]) -> AnyPublisher<(Data?, Progress), Error> {
let r = request(.post, route, params: params)
r.multipartData = multipartData
return r.uploadPublisher()
}

func put(_ route: String, params: Params = Params(), multipartData: MultipartData) -> AnyPublisher<(Data?, Progress), Error> {
func put(_ route: String, params: Params = Params(), multipartData: [MultipartData]) -> AnyPublisher<(Data?, Progress), Error> {
let r = request(.put, route, params: params)
r.multipartData = multipartData
return r.uploadPublisher()
Expand Down
12 changes: 12 additions & 0 deletions Sources/Networking/Multipart/HttpBodyConvertable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// HttpBodyConvertable.swift
//
//
// Created by Jeff Barg on 07/22/2020.
//

import Foundation

public protocol HttpBodyConvertable {
func buildHttpBodyPart(boundary: String) -> Data
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,22 @@

import Foundation

extension MultipartData {

func buildHttpBody(boundary: String) -> Data {
extension MultipartData: HttpBodyConvertable {
public func buildHttpBodyPart(boundary: String) -> Data {
let httpBody = NSMutableData()
httpBody.appendString("--\(boundary)\r\n")
httpBody.appendString("Content-Disposition: form-data; name=\"\(name)\"; filename=\"\(fileName)\"\r\n")
httpBody.appendString("Content-Type: \(mimeType)\r\n\r\n")
httpBody.append(fileData)
httpBody.appendString("\r\n")
httpBody.appendString("--\(boundary)--")
return httpBody as Data
}
}

fileprivate extension NSMutableData {
func appendString(_ string: String) {
if let data = string.data(using: .utf8) {
self.append(data)
internal extension NSMutableData {
func appendString(_ string: String) {
if let data = string.data(using: .utf8) {
self.append(data)
}
}
}
}
23 changes: 23 additions & 0 deletions Sources/Networking/Multipart/Params+HttpBodyConvertable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// Params+HttpBodyConvertable.swift
//
//
// Created by Jeff Barg on 07/22/2020.
//

import Foundation

extension Params: HttpBodyConvertable {
public func buildHttpBodyPart(boundary: String) -> Data {
let httpBody = NSMutableData()

self.forEach { (name, value) in
httpBody.appendString("--\(boundary)\r\n")
httpBody.appendString("Content-Disposition: form-data; name=\"\(name)\"\r\n\r\n")
httpBody.appendString(value.description)
httpBody.appendString("\r\n")
}

return httpBody as Data
}
}
27 changes: 20 additions & 7 deletions Sources/Networking/NetworkingRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class NetworkingRequest: NSObject {
var httpVerb = HTTPVerb.get
public var params = Params()
var headers = [String: String]()
var multipartData: MultipartData?
var multipartData: [MultipartData]?
var logLevels: NetworkingLogLevel {
get { return logger.logLevels }
set { logger.logLevels = newValue }
Expand Down Expand Up @@ -117,10 +117,9 @@ public class NetworkingRequest: NSObject {
return baseURL + route
}

private func buildURLRequest() -> URLRequest? {

internal func buildURLRequest() -> URLRequest? {
var urlString = baseURL + route
if httpVerb == .get || multipartData != nil {
if httpVerb == .get {
urlString = getURLWithParams()
}

Expand All @@ -147,15 +146,29 @@ public class NetworkingRequest: NSObject {
}

// Multipart
if let multipart = multipartData {
if let multiparts = multipartData {
// Construct a unique boundary to separate values
let boundary = "Boundary-\(UUID().uuidString)"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
request.httpBody = multipart.buildHttpBody(boundary: boundary)
request.httpBody = buildMultipartHttpBody(params: params, multiparts: multiparts, boundary: boundary)
}
return request
}


private func buildMultipartHttpBody(params: Params, multiparts: [MultipartData], boundary: String) -> Data {
// Combine all multiparts together
let allMultiparts: [HttpBodyConvertable] = [params] + multiparts;
let boundaryEnding = "--\(boundary)--".data(using: .utf8)!

// Convert multiparts to boundary-seperated Data and combine them
return allMultiparts
.map { (multipart: HttpBodyConvertable) -> Data in
return multipart.buildHttpBodyPart(boundary: boundary)
}
.reduce(Data.init(), +)
+ boundaryEnding
}

func percentEncodedString() -> String {
return params.map { key, value in
let escapedKey = "\(key)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? ""
Expand Down
99 changes: 99 additions & 0 deletions Tests/NetworkingTests/MultipartRequestTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//
// File.swift
//
//
// Created by Jeff Barg on 7/22/20.
//

import Foundation
import XCTest
import Combine

@testable
import Networking

final class MultipartRequestTests: XCTestCase {
let baseClient: NetworkingClient = NetworkingClient(baseURL: "https://example.com/")
let route = "/api/test"

func testRequestGenerationWithSingleFile() {
// Set up test
let params: Params = [:]
let multipartData = MultipartData(name: "test_name", fileData: "test data".data(using: .utf8)!, fileName: "file.txt", mimeType: "text/plain")

// Construct request
let request = baseClient.request(.post, route, params: params)
request.multipartData = [multipartData]

if let urlRequest = request.buildURLRequest(),
let body = urlRequest.httpBody,
let contentTypeHeader = urlRequest.value(forHTTPHeaderField: "Content-Type") {
// Extract boundary from header
XCTAssert(contentTypeHeader.starts(with: "multipart/form-data; boundary="))
let boundary = contentTypeHeader.replacingOccurrences(of: "multipart/form-data; boundary=", with: "")

// Test correct body construction
let expectedBody = "--\(boundary)\r\nContent-Disposition: form-data; name=\"test_name\"; filename=\"file.txt\"\r\nContent-Type: text/plain\r\n\r\ntest data\r\n--\(boundary)--"
let actualBody = String(data: body, encoding: .utf8)
XCTAssertEqual(actualBody, expectedBody)
}
else {
XCTFail("Properly-formed URL request was not constructed")
}
}

func testRequestGenerationWithParams() {
// Set up test
let params: Params = ["test_name": "test_value"]
let multipartData = MultipartData(name: "test_name", fileData: "test data".data(using: .utf8)!, fileName: "file.txt", mimeType: "text/plain")

// Construct request
let request = baseClient.request(.post, route, params: params)
request.multipartData = [multipartData]

if let urlRequest = request.buildURLRequest(),
let body = urlRequest.httpBody,
let contentTypeHeader = urlRequest.value(forHTTPHeaderField: "Content-Type") {
// Extract boundary from header
XCTAssert(contentTypeHeader.starts(with: "multipart/form-data; boundary="))
let boundary = contentTypeHeader.replacingOccurrences(of: "multipart/form-data; boundary=", with: "")

// Test correct body construction
let expectedBody = "--\(boundary)\r\nContent-Disposition: form-data; name=\"test_name\"\r\n\r\ntest_value\r\n--\(boundary)\r\nContent-Disposition: form-data; name=\"test_name\"; filename=\"file.txt\"\r\nContent-Type: text/plain\r\n\r\ntest data\r\n--\(boundary)--"
let actualBody = String(data: body, encoding: .utf8)
XCTAssertEqual(actualBody, expectedBody)
}
else {
XCTFail("Properly-formed URL request was not constructed")
}
}

func testRequestGenerationWithMultipleFiles() {
// Set up test
let params: Params = [:]
let multipartData = [
MultipartData(name: "test_name", fileData: "test data".data(using: .utf8)!, fileName: "file.txt", mimeType: "text/plain"),
MultipartData(name: "second_name", fileData: "another file".data(using: .utf8)!, fileName: "file2.txt", mimeType: "text/plain"),
]

// Construct request
let request = baseClient.request(.post, route, params: params)
request.multipartData = multipartData

if let urlRequest = request.buildURLRequest(),
let body = urlRequest.httpBody,
let contentTypeHeader = urlRequest.value(forHTTPHeaderField: "Content-Type") {
// Extract boundary from header
XCTAssert(contentTypeHeader.starts(with: "multipart/form-data; boundary="))
let boundary = contentTypeHeader.replacingOccurrences(of: "multipart/form-data; boundary=", with: "")

// Test correct body construction
let expectedBody = "--\(boundary)\r\nContent-Disposition: form-data; name=\"test_name\"; filename=\"file.txt\"\r\nContent-Type: text/plain\r\n\r\ntest data\r\n--\(boundary)\r\nContent-Disposition: form-data; name=\"second_name\"; filename=\"file2.txt\"\r\nContent-Type: text/plain\r\n\r\nanother file\r\n--\(boundary)--"
let actualBody = String(data: body, encoding: .utf8)
XCTAssertEqual(actualBody, expectedBody)
}
else {
XCTFail("Properly-formed URL request was not constructed")
}
}
}
7 changes: 5 additions & 2 deletions Tests/NetworkingTests/NetworkingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ final class NetworkingTests: XCTestCase {
// waitForExpectations(timeout: 3, handler: nil)
// }
//
waitForExpectations(timeout: 3, handler: nil)
}

func testGetDecodableModel() {
let exp = expectation(description: "call")
let api: Api = ConcreteApi()
Expand Down Expand Up @@ -104,8 +107,8 @@ final class NetworkingTests: XCTestCase {
// api.fetchCleanPosts().then { posts in
// exp.fulfill()
// }
waitForExpectations(timeout: 3, handler: nil)
}
// waitForExpectations(timeout: 3, handler: nil)
// }
//
//
// //delete
Expand Down

0 comments on commit 99d8118

Please sign in to comment.