Skip to content

Commit

Permalink
feat(multipart): support datafield in nested form (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
rockmagma02 authored Apr 14, 2024
1 parent f55d1fd commit 0cb16ac
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 103 deletions.
36 changes: 30 additions & 6 deletions Sources/HttpX/Content/MultiPart.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,23 @@ public class MultiPart {
/// Initializes a new instance of the MultiPart class.
/// - Parameters:
/// - fromData: An array of tuples containing field names and their corresponding
/// data. Defaults to an empty array.
/// data. Defaults to an empty array. The corresponding data can by format
/// as `[Data or String]`, `Data` or `String`
/// - fromFile: An array of tuples containing field names and their corresponding
/// file information. Defaults to an empty array.
/// - encoding: The string encoding to use for encoding text. Defaults to `.utf8`.
/// - boundary: The boundary used to separate parts in the encoded form-data.
/// If not provided, a new UUID string will be used.
///
/// - Throws: An error if the fields cannot be extracted from the provided data.
public init(
fromData data: [(String, Data)] = [],
fromData data: [(String, Any)] = [],
fromFile files: [(String, File)] = [],
encoding: String.Encoding = .utf8,
boundary: Data? = nil
) throws {
self.boundary = boundary ?? UUID().uuidString.data(using: .utf8)!
fields = try Self.getFields(fromData: data, fromFile: files)
fields = try Self.getFields(fromData: data, fromFile: files, encoding: encoding)
}

deinit {}
Expand Down Expand Up @@ -118,6 +121,11 @@ public class MultiPart {
self.value = value
}

convenience init(name: String, value: String, encoding: String.Encoding) {
let data = value.data(using: encoding)!
self.init(name: name, value: data)
}

deinit {}

// MARK: Internal
Expand Down Expand Up @@ -249,13 +257,29 @@ public class MultiPart {
}

private static func getFields(
fromData data: [(String, Data)],
fromFile file: [(String, File)]
fromData data: [(String, Any)],
fromFile file: [(String, File)],
encoding: String.Encoding
) throws -> [Field] {
var fields: [Field] = []
for (name, value) in data {
fields.append(DataField(name: name, value: value))
if let value = value as? [Any] {
for item in value {
if let item = item as? String {
fields.append(DataField(name: name, value: item, encoding: encoding))
} else if let item = item as? Data {
fields.append(DataField(name: name, value: item))
}
}
} else if let value = value as? String {
fields.append(DataField(name: name, value: value, encoding: encoding))
} else if let value = value as? Data {
fields.append(DataField(name: name, value: value))
} else {
throw ContentError.unsupportedType
}
}

for (name, file) in file {
try fields.append(FileField(name: name, file: file))
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/HttpX/Error.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ public enum StreamError: Error, Equatable {
public enum ContentError: Error {
/// The FIle URL cat't be found.
case pathNotFound
/// The Data type is not supported.
case unsupportedType
}

// MARK: - HttpXError
Expand Down
21 changes: 21 additions & 0 deletions Tests/HttpXTests/Content/MultiPartTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,25 @@ class MultiPartTests: XCTestCase {
XCTAssertEqual(error as? ContentError, ContentError.pathNotFound)
}
}

func testMultiPartInitialization() throws {
let dataFields: [(String, Any)] = [
("key1", "value1"),
("key2", Data("value2".utf8)),
("key3", ["value3", Data("value3".utf8)]),
]
XCTAssertNoThrow(try MultiPart(fromData: dataFields))
let multipart = try MultiPart(fromData: dataFields)
XCTAssertNotNil(multipart)
XCTAssertGreaterThan(multipart.contentLength, 0)
}

func testMultiPartInitializationInvalid() {
let dataFields: [(String, Any)] = [
("key1", 123),
]
XCTAssertThrowsError(try MultiPart(fromData: dataFields)) { error in
XCTAssertEqual(error as? ContentError, ContentError.unsupportedType)
}
}
}
105 changes: 53 additions & 52 deletions Tests/HttpXTests/HttpXAsyncTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -665,55 +665,56 @@ internal final class AsyncRedirectsTests: XCTestCase {

// MARK: - AsyncOnlineTest

internal final class AsyncOnlineTest: XCTestCase {
// MARK: Internal

internal func testStream() async throws {
let url = "\(baseURL)/stream-bytes/5000"
let response = try await HttpX.stream(method: .get, url: URLType.string(url))
XCTAssertEqual(response.URLResponse?.status.0, 200)

var dataLength: [Int] = []
for try await chunk in response.asyncStream! {
dataLength.append(chunk.count)
}
XCTAssertEqual(dataLength.count, 5)
XCTAssertEqual(dataLength, [1_024, 1_024, 1_024, 1_024, 904])
}

func testSendSingleRequest() async throws {
// Timeout
let client = AsyncClient()
let expectation = expectation(description: "timeout")
do {
_ = try await client.sendSingleRequest(
request: URLRequest(url: URL(string: "https://httpbin.org/delay/10")!, timeoutInterval: 1),
stream: (false, nil)
)
} catch {
XCTAssertEqual(error as? HttpXError, HttpXError.networkError(message: "", code: -1_001))
expectation.fulfill()
}
await fulfillment(of: [expectation], timeout: 5)
}

func testSendSingleRequestAsync() async throws {
// Timeout
let client = AsyncClient()
let expectation = expectation(description: "timeout")
do {
_ = try await client.sendSingleRequest(
request: URLRequest(url: URL(string: "https://httpbin.org/delay/10")!, timeoutInterval: 1),
stream: (true, nil)
)
} catch {
XCTAssertEqual(error as? HttpXError, HttpXError.networkError(message: "", code: -1_001))
expectation.fulfill()
}
await fulfillment(of: [expectation], timeout: 5)
}

// MARK: Private

private let baseURL: String = "https://httpbin.org"
}
//
// internal final class AsyncOnlineTest: XCTestCase {
// // MARK: Internal
//
// internal func testStream() async throws {
// let url = "\(baseURL)/stream-bytes/5000"
// let response = try await HttpX.stream(method: .get, url: URLType.string(url))
// XCTAssertEqual(response.URLResponse?.status.0, 200)
//
// var dataLength: [Int] = []
// for try await chunk in response.asyncStream! {
// dataLength.append(chunk.count)
// }
// XCTAssertEqual(dataLength.count, 5)
// XCTAssertEqual(dataLength, [1_024, 1_024, 1_024, 1_024, 904])
// }
//
// func testSendSingleRequest() async throws {
// // Timeout
// let client = AsyncClient()
// let expectation = expectation(description: "timeout")
// do {
// _ = try await client.sendSingleRequest(
// request: URLRequest(url: URL(string: "https://httpbin.org/delay/10")!, timeoutInterval: 1),
// stream: (false, nil)
// )
// } catch {
// XCTAssertEqual(error as? HttpXError, HttpXError.networkError(message: "", code: -1_001))
// expectation.fulfill()
// }
// await fulfillment(of: [expectation], timeout: 5)
// }
//
// func testSendSingleRequestAsync() async throws {
// // Timeout
// let client = AsyncClient()
// let expectation = expectation(description: "timeout")
// do {
// _ = try await client.sendSingleRequest(
// request: URLRequest(url: URL(string: "https://httpbin.org/delay/10")!, timeoutInterval: 1),
// stream: (true, nil)
// )
// } catch {
// XCTAssertEqual(error as? HttpXError, HttpXError.networkError(message: "", code: -1_001))
// expectation.fulfill()
// }
// await fulfillment(of: [expectation], timeout: 5)
// }
//
// // MARK: Private
//
// private let baseURL: String = "https://httpbin.org"
// }
91 changes: 46 additions & 45 deletions Tests/HttpXTests/HttpXTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -665,48 +665,49 @@ internal final class RedirectsTests: XCTestCase {

// MARK: - OnlineTest

internal final class OnlineTest: XCTestCase {
// MARK: Internal

internal func testRelativeRedirect() throws {
let url = "\(baseURL)/relative-redirect/2"
let response = try HttpX.get(url: URLType.string(url), headers: HeadersType.array([("test", "value")]), followRedirects: false)
XCTAssertEqual(response.URLResponse?.status.0, 302)
XCTAssertEqual(response.URLResponse?.getHeaderValue(forHTTPHeaderField: "Location"), "/relative-redirect/1")
XCTAssertEqual(response.nextRequest?.url?.absoluteString, "https://httpbin.org/relative-redirect/1")

let response2 = try HttpX.get(url: URLType.string(url), followRedirects: true)
XCTAssertEqual(response2.URLResponse?.status.0, 200)
XCTAssertEqual(response2.history.count, 2)
}

internal func testStream() throws {
let url = "\(baseURL)/stream-bytes/5000"
let response = try HttpX.stream(method: .get, url: URLType.string(url))
XCTAssertEqual(response.URLResponse?.status.0, 200)

var dataLength: [Int] = []
for chunk in response.syncStream! {
dataLength.append(chunk.count)
}
XCTAssertEqual(dataLength.count, 5)
XCTAssertEqual(dataLength, [1_024, 1_024, 1_024, 1_024, 904])
}

func testSendSingleRequest() throws {
// Timeout
let client = SyncClient()
XCTAssertThrowsError(
try client.sendSingleRequest(
request: URLRequest(url: URL(string: "https://httpbin.org/delay/10")!, timeoutInterval: 1),
stream: (true, nil)
)
) { error in
XCTAssertEqual(error as? HttpXError, HttpXError.networkError(message: "", code: -1_001))
}
}

// MARK: Private

private let baseURL: String = "https://httpbin.org"
}
//
// internal final class OnlineTest: XCTestCase {
// // MARK: Internal
//
// internal func testRelativeRedirect() throws {
// let url = "\(baseURL)/relative-redirect/2"
// let response = try HttpX.get(url: URLType.string(url), headers: HeadersType.array([("test", "value")]), followRedirects: false)
// XCTAssertEqual(response.URLResponse?.status.0, 302)
// XCTAssertEqual(response.URLResponse?.getHeaderValue(forHTTPHeaderField: "Location"), "/relative-redirect/1")
// XCTAssertEqual(response.nextRequest?.url?.absoluteString, "https://httpbin.org/relative-redirect/1")
//
// let response2 = try HttpX.get(url: URLType.string(url), followRedirects: true)
// XCTAssertEqual(response2.URLResponse?.status.0, 200)
// XCTAssertEqual(response2.history.count, 2)
// }
//
// internal func testStream() throws {
// let url = "\(baseURL)/stream-bytes/5000"
// let response = try HttpX.stream(method: .get, url: URLType.string(url))
// XCTAssertEqual(response.URLResponse?.status.0, 200)
//
// var dataLength: [Int] = []
// for chunk in response.syncStream! {
// dataLength.append(chunk.count)
// }
// XCTAssertEqual(dataLength.count, 5)
// XCTAssertEqual(dataLength, [1_024, 1_024, 1_024, 1_024, 904])
// }
//
// func testSendSingleRequest() throws {
// // Timeout
// let client = SyncClient()
// XCTAssertThrowsError(
// try client.sendSingleRequest(
// request: URLRequest(url: URL(string: "https://httpbin.org/delay/10")!, timeoutInterval: 1),
// stream: (true, nil)
// )
// ) { error in
// XCTAssertEqual(error as? HttpXError, HttpXError.networkError(message: "", code: -1_001))
// }
// }
//
// // MARK: Private
//
// private let baseURL: String = "https://httpbin.org"
// }

0 comments on commit 0cb16ac

Please sign in to comment.