-
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(request): Add a Request type (#2)
- Loading branch information
Showing
16 changed files
with
310 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import Foundation | ||
|
||
/// A encoder suited to encode to Data | ||
public protocol DataEncoder { | ||
func encode<T: Encodable>(_ value: T) throws -> Data | ||
} | ||
|
||
/// A decoder suited to decode Data | ||
public protocol DataDecoder { | ||
func decode<T: Decodable>(_ type: T.Type, from: Data) throws -> T | ||
} | ||
|
||
/// A `DataEncoder` providing a `ContentType` | ||
public protocol ContentDataEncoder: DataEncoder { | ||
/// a http content type | ||
static var contentType: HTTPContentType { get } | ||
} | ||
|
||
/// A `DataDecoder` providing a `ContentType` | ||
public protocol ContentDataDecoder: DataDecoder { | ||
/// a http content type | ||
static var contentType: HTTPContentType { get } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import Foundation | ||
|
||
extension Encodable { | ||
/// Encode the object with provided encoder. | ||
/// This technique allow to "open" an existential, that is to use it in a context where a generic is expected | ||
func encoded(with encoder: DataEncoder) throws -> Data { | ||
try encoder.encode(self) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import Foundation | ||
|
||
extension JSONEncoder: ContentDataEncoder { | ||
public static let contentType = HTTPContentType.json | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import Foundation | ||
|
||
#if canImport(FoundationNetworking) | ||
import FoundationNetworking | ||
#endif | ||
|
||
extension URL { | ||
init<Output>(from request: Request<Output>) throws { | ||
guard var components = URLComponents(string: request.path) else { | ||
throw URLComponents.Error.invalid(path: request.path) | ||
} | ||
|
||
let queryItems = (components.queryItems ?? []) + request.parameters.queryItems | ||
|
||
components.queryItems = queryItems.isEmpty ? nil : queryItems | ||
|
||
guard let url = components.url else { | ||
throw URLComponents.Error.cannotGenerateURL(components: components) | ||
} | ||
|
||
self = url | ||
} | ||
} | ||
|
||
extension URLComponents { | ||
enum Error: Swift.Error { | ||
case invalid(path: String) | ||
case cannotGenerateURL(components: URLComponents) | ||
} | ||
} | ||
|
||
extension Dictionary where Key == String, Value == String { | ||
fileprivate var queryItems: [URLQueryItem] { | ||
map { URLQueryItem(name: $0.key, value: $0.value) } | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
22 changes: 22 additions & 0 deletions
22
Sources/Pinata/Foundation/URLRequest/URLRequest+Encode.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import Foundation | ||
|
||
#if canImport(FoundationNetworking) | ||
import FoundationNetworking | ||
#endif | ||
|
||
public extension URLRequest { | ||
func encodedBody(_ body: Encodable, encoder: ContentDataEncoder) throws -> Self { | ||
var request = self | ||
|
||
try request.encodeBody(body, encoder: encoder) | ||
|
||
return request | ||
} | ||
|
||
/// Use a `Encodable` object as request body and set the "Content-Type" header associated to the encoder | ||
mutating func encodeBody(_ body: Encodable, encoder: ContentDataEncoder) throws { | ||
httpBody = try body.encoded(with: encoder) | ||
setHeaders([.contentType: type(of: encoder).contentType.value]) | ||
} | ||
|
||
} |
13 changes: 13 additions & 0 deletions
13
Sources/Pinata/Foundation/URLRequest/URLRequest+HTTPHeader.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import Foundation | ||
|
||
#if canImport(FoundationNetworking) | ||
import FoundationNetworking | ||
#endif | ||
|
||
extension URLRequest { | ||
public mutating func setHeaders(_ headers: HTTPHeaderFields) { | ||
for (header, value) in headers { | ||
setValue(value, forHTTPHeaderField: header.key) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import Foundation | ||
|
||
/// A struct representing a http header content type value | ||
public struct HTTPContentType: Hashable, ExpressibleByStringLiteral { | ||
let value: String | ||
|
||
public init(value: String) { | ||
self.value = value | ||
} | ||
|
||
public init(stringLiteral value: StringLiteralType) { | ||
self.value = value | ||
} | ||
} | ||
|
||
extension HTTPContentType { | ||
public static let json: Self = "application/json" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import Foundation | ||
|
||
/// HTTP headers `Dictionary` and their associated value | ||
public typealias HTTPHeaderFields = [HTTPHeader: String] | ||
|
||
/// A struct representing a http request header key | ||
public struct HTTPHeader: Hashable, ExpressibleByStringLiteral { | ||
public let key: String | ||
|
||
public init(stringLiteral value: StringLiteralType) { | ||
self.key = value | ||
} | ||
} | ||
|
||
extension HTTPHeader { | ||
public static let accept: Self = "Accept" | ||
public static let authentication: Self = "Authentication" | ||
public static let contentType: Self = "Content-Type" | ||
} | ||
|
||
@available(*, unavailable, message: "This is a reserved header. See https://developer.apple.com/documentation/foundation/nsurlrequest#1776617") | ||
extension HTTPHeader { | ||
public static let authorization: Self = "Authorization" | ||
public static let connection: Self = "Connection" | ||
public static let contentLength: Self = "Content-Length" | ||
public static let host: Self = "Host" | ||
public static let proxyAuthenticate: Self = "Proxy-Authenticate" | ||
public static let proxyAuthorization: Self = "Proxy-Authorization" | ||
public static let wwwAuthenticate: Self = "WWW-Authenticate" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import Foundation | ||
|
||
/// A Type representing a URL path | ||
public protocol Path { | ||
var path: String { get } | ||
} | ||
|
||
extension Path where Self: RawRepresentable, RawValue == String { | ||
public var path: String { rawValue } | ||
} | ||
|
||
extension String: Path { | ||
public var path: String { self } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import Foundation | ||
|
||
#if canImport(FoundationNetworking) | ||
import FoundationNetworking | ||
#endif | ||
|
||
extension Request { | ||
func toURLRequest(encoder: ContentDataEncoder) throws -> URLRequest { | ||
var urlRequest = try URLRequest(url: URL(from: self)) | ||
|
||
urlRequest.httpMethod = method.rawValue.uppercased() | ||
urlRequest.setHeaders(headers) | ||
|
||
if let body = body { | ||
try urlRequest.encodeBody(body, encoder: encoder) | ||
} | ||
|
||
return urlRequest | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import Foundation | ||
|
||
public enum Method: String { | ||
case get | ||
case post | ||
case put | ||
case delete | ||
} | ||
|
||
/// A Http request expecting an `Output` response | ||
/// | ||
/// Highly inspired by https://swiftwithmajid.com/2021/02/10/building-type-safe-networking-in-swift/ | ||
public struct Request<Output> { | ||
|
||
/// request relative path | ||
public let path: String | ||
public let method: Method | ||
public let body: Encodable? | ||
public let parameters: [String: String] | ||
public private(set) var headers: HTTPHeaderFields = [:] | ||
|
||
public static func get(_ path: Path, parameters: [String: String] = [:]) -> Self { | ||
self.init(path: path, method: .get, parameters: parameters, body: nil) | ||
} | ||
|
||
public static func post(_ path: Path, body: Encodable?, parameters: [String: String] = [:]) | ||
-> Self { | ||
self.init(path: path, method: .post, parameters: parameters, body: body) | ||
} | ||
|
||
public static func put(_ path: Path, body: Encodable, parameters: [String: String] = [:]) | ||
-> Self { | ||
self.init(path: path, method: .put, parameters: parameters, body: body) | ||
} | ||
|
||
public static func delete(_ path: Path, parameters: [String: String] = [:]) -> Self { | ||
self.init(path: path, method: .delete, parameters: parameters, body: nil) | ||
} | ||
|
||
private init(path: Path, method: Method, parameters: [String: String] = [:], body: Encodable?) { | ||
self.path = path.path | ||
self.method = method | ||
self.body = body | ||
self.parameters = parameters | ||
} | ||
|
||
/// add headers to the request | ||
public func headers(_ newHeaders: [HTTPHeader: String]) -> Self { | ||
var request = self | ||
|
||
request.headers.merge(newHeaders) { $1 } | ||
|
||
return request | ||
} | ||
} |
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import Foundation | ||
import XCTest | ||
@testable import Pinata | ||
|
||
class URLRequestTests: XCTestCase { | ||
func test_initFromRequest_pathIsSetted() throws { | ||
XCTAssertEqual( | ||
try URL(from: Request<Void>.get("test")).path, | ||
"test" | ||
) | ||
} | ||
|
||
func test_initFromRequest_pathHasQueryItems_urlQueryIsSetted() throws { | ||
XCTAssertEqual( | ||
try URL(from: Request<Void>.get("hello/world?test=1")).query, | ||
"test=1" | ||
) | ||
} | ||
|
||
func test_initFromRequest_whenPathHasQueryItems_urlPathHasNoQuery() throws { | ||
XCTAssertEqual( | ||
try URL(from: Request<Void>.get("hello/world?test=1")).path, | ||
"hello/world" | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import XCTest | ||
@testable import Pinata | ||
|
||
class RequestTests: XCTestCase { | ||
enum TestEndpoint: String, Path { | ||
case test | ||
} | ||
|
||
func test_init_withPathAsString() { | ||
XCTAssertEqual(Request<Void>.get("hello_world").path, "hello_world") | ||
} | ||
|
||
func test_toURLRequest_itSetHttpMethod() throws { | ||
let request = try Request<Void>.post(TestEndpoint.test, body: nil) | ||
.toURLRequest(encoder: JSONEncoder()) | ||
|
||
XCTAssertEqual(request.httpMethod, "POST") | ||
} | ||
|
||
func test_toURLRequest_itEncodeBody() throws { | ||
let request = try Request<Void>.post(TestEndpoint.test, body: Body()) | ||
.toURLRequest(encoder: JSONEncoder()) | ||
|
||
XCTAssertEqual(request.httpBody, try JSONEncoder().encode(Body())) | ||
} | ||
|
||
func test_toURLRequest_itFillDefaultHeaders() throws { | ||
let request = try Request<Void>.post(TestEndpoint.test, body: Body()) | ||
.toURLRequest(encoder: JSONEncoder()) | ||
|
||
XCTAssertEqual(request.allHTTPHeaderFields?["Content-Type"], "application/json") | ||
} | ||
|
||
} | ||
|
||
private struct Body: Encodable { | ||
|
||
} |