mirror of
https://github.com/samsonjs/Osiris.git
synced 2026-03-25 08:55:48 +00:00
Convert to Swift package and rewrite the multipart encoder
This commit is contained in:
parent
41769a99e8
commit
c1f4320288
12 changed files with 294 additions and 305 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
.swiftpm
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
Copyright © 2017 1 Second Everyday. All rights reserved. http://1se.co
|
||||
Copyright © 2017 1 Second Everyday. All rights reserved. https://1se.co
|
||||
|
||||
Released under the terms of the MIT license:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,263 +0,0 @@
|
|||
//
|
||||
// Created by Sami Samhuri on 2017-07-28.
|
||||
// Copyright © 2017 1 Second Everyday. All rights reserved.
|
||||
// Released under the terms of the MIT license.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify,
|
||||
// merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies
|
||||
// or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
||||
// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct MultipartEncodingInMemory {
|
||||
let contentType: String
|
||||
let contentLength: Int64
|
||||
let body: Data
|
||||
}
|
||||
|
||||
struct MultipartEncodingOnDisk {
|
||||
let contentType: String
|
||||
let contentLength: Int64
|
||||
let bodyFileURL: URL
|
||||
}
|
||||
|
||||
enum MultipartFormEncodingError: Error {
|
||||
case invalidText(String)
|
||||
case invalidPath(String)
|
||||
case invalidPart(MultipartFormEncoder.Part)
|
||||
case internalError
|
||||
case streamError
|
||||
}
|
||||
|
||||
final class MultipartFormEncoder {
|
||||
struct Part {
|
||||
let data: Data?
|
||||
let dataFileURL: URL?
|
||||
let encoding: String
|
||||
let filename: String?
|
||||
let length: Int64
|
||||
let name: String
|
||||
let type: String
|
||||
|
||||
static func text(name: String, text: String) -> Part? {
|
||||
guard let data = text.data(using: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
return Part(name: name, type: "text/plain; charset=utf-8", encoding: "8bit", data: data)
|
||||
}
|
||||
|
||||
init(name: String, type: String, encoding: String, data: Data, filename: String? = nil) {
|
||||
self.dataFileURL = nil
|
||||
self.name = name
|
||||
self.type = type
|
||||
self.encoding = encoding
|
||||
self.data = data
|
||||
self.filename = filename
|
||||
self.length = Int64(data.count)
|
||||
}
|
||||
|
||||
init(name: String, type: String, encoding: String, dataFileURL: URL, filename: String? = nil) {
|
||||
self.data = nil
|
||||
self.name = name
|
||||
self.type = type
|
||||
self.encoding = encoding
|
||||
self.dataFileURL = dataFileURL
|
||||
self.filename = filename
|
||||
self.length = FileManager.default.sizeOfFile(at: dataFileURL)
|
||||
}
|
||||
|
||||
var isBinary: Bool {
|
||||
return encoding == "binary"
|
||||
}
|
||||
}
|
||||
|
||||
let boundary: String
|
||||
|
||||
private var parts: [Part] = []
|
||||
|
||||
private var contentType: String {
|
||||
return "multipart/form-data; boundary=\"\(boundary)\""
|
||||
}
|
||||
|
||||
private static let boundaryPrefix = "LifeIsMadeOfSeconds"
|
||||
|
||||
class func generateBoundary() -> String {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
return "\(boundaryPrefix)-\(timestamp)"
|
||||
}
|
||||
|
||||
init(boundary: String? = nil) {
|
||||
self.boundary = boundary ?? MultipartFormEncoder.generateBoundary()
|
||||
}
|
||||
|
||||
func addPart(_ part: Part) {
|
||||
assert(part.data != nil || part.dataFileURL != nil)
|
||||
parts.append(part)
|
||||
}
|
||||
|
||||
func addText(name: String, text: String, filename: String? = nil) throws {
|
||||
guard let data = text.data(using: .utf8) else {
|
||||
throw MultipartFormEncodingError.invalidText(text)
|
||||
}
|
||||
let type = "text/plain; charset=utf-8"
|
||||
let part = Part(name: name, type: type, encoding: "8bit", data: data, filename: filename)
|
||||
parts.append(part)
|
||||
}
|
||||
|
||||
func addBinary(name: String, contentType: String, data: Data, filename: String? = nil) {
|
||||
let part = Part(name: name, type: contentType, encoding: "binary", data: data, filename: filename)
|
||||
parts.append(part)
|
||||
}
|
||||
|
||||
func addBinary(name: String, contentType: String, fileURL: URL, filename: String? = nil) {
|
||||
assert(FileManager.default.fileExists(atPath: fileURL.path))
|
||||
let part = Part(name: name, type: contentType, encoding: "binary", dataFileURL: fileURL, filename: filename)
|
||||
parts.append(part)
|
||||
}
|
||||
|
||||
func encodeToMemory() throws -> MultipartEncodingInMemory {
|
||||
let stream = OutputStream.toMemory()
|
||||
stream.open()
|
||||
do {
|
||||
try encode(to: stream)
|
||||
stream.close()
|
||||
guard let data = stream.property(forKey: .dataWrittenToMemoryStreamKey) as? Data else {
|
||||
throw MultipartFormEncodingError.internalError
|
||||
}
|
||||
return MultipartEncodingInMemory(contentType: contentType, contentLength: Int64(data.count), body: data)
|
||||
}
|
||||
catch {
|
||||
stream.close()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
func encodeToDisk(path: String) throws -> MultipartEncodingOnDisk {
|
||||
guard let stream = OutputStream(toFileAtPath: path, append: false) else {
|
||||
throw MultipartFormEncodingError.invalidPath(path)
|
||||
}
|
||||
stream.open()
|
||||
do {
|
||||
try encode(to: stream)
|
||||
stream.close()
|
||||
let fileURL = URL(fileURLWithPath: path)
|
||||
let length = FileManager.default.sizeOfFile(at: fileURL)
|
||||
return MultipartEncodingOnDisk(contentType: contentType, contentLength: length, bodyFileURL: fileURL)
|
||||
}
|
||||
catch {
|
||||
stream.close()
|
||||
_ = try? FileManager.default.removeItem(atPath: path)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
|
||||
private func encode(to stream: OutputStream) throws {
|
||||
for part in parts {
|
||||
try writeHeader(part, to: stream)
|
||||
try writeBody(part, to: stream)
|
||||
try writeFooter(part, to: stream)
|
||||
}
|
||||
}
|
||||
|
||||
private let lineEnd = "\r\n".data(using: .utf8)!
|
||||
|
||||
private func writeHeader(_ part: Part, to stream: OutputStream) throws {
|
||||
let disposition: String
|
||||
if let filename = part.filename {
|
||||
disposition = "Content-Disposition: form-data; name=\"\(part.name)\"; filename=\"\(filename)\""
|
||||
}
|
||||
else {
|
||||
disposition = "Content-Disposition: form-data; name=\"\(part.name)\""
|
||||
}
|
||||
let header = [
|
||||
"--\(boundary)",
|
||||
disposition,
|
||||
"Content-Length: \(part.length)",
|
||||
"Content-Type: \(part.type)",
|
||||
"", // ends with a newline
|
||||
].joined(separator: "\r\n")
|
||||
try writeString(header, to: stream)
|
||||
try writeData(lineEnd, to: stream)
|
||||
}
|
||||
|
||||
private func writeBody(_ part: Part, to stream: OutputStream) throws {
|
||||
if let data = part.data {
|
||||
try writeData(data, to: stream)
|
||||
}
|
||||
else if let fileURL = part.dataFileURL {
|
||||
try writeFile(fileURL, to: stream)
|
||||
}
|
||||
else {
|
||||
throw MultipartFormEncodingError.invalidPart(part)
|
||||
}
|
||||
try writeData(lineEnd, to: stream)
|
||||
}
|
||||
|
||||
private func writeFooter(_ part: Part, to stream: OutputStream) throws {
|
||||
let footer = "--\(boundary)--\r\n\r\n"
|
||||
try writeString(footer, to: stream)
|
||||
}
|
||||
|
||||
private func writeString(_ string: String, to stream: OutputStream) throws {
|
||||
guard let data = string.data(using: .utf8) else {
|
||||
throw MultipartFormEncodingError.invalidText(string)
|
||||
}
|
||||
try writeData(data, to: stream)
|
||||
}
|
||||
|
||||
private func writeData(_ data: Data, to stream: OutputStream) throws {
|
||||
guard !data.isEmpty else {
|
||||
log.warning("Ignoring request to write 0 bytes of data to stream \(stream)")
|
||||
return
|
||||
}
|
||||
try data.withUnsafeBytes { (bytes: UnsafePointer<UInt8>) throws -> Void in
|
||||
let written = stream.write(bytes, maxLength: data.count)
|
||||
if written < 0 {
|
||||
throw MultipartFormEncodingError.streamError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func writeFile(_ url: URL, to stream: OutputStream) throws {
|
||||
guard let inStream = InputStream(fileAtPath: url.path) else {
|
||||
throw MultipartFormEncodingError.streamError
|
||||
}
|
||||
let bufferSize = 128 * 1024
|
||||
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
|
||||
inStream.open()
|
||||
|
||||
defer {
|
||||
buffer.deallocate(capacity: bufferSize)
|
||||
inStream.close()
|
||||
}
|
||||
|
||||
while inStream.hasBytesAvailable {
|
||||
let bytesRead = inStream.read(buffer, maxLength: bufferSize)
|
||||
if bytesRead > 0 {
|
||||
let bytesWritten = stream.write(buffer, maxLength: bytesRead)
|
||||
if bytesWritten < 0 {
|
||||
throw MultipartFormEncodingError.streamError
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw MultipartFormEncodingError.streamError
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
Package.swift
Normal file
28
Package.swift
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// swift-tools-version:5.3
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Osiris",
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "Osiris",
|
||||
targets: ["Osiris"]),
|
||||
],
|
||||
dependencies: [
|
||||
// Dependencies declare other packages that this package depends on.
|
||||
// .package(url: /* package url */, from: "1.0.0"),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "Osiris",
|
||||
dependencies: []),
|
||||
.testTarget(
|
||||
name: "OsirisTests",
|
||||
dependencies: ["Osiris"]),
|
||||
]
|
||||
)
|
||||
38
Readme.md
38
Readme.md
|
|
@ -13,42 +13,24 @@ Create an encoder and then add parts to it as needed:
|
|||
|
||||
```Swift
|
||||
let encoder = MultipartFormEncoder()
|
||||
try! encoder.addText(name: "email", text: "somebody@example.com")
|
||||
try! encoder.addText(name: "password", text: "secret")
|
||||
let avatarData = UIImageJPEGRepresentation(avatar, 1)!
|
||||
encoder.addBinary(name: "avatar.jpg", contentType: "image/jpeg", data: avatarData)
|
||||
encoder.addPart(.text(name: "email", text: "somebody@example.com"))
|
||||
encoder.addPart(.text(name: "password", text: "secret"))
|
||||
let avatarData = UIImage(from: somewhere).jpegData(compressionQuality: 1)
|
||||
encoder.addPart(.binary(name: "avatar", type: "image/jpeg", data: avatarData, filename: "avatar.jpg"))
|
||||
```
|
||||
|
||||
You can encode the entire form as `Data` in memory if it's not very big:
|
||||
The entire form is encoded as `Data` in memory so you may not want to use this for more than a few megabytes at a time:
|
||||
|
||||
```Swift
|
||||
let encoded = try encoder.encodeToMemory()
|
||||
let body = encoder.encode()
|
||||
var request = URLRequest(url: URL(string: "https://example.com/accounts")!)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = encoded.body
|
||||
request.addValue(encoded.contentType, forHTTPHeaderField: "Content-Type")
|
||||
request.addValue("\(encoded.contentLength)", forHTTPHeaderField: "Content-Length")
|
||||
request.httpBody = body.data
|
||||
request.addValue(body.contentType, forHTTPHeaderField: "Content-Type")
|
||||
request.addValue("\(body.contentLength)", forHTTPHeaderField: "Content-Length")
|
||||
// ... whatever you normally do with requests
|
||||
```
|
||||
|
||||
For larger forms you can also stream the encoded form data directly to disk:
|
||||
|
||||
```Swift
|
||||
let path = NSTemporaryDirectory().appending("/form.data")
|
||||
let encoded = try encoder.encodeToDisk(path: path)
|
||||
var request = URLRequest(url: URL(string: "https://example.com/accounts")!)
|
||||
request.httpMethod = "POST"
|
||||
request.addValue(encoded.contentType, forHTTPHeaderField: "Content-Type")
|
||||
request.addValue("\(encoded.contentLength)", forHTTPHeaderField: "Content-Length")
|
||||
let task = URLSession.shared.uploadTask(with: request, fromFile: encoded.bodyFileURL) { maybeData, maybeResponse, maybeError in
|
||||
|
||||
}
|
||||
task.resume()
|
||||
|
||||
```
|
||||
|
||||
You can create and add your own parts using the `MultipartFormEncoder.Part` struct and `MultipartFormEncoder.addPart(_ part: Part)`.
|
||||
|
||||
# HTTPRequest
|
||||
|
||||
Basic usage:
|
||||
|
|
@ -131,7 +113,7 @@ I don't recommend you use `Service` as shown here, but maybe use it as a jumping
|
|||
|
||||
Mostly created by Sami Samhuri for [1SE][]. `FormEncoder.swift` was lifted from [Alamofire][].
|
||||
|
||||
[1SE]: http://1se.co
|
||||
[1SE]: https://1se.co
|
||||
|
||||
# License
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,15 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
extension NSNumber {
|
||||
/// [From Argo](https://github.com/thoughtbot/Argo/blob/3da833411e2633bc01ce89542ac16803a163e0f0/Argo/Extensions/NSNumber.swift)
|
||||
///
|
||||
/// - Returns: `true` if this instance represent a `CFBoolean` under the hood, as opposed to say a double or integer.
|
||||
var isBool: Bool {
|
||||
return CFBooleanGetTypeID() == CFGetTypeID(self)
|
||||
}
|
||||
}
|
||||
|
||||
final class FormEncoder {
|
||||
class func encode(_ parameters: [String: Any]) -> String {
|
||||
var components: [(String, String)] = []
|
||||
|
|
@ -6,6 +6,10 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
enum HTTPMethod: String {
|
||||
case delete
|
||||
case get
|
||||
|
|
@ -44,14 +48,16 @@ final class HTTPRequest {
|
|||
headers[name] = value
|
||||
}
|
||||
|
||||
#if canImport(UIKit)
|
||||
func addMultipartJPEG(name: String, image: UIImage, quality: CGFloat, filename: String? = nil) {
|
||||
guard let data = UIImageJPEGRepresentation(image, quality) else {
|
||||
guard let data = image.jpegData(compressionQuality: quality) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
let part = MultipartFormEncoder.Part(name: name, type: "image/jpeg", encoding: "binary", data: data, filename: filename)
|
||||
let part = MultipartFormEncoder.Part(name: name, content: .binary(data, type: "image/jpeg", filename: filename ?? "image.jpeg"))
|
||||
addPart(part)
|
||||
}
|
||||
#endif
|
||||
|
||||
private func addPart(_ part: MultipartFormEncoder.Part) {
|
||||
// Convert this request to multipart
|
||||
|
|
@ -112,11 +118,11 @@ enum HTTPResponse {
|
|||
|
||||
var bodyString: String {
|
||||
guard let data = self.data else {
|
||||
log.warning("No data found on response: \(self)")
|
||||
NSLog("[WARN] No data found on response: \(self)")
|
||||
return ""
|
||||
}
|
||||
guard let string = String(data: data, encoding: .utf8) else {
|
||||
log.warning("Data is not UTF8: \(data)")
|
||||
NSLog("[WARN] Data is not UTF8: \(data)")
|
||||
return ""
|
||||
}
|
||||
return string
|
||||
|
|
@ -124,13 +130,13 @@ enum HTTPResponse {
|
|||
|
||||
var dictionaryFromJSON: [String : Any] {
|
||||
guard let data = self.data else {
|
||||
log.warning("No data found on response: \(self)")
|
||||
NSLog("[WARN] No data found on response: \(self)")
|
||||
return [:]
|
||||
}
|
||||
do {
|
||||
guard let dictionary = try JSONSerialization.jsonObject(with: data, options: []) as? [String : Any] else {
|
||||
if let parsed = try? JSONSerialization.jsonObject(with: data, options: []) {
|
||||
log.error("Failed to parse JSON as dictionary: \(parsed)")
|
||||
NSLog("[ERROR] Failed to parse JSON as dictionary: \(parsed)")
|
||||
}
|
||||
return [:]
|
||||
}
|
||||
|
|
@ -138,7 +144,7 @@ enum HTTPResponse {
|
|||
}
|
||||
catch {
|
||||
let json = String(data: data, encoding: .utf8) ?? "<invalid data>"
|
||||
log.error("Failed to parse JSON \(json): \(error)")
|
||||
NSLog("[ERROR] Failed to parse JSON \(json): \(error)")
|
||||
return [:]
|
||||
}
|
||||
}
|
||||
100
Sources/Osiris/MultipartFormEncoder.swift
Normal file
100
Sources/Osiris/MultipartFormEncoder.swift
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
//
|
||||
// Created by Sami Samhuri on 2017-07-28.
|
||||
// Copyright © 2017 1 Second Everyday. All rights reserved.
|
||||
// Released under the terms of the MIT license.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify,
|
||||
// merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies
|
||||
// or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
||||
// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension MultipartFormEncoder {
|
||||
struct Body {
|
||||
let contentType: String
|
||||
let data: Data
|
||||
|
||||
var contentLength: Int {
|
||||
data.count
|
||||
}
|
||||
}
|
||||
|
||||
struct Part {
|
||||
enum Content {
|
||||
case text(String)
|
||||
case binary(Data, type: String, filename: String)
|
||||
}
|
||||
|
||||
let name: String
|
||||
let content: Content
|
||||
|
||||
static func text(name: String, value: String) -> Part {
|
||||
Part(name: name, content: .text(value))
|
||||
}
|
||||
|
||||
static func binary(name: String, data: Data, type: String, filename: String) -> Part {
|
||||
Part(name: name, content: .binary(data, type: type, filename: filename))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class MultipartFormEncoder {
|
||||
let boundary: String
|
||||
|
||||
private var parts: [Part] = []
|
||||
|
||||
init(boundary: String? = nil) {
|
||||
self.boundary = boundary ?? "LifeIsMadeOfSeconds-\(UUID().uuidString)"
|
||||
}
|
||||
|
||||
func addPart(_ part: Part) {
|
||||
parts.append(part)
|
||||
}
|
||||
|
||||
func encode() -> Body {
|
||||
var bodyData = Data()
|
||||
for part in parts {
|
||||
// Header
|
||||
bodyData.append(Data("--\(boundary)\r\n".utf8))
|
||||
switch part.content {
|
||||
case .text:
|
||||
bodyData.append(Data("Content-Disposition: form-data; name=\"\(part.name)\"\r\n".utf8))
|
||||
|
||||
case let .binary(data, type, filename):
|
||||
bodyData.append(Data("Content-Disposition: form-data; name=\"\(part.name)\"; filename=\"\(filename)\"\r\n".utf8))
|
||||
bodyData.append(Data("Content-Type: \(type)\r\n".utf8))
|
||||
bodyData.append(Data("Content-Length: \(data.count)\r\n".utf8))
|
||||
}
|
||||
bodyData.append(Data("\r\n".utf8))
|
||||
|
||||
// Body
|
||||
switch part.content {
|
||||
case let .text(string):
|
||||
bodyData.append(Data(string.utf8))
|
||||
|
||||
case let .binary(data, _, _):
|
||||
bodyData.append(data)
|
||||
}
|
||||
bodyData.append(Data("\r\n".utf8))
|
||||
}
|
||||
|
||||
// Footer
|
||||
bodyData.append(Data("--\(boundary)--".utf8))
|
||||
|
||||
return Body(contentType: "multipart/form-data; boundary=\"\(boundary)\"", data: bodyData)
|
||||
}
|
||||
}
|
||||
|
|
@ -19,11 +19,10 @@ final class RequestBuilder {
|
|||
result.addValue(value, forHTTPHeaderField: name)
|
||||
}
|
||||
if let params = request.parameters {
|
||||
let data: Data
|
||||
switch request.contentType {
|
||||
case .json:
|
||||
result.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
data = try JSONSerialization.data(withJSONObject: params, options: [])
|
||||
result.httpBody = try JSONSerialization.data(withJSONObject: params, options: [])
|
||||
|
||||
case .none:
|
||||
// Fall back to form encoding for maximum compatibility.
|
||||
|
|
@ -41,10 +40,10 @@ final class RequestBuilder {
|
|||
for part in request.parts {
|
||||
encoder.addPart(part)
|
||||
}
|
||||
let encoded = try encoder.encodeToMemory()
|
||||
result.addValue(encoded.contentType, forHTTPHeaderField: "Content-Type")
|
||||
result.addValue("\(encoded.contentLength)", forHTTPHeaderField: "Content-Length")
|
||||
result.httpBody = encoded.body
|
||||
let body = encoder.encode()
|
||||
result.addValue(body.contentType, forHTTPHeaderField: "Content-Type")
|
||||
result.addValue("\(body.contentLength)", forHTTPHeaderField: "Content-Length")
|
||||
result.httpBody = body.data
|
||||
}
|
||||
}
|
||||
return result
|
||||
7
Tests/LinuxMain.swift
Normal file
7
Tests/LinuxMain.swift
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import XCTest
|
||||
|
||||
import OsirisTests
|
||||
|
||||
var tests = [XCTestCaseEntry]()
|
||||
tests += OsirisTests.allTests()
|
||||
XCTMain(tests)
|
||||
111
Tests/OsirisTests/MultipartFormEncoderTests.swift
Normal file
111
Tests/OsirisTests/MultipartFormEncoderTests.swift
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
//
|
||||
// MultipartFormEncoderTests.swift
|
||||
// VidjoTests
|
||||
//
|
||||
// Created by Sami Samhuri on 2020-10-20.
|
||||
// Copyright © 2020 Guru Logic Inc. All rights reserved.
|
||||
//
|
||||
|
||||
@testable import Osiris
|
||||
import XCTest
|
||||
|
||||
func AssertBodyEqual(_ expression1: @autoclosure () throws -> Data, _ expression2: @autoclosure () throws -> String, _ message: @autoclosure () -> String? = nil, file: StaticString = #filePath, line: UInt = #line) {
|
||||
let data1 = try! expression1()
|
||||
let string1 = String(bytes: data1, encoding: .utf8)!
|
||||
let string2 = try! expression2()
|
||||
let message = message() ?? "\"\(string1)\" is not equal to \"\(string2)\""
|
||||
XCTAssertEqual(data1, Data(string2.utf8), message, file: file, line: line)
|
||||
}
|
||||
|
||||
class MultipartFormEncoderTests: XCTestCase {
|
||||
var boundary = "SuperAwesomeBoundary"
|
||||
var subject: MultipartFormEncoder!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
subject = MultipartFormEncoder(boundary: boundary)
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
subject = nil
|
||||
}
|
||||
|
||||
func testEncodeNothing() throws {
|
||||
let body = subject.encode()
|
||||
XCTAssertEqual(body.contentType, "multipart/form-data; boundary=\"SuperAwesomeBoundary\"")
|
||||
AssertBodyEqual(body.data, "--SuperAwesomeBoundary--")
|
||||
}
|
||||
|
||||
func testEncodeText() throws {
|
||||
subject.addPart(.text(name: "name", value: "Tina"))
|
||||
AssertBodyEqual(
|
||||
subject.encode().data,
|
||||
[
|
||||
"--SuperAwesomeBoundary",
|
||||
"Content-Disposition: form-data; name=\"name\"",
|
||||
"",
|
||||
"Tina",
|
||||
"--SuperAwesomeBoundary--",
|
||||
].joined(separator: "\r\n")
|
||||
)
|
||||
}
|
||||
|
||||
func testEncodeData() throws {
|
||||
subject.addPart(.binary(name: "video", data: Data("phony video data".utf8), type: "video/mp4", filename: "LiesSex&VideoTape.mp4"))
|
||||
AssertBodyEqual(
|
||||
subject.encode().data,
|
||||
[
|
||||
"--SuperAwesomeBoundary",
|
||||
"Content-Disposition: form-data; name=\"video\"; filename=\"LiesSex&VideoTape.mp4\"",
|
||||
"Content-Type: video/mp4",
|
||||
"Content-Length: 16",
|
||||
"",
|
||||
"phony video data",
|
||||
"--SuperAwesomeBoundary--"
|
||||
].joined(separator: "\r\n")
|
||||
)
|
||||
}
|
||||
|
||||
func testEncodeEverything() throws {
|
||||
subject.addPart(.text(name: "name", value: "Queso"))
|
||||
subject.addPart(.binary(name: "image", data: Data("phony image data".utf8), type: "image/jpeg", filename: "feltcute.jpg"))
|
||||
subject.addPart(.text(name: "spot", value: "top of the bbq"))
|
||||
subject.addPart(.binary(name: "video", data: Data("phony video data".utf8), type: "video/mp4", filename: "LiesSex&VideoTape.mp4"))
|
||||
AssertBodyEqual(
|
||||
subject.encode().data,
|
||||
[
|
||||
"--SuperAwesomeBoundary",
|
||||
"Content-Disposition: form-data; name=\"name\"",
|
||||
"",
|
||||
"Queso",
|
||||
|
||||
"--SuperAwesomeBoundary",
|
||||
"Content-Disposition: form-data; name=\"image\"; filename=\"feltcute.jpg\"",
|
||||
"Content-Type: image/jpeg",
|
||||
"Content-Length: 16",
|
||||
"",
|
||||
"phony image data",
|
||||
|
||||
"--SuperAwesomeBoundary",
|
||||
"Content-Disposition: form-data; name=\"spot\"",
|
||||
"",
|
||||
"top of the bbq",
|
||||
|
||||
"--SuperAwesomeBoundary",
|
||||
"Content-Disposition: form-data; name=\"video\"; filename=\"LiesSex&VideoTape.mp4\"",
|
||||
"Content-Type: video/mp4",
|
||||
"Content-Length: 16",
|
||||
"",
|
||||
"phony video data",
|
||||
|
||||
"--SuperAwesomeBoundary--"
|
||||
].joined(separator: "\r\n")
|
||||
)
|
||||
}
|
||||
|
||||
static var allTests = [
|
||||
("testEncodeNothing", testEncodeNothing),
|
||||
("testEncodeText", testEncodeText),
|
||||
("testEncodeData", testEncodeData),
|
||||
("testEncodeEverything", testEncodeEverything),
|
||||
]
|
||||
}
|
||||
9
Tests/OsirisTests/XCTestManifests.swift
Normal file
9
Tests/OsirisTests/XCTestManifests.swift
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import XCTest
|
||||
|
||||
#if !canImport(ObjectiveC)
|
||||
public func allTests() -> [XCTestCaseEntry] {
|
||||
return [
|
||||
testCase(MultipartFormEncoderTests.allTests),
|
||||
]
|
||||
}
|
||||
#endif
|
||||
Loading…
Reference in a new issue