-
-
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(foundation): Foundation API (#1)
- Loading branch information
Showing
9 changed files
with
240 additions
and
0 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,20 @@ | ||
name: Test | ||
|
||
on: | ||
push: | ||
branches: | ||
- main | ||
pull_request: | ||
branches: | ||
- main | ||
|
||
jobs: | ||
test: | ||
runs-on: macos-11 | ||
|
||
steps: | ||
- name: Checkout repo | ||
uses: actions/checkout@v2 | ||
|
||
- name: Run tests | ||
run: swift test --enable-test-discovery |
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,31 @@ | ||
#if canImport(Combine) | ||
|
||
import Foundation | ||
import Combine | ||
|
||
/// A function converting data when a http error occur into a custom error | ||
public typealias DataErrorConverter = (Data) throws -> Error | ||
|
||
extension Publisher where Output == URLSession.DataTaskPublisher.Output { | ||
/// validate publisher result optionally converting HTTP error into a custom one | ||
/// - Parameter converter: called when error is `HTTPError` and data was found in the output. Use it to convert | ||
/// data in a custom `Error` that will be returned of the http one. | ||
public func validate(_ converter: DataErrorConverter? = nil) -> AnyPublisher<Output, Error> { | ||
tryMap { output in | ||
do { | ||
try (output.response as? HTTPURLResponse)?.validate() | ||
return output | ||
} | ||
catch { | ||
if let _ = error as? HTTPError, let convert = converter, !output.data.isEmpty { | ||
throw try convert(output.data) | ||
} | ||
|
||
throw error | ||
} | ||
} | ||
.eraseToAnyPublisher() | ||
} | ||
} | ||
|
||
#endif |
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<T: Encodable>(_ body: T, encoder: JSONEncoder) throws -> Self { | ||
var request = self | ||
|
||
try request.encodeBody(body, encoder: encoder) | ||
|
||
return request | ||
} | ||
|
||
/// Use a `JSONEncoder` object as request body and set the "Content-Type" header associated to the encoder | ||
mutating func encodeBody<T: Encodable>(_ body: T, encoder: JSONEncoder) throws { | ||
httpBody = try encoder.encode(body) | ||
setValue("Content-Type", forHTTPHeaderField: "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,14 @@ | ||
import Foundation | ||
|
||
#if canImport(FoundationNetworking) | ||
import FoundationNetworking | ||
#endif | ||
|
||
extension HTTPURLResponse { | ||
/// check whether a response is valid or not | ||
public func validate() throws { | ||
guard (200..<300).contains(statusCode) else { | ||
throw HTTPError(statusCode: statusCode) | ||
} | ||
} | ||
} |
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,39 @@ | ||
import Foundation | ||
|
||
/// An error generated by a HTTP response | ||
public struct HTTPError: Error, Equatable, ExpressibleByIntegerLiteral { | ||
public let statusCode: Int | ||
|
||
public init(statusCode: Int) { | ||
self.statusCode = statusCode | ||
} | ||
|
||
public init(integerLiteral value: IntegerLiteralType) { | ||
self.init(statusCode: value) | ||
} | ||
} | ||
|
||
public extension HTTPError { | ||
static let badRequest: Self = 400 | ||
|
||
static let unauthorized: Self = 401 | ||
|
||
/// Request contained valid data and was understood by the server, but the server is refusing action | ||
static let forbidden: Self = 403 | ||
|
||
static let notFound: Self = 404 | ||
|
||
static let requestTimeout: Self = 408 | ||
|
||
/// Generic error message when an unexpected condition was encountered and no more specific message is suitable | ||
static let serverError: Self = 500 | ||
|
||
/// Server was acting as a gateway or proxy and received an invalid response from the upstream server | ||
static let badGateway: Self = 502 | ||
|
||
/// Server cannot handle the request (because it is overloaded or down for maintenance) | ||
static let serviceUnavailable: Self = 503 | ||
|
||
/// Server was acting as a gateway or proxy and did not receive a timely response from the upstream server | ||
static let gatewayTimeout: Self = 504 | ||
} |
53 changes: 53 additions & 0 deletions
53
Tests/PinataTests/Foundation/PublisherTests+Validate.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,53 @@ | ||
import XCTest | ||
import Combine | ||
import Pinata | ||
|
||
class PublisherValidateTests: XCTestCase { | ||
var cancellables: Set<AnyCancellable> = [] | ||
|
||
override func tearDown() { | ||
cancellables = [] | ||
} | ||
|
||
func test_validate_responseIsError_dataIsEmpty_converterIsNotCalled() throws { | ||
let output: URLSession.DataTaskPublisher.Output = (data: Data(), response: HTTPURLResponse.notFound) | ||
let transformer: DataErrorConverter = { _ in | ||
XCTFail("transformer should not be called when data is empty") | ||
throw NSError() | ||
} | ||
|
||
Just(output) | ||
.validate(transformer) | ||
.sink(receiveCompletion: { _ in }, receiveValue: { _ in }) | ||
.store(in: &cancellables) | ||
} | ||
|
||
func test_validate_responseIsError_dataIsNotEmpty_returnCustomError() throws { | ||
let customError = CustomError(code: 22, message: "custom message") | ||
let output: URLSession.DataTaskPublisher.Output = ( | ||
data: try JSONEncoder().encode(customError), | ||
response: HTTPURLResponse.notFound | ||
) | ||
let transformer: DataErrorConverter = { data in | ||
return try JSONDecoder().decode(CustomError.self, from: data) | ||
} | ||
|
||
Just(output) | ||
.validate(transformer) | ||
.sink( | ||
receiveCompletion: { | ||
guard case let .failure(error) = $0 else { | ||
return XCTFail() | ||
} | ||
|
||
XCTAssertEqual(error as? CustomError, customError) | ||
}, | ||
receiveValue: { _ in }) | ||
.store(in: &cancellables) | ||
} | ||
} | ||
|
||
private struct CustomError: Error, Equatable, Codable { | ||
let code: Int | ||
let message: String | ||
} |
17 changes: 17 additions & 0 deletions
17
Tests/PinataTests/Foundation/URLRequestsTests+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,17 @@ | ||
import XCTest | ||
import Pinata | ||
|
||
#if canImport(FoundationNetworking) | ||
import FoundationNetworking | ||
#endif | ||
|
||
class URLRequestEncodeTests: XCTest { | ||
|
||
func test_encodedBody_itSetContentTypeHeader() throws { | ||
let body: [String:String] = [:] | ||
let request = try URLRequest(url: URL(string: "/")!) | ||
.encodedBody(body, encoder: JSONEncoder()) | ||
|
||
XCTAssertEqual(request.allHTTPHeaderFields?["Content-Type"], "application/json") | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
Tests/PinataTests/Foundation/URLResponseValidateTests.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,27 @@ | ||
import XCTest | ||
import Pinata | ||
|
||
#if canImport(FoundationNetworking) | ||
import FoundationNetworking | ||
#endif | ||
|
||
class URLResponseValidateTests: XCTest { | ||
let url = URL(string: "/")! | ||
|
||
func test_validate_statusCodeIsOK_itThrowNoError() throws { | ||
try HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!.validate() | ||
} | ||
|
||
// we should never have redirection that's why we consider it as an error | ||
func test_validate_statusCodeIsRedirection_itThrow() { | ||
XCTAssertThrowsError( | ||
try HTTPURLResponse(url: url, statusCode: 302, httpVersion: nil, headerFields: nil)!.validate() | ||
) | ||
} | ||
|
||
func test_validate_statusCodeIsClientError_itThrow() { | ||
XCTAssertThrowsError( | ||
try HTTPURLResponse(url: url, statusCode: 404, httpVersion: nil, headerFields: nil)!.validate() | ||
) | ||
} | ||
} |
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,17 @@ | ||
import Foundation | ||
|
||
#if canImport(FoundationNetworking) | ||
import FoundationNetworking | ||
#endif | ||
|
||
extension HTTPURLResponse { | ||
convenience init(statusCode: Int) { | ||
self.init(url: URL(string: "/")!, statusCode: statusCode, httpVersion: nil, headerFields: nil)! | ||
} | ||
} | ||
|
||
extension URLResponse { | ||
static let success = HTTPURLResponse(statusCode: 200) | ||
static let unauthorized = HTTPURLResponse(statusCode: 401) | ||
static let notFound = HTTPURLResponse(statusCode: 404) | ||
} |