From f7cad491f7b1debfbb5953287642fde6fbc7f46a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Caama=C3=B1o=20Souto?= Date: Fri, 29 Sep 2023 10:31:35 +0200 Subject: [PATCH 1/2] feat: added custom media support --- Sources/NClient/Endpoint.swift | 25 +++++++++--- Sources/NClient/HTTP.swift | 25 ++++++++---- Tests/NClientTests/Unit/APIClientTests.swift | 2 +- Tests/NClientTests/Unit/EndpointTests.swift | 40 ++++++++++++++++++-- 4 files changed, 74 insertions(+), 18 deletions(-) diff --git a/Sources/NClient/Endpoint.swift b/Sources/NClient/Endpoint.swift index a0025d3..8a42131 100644 --- a/Sources/NClient/Endpoint.swift +++ b/Sources/NClient/Endpoint.swift @@ -30,11 +30,13 @@ public protocol Endpoint { /// The HTTP method of the endpoint. var method: HTTP.Method { get } + var contentType: HTTP.MIMEType { get } + /// Constructs the URL components for the endpoint given the parameters. func url(parameters: Parameters) -> URLComponents /// Serializes the request body into the given URLRequest. - func serializeBody(_ body: RequestBody, into request: inout URLRequest) throws + func serializeBody(_ body: RequestBody, contentType: HTTP.MIMEType, into request: inout URLRequest) throws /// Deserializes the response body from the received data. func deserializeBody(_ data: Data) throws -> ResponseBody @@ -43,6 +45,9 @@ public protocol Endpoint { public extension Endpoint { /// The default HTTP method for the endpoint is `GET`. var method: HTTP.Method { .GET } + + /// The default Content-Type for the endpoint is `application/json`. + var contentType: HTTP.MIMEType { .json } } // MARK: Request creation @@ -73,8 +78,8 @@ public extension Endpoint { var request = URLRequest(url: url.absoluteURL) request.httpMethod = method.rawValue - request.setHeader(.accept, value: HTTP.MIMEType.json) - try serializeBody(requestBody, into: &request) + request.setHeader(.accept, value: HTTP.MIMEType.json.rawValue) + try serializeBody(requestBody, contentType: contentType, into: &request) return request } @@ -84,18 +89,26 @@ public extension Endpoint { public extension Endpoint where RequestBody == Empty { /// No implementation in case of empty request body. - func serializeBody(_ body: RequestBody, into request: inout URLRequest) throws {} + func serializeBody(_ body: RequestBody, contentType: HTTP.MIMEType, into request: inout URLRequest) throws {} } public extension Endpoint where RequestBody: Encodable { /// Serializes an encodable request body as JSON. - func serializeBody(_ body: RequestBody, into request: inout URLRequest) throws { - request.setHeader(.contentType, value: HTTP.MIMEType.json) + func serializeBody(_ body: RequestBody, contentType: HTTP.MIMEType, into request: inout URLRequest) throws { + request.setHeader(.contentType, value: contentType.rawValue) let data = try JSONEncoder().encode(body) request.httpBody = data } } +public extension Endpoint where RequestBody == Data { + /// Bypass serialization if request body is already of Data type + func serializeBody(_ body: RequestBody, contentType: HTTP.MIMEType, into request: inout URLRequest) throws { + request.setHeader(.contentType, value: contentType.rawValue) + request.httpBody = body + } +} + // MARK: Deserialize public extension Endpoint where ResponseBody == Empty { diff --git a/Sources/NClient/HTTP.swift b/Sources/NClient/HTTP.swift index ed2352d..55b701a 100644 --- a/Sources/NClient/HTTP.swift +++ b/Sources/NClient/HTTP.swift @@ -18,16 +18,14 @@ public enum HTTP { struct HeaderName: RawRepresentable { let rawValue: String } -} -extension HTTP { - /// Represents common MIME types used in HTTP requests and responses. - enum MIMEType { - /// JSON data. - static let json = "application/json" + /// Supported MIME types + public struct MIMEType: RawRepresentable { + public let rawValue: String - /// URL-encoded form data. - static let formUrlEncoded = "application/x-www-form-urlencoded" + public init(rawValue: String) { + self.rawValue = rawValue + } } } @@ -45,6 +43,17 @@ extension HTTP.HeaderName { static let contentType = HTTP.HeaderName("Content-Type") } +public extension HTTP.MIMEType { + /// JSON data. + static let json = HTTP.MIMEType(rawValue: "application/json") + + /// URL-encoded form data. + static let formUrlEncoded = HTTP.MIMEType(rawValue: "application/x-www-form-urlencoded") + + /// MP4 + static let mp4 = HTTP.MIMEType(rawValue: "audio/mp4") +} + extension URLRequest { mutating func setHeader(_ name: HTTP.HeaderName, value: String) { setValue(value, forHTTPHeaderField: name.rawValue) diff --git a/Tests/NClientTests/Unit/APIClientTests.swift b/Tests/NClientTests/Unit/APIClientTests.swift index 2a589f8..bf98f57 100644 --- a/Tests/NClientTests/Unit/APIClientTests.swift +++ b/Tests/NClientTests/Unit/APIClientTests.swift @@ -35,7 +35,7 @@ final class APIClientTests: XCTestCase { result: .success( .init( statusCode: 200, - headers: [HTTP.HeaderName.contentType.rawValue: HTTP.MIMEType.json], + headers: [HTTP.HeaderName.contentType.rawValue: HTTP.MIMEType.json.rawValue], data: mockResponseData ) ) diff --git a/Tests/NClientTests/Unit/EndpointTests.swift b/Tests/NClientTests/Unit/EndpointTests.swift index 0b0fe5e..56d3619 100644 --- a/Tests/NClientTests/Unit/EndpointTests.swift +++ b/Tests/NClientTests/Unit/EndpointTests.swift @@ -20,7 +20,7 @@ final class EndpointTests: XCTestCase { XCTAssertEqual(request.url?.absoluteString, "https://example.com/path") XCTAssertEqual(request.httpMethod, HTTP.Method.GET.rawValue) - XCTAssertEqual(request.value(forHTTPHeaderField: HTTP.HeaderName.accept.rawValue), HTTP.MIMEType.json) + XCTAssertEqual(request.value(forHTTPHeaderField: HTTP.HeaderName.accept.rawValue), HTTP.MIMEType.json.rawValue) XCTAssertNil(request.value(forHTTPHeaderField: HTTP.HeaderName.contentType.rawValue)) XCTAssertNil(request.httpBody) @@ -48,14 +48,32 @@ final class EndpointTests: XCTestCase { XCTAssertEqual(request.url?.absoluteString, "https://example.com/path") XCTAssertEqual(request.httpMethod, HTTP.Method.POST.rawValue) - XCTAssertEqual(request.value(forHTTPHeaderField: HTTP.HeaderName.accept.rawValue), HTTP.MIMEType.json) - XCTAssertEqual(request.value(forHTTPHeaderField: HTTP.HeaderName.contentType.rawValue), HTTP.MIMEType.json) + XCTAssertEqual(request.value(forHTTPHeaderField: HTTP.HeaderName.accept.rawValue), HTTP.MIMEType.json.rawValue) + XCTAssertEqual(request.value(forHTTPHeaderField: HTTP.HeaderName.contentType.rawValue), HTTP.MIMEType.json.rawValue) let bodyData = try JSONEncoder().encode(mockRequestBody) XCTAssertNotNil(request.httpBody) XCTAssertEqual(request.httpBody, bodyData) } + func testEndpointWithRawRequestBody() throws { + let endpoint = MockEndpointWithRawResponseBody() + let request = try endpoint.request( + baseUrl: mockBaseUrl, + parameters: .empty, + requestBody: try XCTUnwrap(mockMessage.data(using: .utf8)) + ) + + XCTAssertEqual(request.url?.absoluteString, "https://example.com/path") + XCTAssertEqual(request.httpMethod, HTTP.Method.POST.rawValue) + + XCTAssertEqual(request.value(forHTTPHeaderField: HTTP.HeaderName.accept.rawValue), HTTP.MIMEType.json.rawValue) + XCTAssertEqual(request.value(forHTTPHeaderField: HTTP.HeaderName.contentType.rawValue), HTTP.MIMEType.mp4.rawValue) + + let requestBody = try XCTUnwrap(request.httpBody) + XCTAssertEqual(String(data: requestBody, encoding: .utf8), mockMessage) + } + // MARK: Deserialize func testDeserializeEmptyBody() throws { @@ -140,3 +158,19 @@ private struct MockEndpointWithResponseBody: Endpoint { .init(path: mockPath) } } + +private struct MockEndpointWithRawResponseBody: Endpoint { + typealias RequestBody = Data + + var method: HTTP.Method { + .POST + } + + var contentType: HTTP.MIMEType { + .mp4 + } + + func url(parameters: Parameters) -> URLComponents { + .init(path: mockPath) + } +} From 61e1bdb7a6d606143a04e78ef9774f976d1417fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Caama=C3=B1o=20Souto?= Date: Tue, 3 Oct 2023 23:11:47 +0200 Subject: [PATCH 2/2] fix: apply PR suggestions --- Sources/NClient/Endpoint.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/NClient/Endpoint.swift b/Sources/NClient/Endpoint.swift index 8a42131..4489877 100644 --- a/Sources/NClient/Endpoint.swift +++ b/Sources/NClient/Endpoint.swift @@ -36,7 +36,7 @@ public protocol Endpoint { func url(parameters: Parameters) -> URLComponents /// Serializes the request body into the given URLRequest. - func serializeBody(_ body: RequestBody, contentType: HTTP.MIMEType, into request: inout URLRequest) throws + func serializeBody(_ body: RequestBody, into request: inout URLRequest) throws /// Deserializes the response body from the received data. func deserializeBody(_ data: Data) throws -> ResponseBody @@ -79,7 +79,7 @@ public extension Endpoint { var request = URLRequest(url: url.absoluteURL) request.httpMethod = method.rawValue request.setHeader(.accept, value: HTTP.MIMEType.json.rawValue) - try serializeBody(requestBody, contentType: contentType, into: &request) + try serializeBody(requestBody, into: &request) return request } @@ -89,12 +89,12 @@ public extension Endpoint { public extension Endpoint where RequestBody == Empty { /// No implementation in case of empty request body. - func serializeBody(_ body: RequestBody, contentType: HTTP.MIMEType, into request: inout URLRequest) throws {} + func serializeBody(_ body: RequestBody, into request: inout URLRequest) throws {} } public extension Endpoint where RequestBody: Encodable { /// Serializes an encodable request body as JSON. - func serializeBody(_ body: RequestBody, contentType: HTTP.MIMEType, into request: inout URLRequest) throws { + func serializeBody(_ body: RequestBody, into request: inout URLRequest) throws { request.setHeader(.contentType, value: contentType.rawValue) let data = try JSONEncoder().encode(body) request.httpBody = data @@ -103,7 +103,7 @@ public extension Endpoint where RequestBody: Encodable { public extension Endpoint where RequestBody == Data { /// Bypass serialization if request body is already of Data type - func serializeBody(_ body: RequestBody, contentType: HTTP.MIMEType, into request: inout URLRequest) throws { + func serializeBody(_ body: RequestBody, into request: inout URLRequest) throws { request.setHeader(.contentType, value: contentType.rawValue) request.httpBody = body }