Skip to content

Instantly share code, notes, and snippets.

@KoCMoHaBTa
Last active August 10, 2018 23:12
Show Gist options
  • Save KoCMoHaBTa/a4a422cd5daa1c040ca3ab6803202c70 to your computer and use it in GitHub Desktop.
Save KoCMoHaBTa/a4a422cd5daa1c040ca3ab6803202c70 to your computer and use it in GitHub Desktop.
Multipart Form Data implementation on Swift
//
// MultipartFormData.swift
// https://gist.github.com/KoCMoHaBTa/a4a422cd5daa1c040ca3ab6803202c70
//
// Created by Milen Halachev on 20.07.18.
// Copyright © 2018 Milen Halachev. All rights reserved.
//
import Foundation
public struct MultipartFormData {
///The boundary of the form data. Default to random UUID.
public var boundary: String = "---------------------" + UUID().uuidString + "---------------------"
///The line ending of the form data. Default to `\r\n`
public var lineEnding: String = "\r\n"
///The parts, used to build the form data.
public var parts: [Part] = []
///Creates an instance of the receiver with a single part
public init(parts: [Part]) {
self.parts = parts
}
///Creates an instance of the receiver with multiple parts
public init(part: Part) {
self.init(parts: [part])
}
///Computes the combined data of all parts and returns the result.
public var data: Data {
//if there are no parts - return empty data
guard self.parts.isEmpty == false else {
return Data()
}
let boundary = "--\(self.boundary)".data(using: .utf8)! //preppend -- to the boundary - MIME standart
let lineEnding = self.lineEnding.data(using: .utf8)!
var data = self.parts.reduce(into: Data(), { (data, part) in
let contentDisposition = "Content-Disposition: form-data; name=\"\(part.name)\"; filename=\"\(part.fileName)\"".data(using: .utf8)!
let contentType = "Content-Type: \(part.contentType)".data(using: .utf8)!
data.append(boundary)
data.append(lineEnding)
data.append(contentDisposition)
data.append(lineEnding)
data.append(contentType)
data.append(lineEnding)
data.append(lineEnding) //extra line
data.append(part.data)
data.append(lineEnding)
})
data.append(boundary) //append the last boundar
data.append("--".data(using: .utf8)!) //append -- to the last boundary - MIME standart
return data
}
}
extension MultipartFormData {
public struct Part {
///The binary data of the part.
public let data: Data
///The value of the `Content-Type` header of the part
public let contentType: String
///The value of the `name` parameter in the `Content-Disposition` header section of the part.
public let name: String
///The value of the `filename` parameter in the `Content-Disposition` header section of the part.
public let fileName: String
public init(data: Data, contentType: String, name: String, fileName: String) {
self.data = data
self.contentType = contentType
self.name = name
self.fileName = fileName
}
///Creates an instance of the receiver with the provided data. The name is set to random UUID
public init(data: Data, contentType: String, fileName: String) {
self.init(data: data, contentType: contentType, name: .uuid, fileName: fileName)
}
///Creates an instance of the receiver with the provided data. The name and fileName are set to random UUID
public init(data: Data, contentType: String) {
self.init(data: data, contentType: contentType, name: .uuid, fileName: .uuid)
}
///Creates an instance of the receiver with the provided data. The content type is set to `application/octet-stream` and the name and fileName are set to random GUID
public init(data: Data) {
self.init(data: data, contentType: "application/octet-stream", name: .uuid, fileName: .uuid)
}
}
}
//
// MultipartFormDataTests.swift
// https://gist.github.com/KoCMoHaBTa/a4a422cd5daa1c040ca3ab6803202c70
//
// Created by Milen Halachev on 20.07.18.
// Copyright © 2018 Milen Halachev. All rights reserved.
//
import Foundation
import XCTest
class MultipartFormDataTests: XCTestCase {
//when no parts are provided - produces empty data
func testNoParts() {
let multipart = MultipartFormData(parts: [])
XCTAssertEqual(multipart.data, Data())
}
func testSinglePart() {
let expectedResult =
"""
--812dfde1-e9eb-447c-a4e7-e19d3a32290a
Content-Disposition: form-data; name="gg"; filename="gg.txt"
Content-Type: text/plain
123ABC
--812dfde1-e9eb-447c-a4e7-e19d3a32290a--
"""
var multipart = MultipartFormData(part: MultipartFormData.Part(data: "123ABC".data(using: .utf8)!, contentType: "text/plain", name: "gg", fileName: "gg.txt"))
multipart.boundary = "812dfde1-e9eb-447c-a4e7-e19d3a32290a"
multipart.lineEnding = "\n"
let data = multipart.data
let string = String(data: data, encoding: .utf8)
XCTAssertEqual(expectedResult, string)
}
func testMultipleParts() {
let expectedResult =
"""
--812dfde1-e9eb-447c-a4e7-e19d3a32290a
Content-Disposition: form-data; name="gg"; filename="gg.txt"
Content-Type: text/plain
123ABC
--812dfde1-e9eb-447c-a4e7-e19d3a32290a
Content-Disposition: form-data; name="gson"; filename="omg.json"
Content-Type: application/json
{"value":123}
--812dfde1-e9eb-447c-a4e7-e19d3a32290a
Content-Disposition: form-data; name="snimka"; filename="omg.png"
Content-Type: image/png
lkjaskjldakljfjkdah
--812dfde1-e9eb-447c-a4e7-e19d3a32290a--
"""
let parts = [
MultipartFormData.Part(data: "123ABC".data(using: .utf8)!, contentType: "text/plain", name: "gg", fileName: "gg.txt"),
MultipartFormData.Part(data: try! JSONSerialization.data(withJSONObject: ["value": 123], options: []), contentType: "application/json", name: "gson", fileName: "omg.json"),
MultipartFormData.Part(data: "lkjaskjldakljfjkdah".data(using: .utf8)!, contentType: "image/png", name: "snimka", fileName: "omg.png")
]
var multipart = MultipartFormData(parts: parts)
multipart.boundary = "812dfde1-e9eb-447c-a4e7-e19d3a32290a"
multipart.lineEnding = "\n"
let data = multipart.data
let string = String(data: data, encoding: .utf8)
XCTAssertEqual(expectedResult, string)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment