Skip to content

Commit

Permalink
Some QOL updates
Browse files Browse the repository at this point in the history
  • Loading branch information
henrik-dmg committed Jan 19, 2021
1 parent b94bf42 commit 827edb4
Show file tree
Hide file tree
Showing 11 changed files with 87 additions and 89 deletions.
3 changes: 1 addition & 2 deletions HPNetwork.podspec
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
Pod::Spec.new do |s|

s.name = "HPNetwork"
s.version = "1.0.1"
s.version = "1.1.0"
s.summary = "A lightweight but customisable networking stack written in Swift"
s.swift_version = "5.0"

s.homepage = "https://panhans.dev/opensource/hpnetwork"

Expand Down
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ Network.shared.schedule(request) { result in
}
}
```
The `result` is `Result<DataRequest.Output, Error>` where `DataRequest.Output` is inferred from the request object.
of with the convenience method:
```swift
request.schedule { result in
// Handle result
}
```
The `result` is `Result<Request.Output, Error>` where `Request.Output` is inferred from the request object.
`Network.shared` will do its networking on “com.henrikpanhans.Network” (which is a concurrent queue). If you want to use a custom queue, you can pass it in the initialiser:

```swift
Expand Down Expand Up @@ -60,7 +66,7 @@ struct BasicDataRequest: NetworkRequest {
}

let basicRequest = BasicDataRequest(
url: URL(string: "https://panhans.dev/index.html"),
url: URL(string: "https://panhans.dev/"),
requestMethod: .get
)
```
Expand All @@ -83,7 +89,7 @@ struct BasicDecodableRequest<Output: Decodable>: DecodableRequest {
```

### Combine
You can also call `dataTaskPublisher()` on any `NetworkRequest` instance to get an instance of `AnyPublisher<NetworkRequest.Output, Error`. The publisher will walk through the same validation and error handling process as the regular `Network`.
You can also call `dataTaskPublisher()` on any `NetworkRequest` instance to get an instance of `AnyPublisher<Request.Output, Error`. The publisher will walk through the same validation and error handling process as the regular `Network`.

### Intercepting Errors
By default, instances of `NetworkRequest` will simply forward any encountered errors to the completion block. If you want to do some custom error conversion based on the raw `Data` that was received, you can implement `func convertError(_ error: Error, data: Data?, response: URLResponse?) -> Error` in your request model.
Expand All @@ -110,4 +116,4 @@ Any call to `schedule(request) { result in ... }` returns an instance of `Networ
- [x] Cancellation support
- [x] Progress callback
- [x] Improving the documentation
- [ ] Cookie support
- [ ] Cookie support
File renamed without changes.
19 changes: 19 additions & 0 deletions Sources/HPNetwork/Extensions/URLResponse+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Foundation

extension URLResponse {

func urlError() -> URLError? {
guard let httpResponse = self as? HTTPURLResponse else {
return nil
}

switch httpResponse.statusCode {
case 200...299:
return nil
default:
let errorCode = URLError.Code(rawValue: httpResponse.statusCode)
return URLError(errorCode)
}
}

}
20 changes: 1 addition & 19 deletions Sources/HPNetwork/Network.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,29 +37,11 @@ public class Network {
// MARK: - Requests

@discardableResult
public func schedule<T: NetworkRequest>(request: T, progressHandler: ProgressHandler? = nil, completion: @escaping (Result<T.Output, Error>) -> Void) -> NetworkTask {
public func schedule<T: NetworkRequest>(request: T, progressHandler: ProgressHandler? = nil, completion: @escaping T.Completion) -> NetworkTask {
let operation = NetworkOperation(request: request, progressHandler: progressHandler)
operation.networkCompletionBlock = completion
operationQueue.addOperation(operation)
return operation.networkTask
}

}

extension URLResponse {

func urlError() -> URLError? {
guard let httpResponse = self as? HTTPURLResponse else {
return nil
}

switch httpResponse.statusCode {
case 200...299:
return nil
default:
let errorCode = URLError.Code(rawValue: httpResponse.statusCode)
return URLError(errorCode)
}
}

}
9 changes: 1 addition & 8 deletions Sources/HPNetwork/NetworkRequestAuthentication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ public enum NetworkRequestAuthentication {
case basic(username: String, password: String)
case raw(string: String)
case bearer(token: String)
case musicUserToken(token: String)

var headerString: String {
switch self {
Expand All @@ -17,17 +16,11 @@ public enum NetworkRequestAuthentication {
return string
case .bearer(let token):
return "Bearer \(token)"
case .musicUserToken(let token):
return token
}
}

var headerField: NetworkRequestHeaderField {
if case .musicUserToken = self {
return NetworkRequestHeaderField(name: "Music-User-Token", value: headerString)
} else {
return NetworkRequestHeaderField(name: "Authorization", value: headerString)
}
NetworkRequestHeaderField(name: "Authorization", value: headerString)
}

}
4 changes: 4 additions & 0 deletions Sources/HPNetwork/NetworkRequestHeaderField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ public struct NetworkRequestHeaderField {
NetworkRequestHeaderField(name: "Content-Type", value: "application/json")
}

public static func musicUserToken(_ userToken: String) -> NetworkRequestHeaderField {
NetworkRequestHeaderField(name: "Music-User-Token", value: userToken)
}

public init(name: String, value: String) {
self.name = name
self.value = value
Expand Down
7 changes: 2 additions & 5 deletions Sources/HPNetwork/Operations/NetworkOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,9 @@ public typealias ProgressHandler = (Progress) -> Void

class NetworkOperation<R: NetworkRequest>: Operation {

typealias RequestResult = Result<R.Output, Error>
typealias Completion = (Result<R.Output, Error>) -> Void

let request: R
let progressHandler: ProgressHandler?
var networkCompletionBlock: Completion?
var networkCompletionBlock: R.Completion?
let networkTask = NetworkTask()

private var data: Data?
Expand Down Expand Up @@ -82,7 +79,7 @@ class NetworkOperation<R: NetworkRequest>: Operation {
#endif
}

private func finish(with result: RequestResult) {
private func finish(with result: R.RequestResult) {
request.finishingQueue.sync {
networkCompletionBlock?(result)
networkCompletionBlock = nil
Expand Down
43 changes: 16 additions & 27 deletions Sources/HPNetwork/Requests/NetworkRequest+Combine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,23 @@ public extension NetworkRequest {

@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
func dataTaskPublisher() -> AnyPublisher<Output, Error> {
guard let request = urlRequest() else {
return NetworkRequestErrorPublisher<Output>(error: NSError.failedToCreate).eraseToAnyPublisher()
}

return urlSession.dataTaskPublisher(for: request)
.receive(on: finishingQueue)
.tryMap { data, response in
if let error = response.urlError() {
let convertedError = convertError(error, data: data, response: response)
throw convertedError
if let request = urlRequest() {
return urlSession.dataTaskPublisher(for: request)
.receive(on: finishingQueue)
.tryMap { data, response in
if let error = response.urlError() {
let convertedError = convertError(error, data: data, response: response)
throw convertedError
}
return NetworkResponse(data: data, urlResponse: response)
}
return NetworkResponse(data: data, urlResponse: response)
}
.tryMap(convertResponse)
.eraseToAnyPublisher()
}

}

@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
struct NetworkRequestErrorPublisher<Output>: Publisher {

typealias Failure = Error

let error: Failure

func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
subscriber.receive(completion: .failure(error))
.tryMap(convertResponse)
.eraseToAnyPublisher()
} else {
return Future<Output, Error> { completion in
completion(.failure(NSError.failedToCreate))
}.eraseToAnyPublisher()
}
}

}
Expand Down
51 changes: 32 additions & 19 deletions Sources/HPNetwork/Requests/NetworkRequest.swift
Original file line number Diff line number Diff line change
@@ -1,41 +1,33 @@
import Foundation

// MARK: - NetworkRequest

public protocol NetworkRequest {

associatedtype Output

/**
Generates a URLRequest from the request. This will be run on a background thread so model parsing is allowed.
*/
func urlRequest() -> URLRequest?
typealias RequestResult = Result<Output, Error>
typealias Completion = (RequestResult) -> Void

var finishingQueue: DispatchQueue { get }
var url: URL? { get }
var headerFields: [NetworkRequestHeaderField]? { get }
var httpBody: Data? { get }
var urlSession: URLSession { get }

var headerFields: [NetworkRequestHeaderField]? { get }
var requestMethod: NetworkRequestMethod { get }
var authentication: NetworkRequestAuthentication? { get }
var urlSession: URLSession { get }

func convertResponse(response: NetworkResponse) throws -> Output
func convertError(_ error: Error, data: Data?, response: URLResponse?) -> Error

}

// Some sensible defaults

public extension NetworkRequest {

var headerFields: [NetworkRequestHeaderField]? { nil }

var httpBody: Data? { nil }

var finishingQueue: DispatchQueue { .main }

var authentication: NetworkRequestAuthentication? { nil }

var urlSession: URLSession { .shared }
extension NetworkRequest {

/**
Generates a URLRequest from the request. This will be run on a background thread so model parsing is allowed.
*/
func urlRequest() -> URLRequest? {
guard let url = url else {
return nil
Expand All @@ -53,10 +45,31 @@ public extension NetworkRequest {
return request
}

}

// MARK: - Sensible Defaults

public extension NetworkRequest {

var finishingQueue: DispatchQueue { .main }

var httpBody: Data? { nil }

var urlSession: URLSession { .shared }

var headerFields: [NetworkRequestHeaderField]? { nil }

var authentication: NetworkRequestAuthentication? { nil }

func convertError(_ error: Error, data: Data?, response: URLResponse?) -> Error {
error
}

@discardableResult
func schedule(on network: Network = .shared, progressHandler: ProgressHandler? = nil, completion: @escaping Completion) -> NetworkTask {
network.schedule(request: self, progressHandler: progressHandler, completion: completion)
}

}

// MARK: - Raw Data
Expand Down
6 changes: 1 addition & 5 deletions Tests/HPNetworkTests/NetworkTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,13 @@ class NetworkTests: XCTestCase {
}

func testConcurrentOperations() {
let network = Network.shared

let expectation = XCTestExpectation(description: "fetched request from server")
expectation.expectedFulfillmentCount = 20

for _ in 0...20 {
let request = BasicRequest(url: URL(string: "https://panhans.dev"))

network.schedule(request: request) { progress in
// print("Progress for request \(i):", progress.fractionCompleted)
} completion: { result in
request.schedule { _ in
expectation.fulfill()
}
}
Expand Down

0 comments on commit 827edb4

Please sign in to comment.