├── .gitignore ├── .swiftformat ├── .swiftpm └── xcode │ ├── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ └── davidbeck.xcuserdatad │ │ └── UserInterfaceState.xcuserstate │ └── xcuserdata │ └── davidbeck.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── LICENSE ├── MultipartForm.podspec ├── Package.swift ├── README.md ├── Sources └── MultipartForm │ ├── Data+Helpers.swift │ └── MultipartForm.swift └── Tests ├── LinuxMain.swift └── MultipartFormTests ├── 2.png ├── MultipartFormTests.swift ├── XCTestManifests.swift └── exampleForm /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --header strip 2 | --ranges no-space 3 | --disable redundantGet,redundantRawValues 4 | --self insert 5 | --trimwhitespace "nonblank-lines" 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcuserdata/davidbeck.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davbeck/MultipartForm/50dc16e9b72215f71a61395cb31fa5511a357c40/.swiftpm/xcode/package.xcworkspace/xcuserdata/davidbeck.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /.swiftpm/xcode/xcuserdata/davidbeck.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MultipartForm.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | MultipartForm 16 | 17 | primary 18 | 19 | 20 | MultipartFormTests 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 David Beck 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MultipartForm.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'MultipartForm' 3 | s.version = '0.1.0' 4 | s.summary = 'The missing multipart form support for URLSession.' 5 | 6 | s.homepage = 'https://github.com/davbeck/MultipartForm' 7 | s.license = { :type => 'MIT', :file => 'LICENSE' } 8 | s.author = { 'davbeck' => 'code@davidbeck.co' } 9 | s.source = { :git => 'https://github.com/davbeck/MultipartForm.git', :tag => s.version.to_s } 10 | s.social_media_url = 'https://twitter.com/davbeck' 11 | 12 | s.ios.deployment_target = '9.0' 13 | s.osx.deployment_target = '10.10' 14 | 15 | s.swift_version = '5.1' 16 | s.source_files = 'Sources/MultipartForm/*.swift' 17 | 18 | s.test_spec 'MultipartFormTests' do |test_spec| 19 | test_spec.source_files = 'Tests/MultipartFormTests/*' 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "MultipartForm", 6 | products: [ 7 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 8 | .library( 9 | name: "MultipartForm", 10 | targets: ["MultipartForm"] 11 | ), 12 | ], 13 | dependencies: [ 14 | // Dependencies declare other packages that this package depends on. 15 | // .package(url: /* package url */, from: "1.0.0"), 16 | ], 17 | targets: [ 18 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 19 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 20 | .target( 21 | name: "MultipartForm", 22 | dependencies: [] 23 | ), 24 | .testTarget( 25 | name: "MultipartFormTests", 26 | dependencies: ["MultipartForm"] 27 | ), 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MultipartForm 2 | 3 | A simple way to create multipart form requests in Swift. 4 | 5 | ## Example 6 | 7 | ```swift 8 | import MultipartForm 9 | 10 | let form = MultipartForm(parts: [ 11 | MultipartForm.Part(name: "a", value: "1"), 12 | MultipartForm.Part(name: "b", value: "2"), 13 | MultipartForm.Part(name: "c", data: imageData, filename: "3.png", contentType: "image/png"), 14 | ]) 15 | 16 | var request = URLRequest(url: url) 17 | request.httpMethod = "POST" 18 | request.setValue(form.contentType, forHTTPHeaderField: "Content-Type") 19 | 20 | let task = session.uploadTask(with: request, from: form.bodyData) 21 | task.resume() 22 | ``` 23 | 24 | To upload a file in the background, you can write out `form.bodyData` to a file and create an upload task from that. 25 | -------------------------------------------------------------------------------- /Sources/MultipartForm/Data+Helpers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Data { 4 | mutating func append(_ string: String) { 5 | self.append(string.data(using: .utf8, allowLossyConversion: true)!) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Sources/MultipartForm/MultipartForm.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct MultipartForm: Hashable, Equatable { 4 | public struct Part: Hashable, Equatable { 5 | public var name: String 6 | public var data: Data 7 | public var filename: String? 8 | public var contentType: String? 9 | 10 | public var value: String? { 11 | get { 12 | return String(bytes: self.data, encoding: .utf8) 13 | } 14 | set { 15 | guard let value = newValue else { 16 | self.data = Data() 17 | return 18 | } 19 | 20 | self.data = value.data(using: .utf8, allowLossyConversion: true)! 21 | } 22 | } 23 | 24 | public init(name: String, data: Data, filename: String? = nil, contentType: String? = nil) { 25 | self.name = name 26 | self.data = data 27 | self.filename = filename 28 | self.contentType = contentType 29 | } 30 | 31 | public init(name: String, value: String) { 32 | let data = value.data(using: .utf8, allowLossyConversion: true)! 33 | self.init(name: name, data: data, filename: nil, contentType: nil) 34 | } 35 | } 36 | 37 | public enum MultipartType: String { 38 | case formData = "form-data" 39 | case mixed = "mixed" 40 | } 41 | 42 | public var boundary: String 43 | public var parts: [Part] 44 | public var multipartType: MultipartType 45 | 46 | public var contentType: String { 47 | return "multipart/\(multipartType.rawValue); boundary=\(self.boundary)" 48 | } 49 | 50 | public var bodyData: Data { 51 | var body = Data() 52 | for part in self.parts { 53 | body.append("--\(self.boundary)\r\n") 54 | body.append("Content-Disposition: form-data; name=\"\(part.name)\"") 55 | if let filename = part.filename?.replacingOccurrences(of: "\"", with: "_") { 56 | body.append("; filename=\"\(filename)\"") 57 | } 58 | body.append("\r\n") 59 | if let contentType = part.contentType { 60 | body.append("Content-Type: \(contentType)\r\n") 61 | } 62 | body.append("\r\n") 63 | body.append(part.data) 64 | body.append("\r\n") 65 | } 66 | body.append("--\(self.boundary)--\r\n") 67 | 68 | return body 69 | } 70 | 71 | public init(parts: [Part] = [], boundary: String = UUID().uuidString, multipartType: MultipartType = .formData) { 72 | self.parts = parts 73 | self.boundary = boundary 74 | self.multipartType = multipartType 75 | } 76 | 77 | public subscript(name: String) -> Part? { 78 | get { 79 | return self.parts.first(where: { $0.name == name }) 80 | } 81 | set { 82 | precondition(newValue == nil || newValue?.name == name) 83 | 84 | var parts = self.parts 85 | parts = parts.filter { $0.name != name } 86 | if let newValue = newValue { 87 | parts.append(newValue) 88 | } 89 | self.parts = parts 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import MultipartFormTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += MultipartFormTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/MultipartFormTests/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davbeck/MultipartForm/50dc16e9b72215f71a61395cb31fa5511a357c40/Tests/MultipartFormTests/2.png -------------------------------------------------------------------------------- /Tests/MultipartFormTests/MultipartFormTests.swift: -------------------------------------------------------------------------------- 1 | @testable import MultipartForm 2 | import XCTest 3 | 4 | final class MultipartFormTests: XCTestCase { 5 | func testExample() throws { 6 | let imageURL = URL(fileURLWithPath: #file).deletingLastPathComponent().appendingPathComponent("2.png") 7 | let imageData = try Data(contentsOf: imageURL) 8 | 9 | let formURL = URL(fileURLWithPath: #file).deletingLastPathComponent().appendingPathComponent("exampleForm") 10 | let formData = try Data(contentsOf: formURL) 11 | 12 | let form = MultipartForm(parts: [ 13 | MultipartForm.Part(name: "a", value: "1"), 14 | MultipartForm.Part(name: "b", data: imageData, filename: "b.txt", contentType: "text/plain"), 15 | MultipartForm.Part(name: "c", value: "3"), 16 | ], boundary: "9BFDAA7B-7244-4DA8-916B-2311D3CD1FEE") 17 | 18 | XCTAssertEqual(form.bodyData, formData) 19 | } 20 | 21 | func testContentType() { 22 | let form = MultipartForm(boundary: "9BFDAA7B-7244-4DA8-916B-2311D3CD1FEE") 23 | 24 | XCTAssertEqual(form.contentType, "multipart/form-data; boundary=9BFDAA7B-7244-4DA8-916B-2311D3CD1FEE") 25 | } 26 | 27 | func testMultipartMixedType() { 28 | let form = MultipartForm(boundary: "9BFDAA7B-7244-4DA8-916B-2311D3CD1FEE", multipartType: .mixed) 29 | 30 | XCTAssertEqual(form.contentType, "multipart/mixed; boundary=9BFDAA7B-7244-4DA8-916B-2311D3CD1FEE") 31 | } 32 | 33 | func testMultipartFormDataType() { 34 | let form = MultipartForm(boundary: "9BFDAA7B-7244-4DA8-916B-2311D3CD1FEE", multipartType: .formData) 35 | 36 | XCTAssertEqual(form.contentType, "multipart/form-data; boundary=9BFDAA7B-7244-4DA8-916B-2311D3CD1FEE") 37 | } 38 | 39 | func testSubscriptGet() { 40 | let form = MultipartForm(parts: [ 41 | MultipartForm.Part(name: "a", value: "1"), 42 | MultipartForm.Part(name: "a", value: "2"), 43 | MultipartForm.Part(name: "c", value: "3"), 44 | ]) 45 | 46 | XCTAssertEqual(form["a"]?.value, "1") 47 | XCTAssertNil(form["b"]) 48 | XCTAssertEqual(form["c"]?.value, "3") 49 | } 50 | 51 | func testSubscriptSet() { 52 | var form = MultipartForm(parts: [ 53 | MultipartForm.Part(name: "a", value: "1"), 54 | MultipartForm.Part(name: "a", value: "2"), 55 | MultipartForm.Part(name: "c", value: "3"), 56 | ]) 57 | 58 | form["a"] = MultipartForm.Part(name: "a", value: "3") 59 | XCTAssertEqual(form["a"]?.value, "3") 60 | 61 | XCTAssertEqual(form.parts, [ 62 | MultipartForm.Part(name: "c", value: "3"), 63 | MultipartForm.Part(name: "a", value: "3"), 64 | ]) 65 | } 66 | 67 | func testGetValue() { 68 | let part = MultipartForm.Part(name: "a", data: "1".data(using: .utf8)!) 69 | 70 | XCTAssertEqual(part.value, "1") 71 | } 72 | 73 | func testSetValue() { 74 | var part = MultipartForm.Part(name: "a", value: "1") 75 | 76 | part.value = "2" 77 | XCTAssertEqual(part.value, "2") 78 | XCTAssertEqual(part.data, "2".data(using: .utf8)!) 79 | } 80 | 81 | func testSetNilValue() { 82 | var part = MultipartForm.Part(name: "a", value: "1") 83 | 84 | part.value = nil 85 | XCTAssertEqual(part.value, "") 86 | XCTAssertEqual(part.data, Data()) 87 | } 88 | 89 | static var allTests = [ 90 | ("testExample", testExample), 91 | ("testContentType", testContentType), 92 | ("testSubscriptGet", testSubscriptGet), 93 | ("testSubscriptSet", testSubscriptSet), 94 | ("testGetValue", testGetValue), 95 | ("testSetValue", testSetValue), 96 | ("testSetNilValue", testSetNilValue), 97 | ] 98 | } 99 | -------------------------------------------------------------------------------- /Tests/MultipartFormTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if os(Linux) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(MultipartFormTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/MultipartFormTests/exampleForm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davbeck/MultipartForm/50dc16e9b72215f71a61395cb31fa5511a357c40/Tests/MultipartFormTests/exampleForm --------------------------------------------------------------------------------