├── .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
--------------------------------------------------------------------------------