-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
satish
committed
Apr 29, 2023
0 parents
commit 4ca5c08
Showing
29 changed files
with
1,160 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,9 @@ | ||
.DS_Store | ||
/.build | ||
/Packages | ||
/*.xcodeproj | ||
xcuserdata/ | ||
DerivedData/ | ||
.swiftpm/config/registries.json | ||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata | ||
.netrc |
8 changes: 8 additions & 0 deletions
8
.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
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,8 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
<plist version="1.0"> | ||
<dict> | ||
<key>IDEDidComputeMac32BitWarning</key> | ||
<true/> | ||
</dict> | ||
</plist> |
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,29 @@ | ||
// swift-tools-version: 5.8 | ||
// The swift-tools-version declares the minimum version of Swift required to build this package. | ||
|
||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "NetworkKing", | ||
platforms: [.iOS(.v16)], | ||
products: [ | ||
// Products define the executables and libraries a package produces, and make them visible to other packages. | ||
.library( | ||
name: "NetworkKing", | ||
targets: ["NetworkKing"]), | ||
], | ||
dependencies: [ | ||
// Dependencies declare other packages that this package depends on. | ||
// .package(url: /* package url */, from: "1.0.0"), | ||
], | ||
targets: [ | ||
// Targets are the basic building blocks of a package. A target can define a module or a test suite. | ||
// Targets can depend on other targets in this package, and on products in packages this package depends on. | ||
.target( | ||
name: "NetworkKing", | ||
dependencies: []), | ||
.testTarget( | ||
name: "NetworkKingTests", | ||
dependencies: ["NetworkKing"]), | ||
] | ||
) |
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,128 @@ | ||
# NetworkKing | ||
|
||
![Swift](https://img.shields.io/badge/Swift-5.8-orange?style=flat-square) | ||
![Swift Package Manager](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange?style=flat-square) | ||
![Platforms](https://img.shields.io/badge/Platforms-iOS-yellowgreen?style=flat-square) | ||
|
||
Networking is a network abstraction layer written in Swift. It does not implement its own HTTP networking functionality. Instead it builds on top of URLSession. | ||
|
||
##Features | ||
|
||
✅ Compile-time checking for correct API endpoint accesses. | ||
✅ Lets you define a clear usage of different endpoints with associated enum values. | ||
✅ Swift's concurrency support | ||
✅ Inspection and mutation support for each request before being start | ||
✅ Response validation | ||
✅ Errors handling | ||
✅ Comprehensive Unit test coverage | ||
|
||
##Usage | ||
|
||
__Routing__: So how do you use this module? Well it's really simple. First set up `enum` with all your api targets. You can include information as part of your enum. For example first create a new enum MyApiTarget: | ||
|
||
```Swift | ||
import Networking | ||
|
||
enum MyApiTarget { | ||
case getMyData | ||
//... | ||
} | ||
|
||
extension MyApiTarget: NetworkTargetType { | ||
var baseURL: URL { | ||
URL(string: "https://jsonplaceholder.cypress.io")! | ||
} | ||
var path: String { | ||
"/todos/1" | ||
} | ||
var method: Networking.HTTPMethod { | ||
.get | ||
} | ||
} | ||
``` | ||
This enum is used to make sure that you provide implementation details for each target at compile time. The enum must additionally confirm to the `NetworkTargetType` protocol like above. | ||
|
||
Now create an instance of NetworkProvider and retain the provider somewhere. (Note that NetworkProvider is a generic class) | ||
|
||
```Swift | ||
let myProvider = NetworkProvider<MyApiTarget>.init() | ||
``` | ||
|
||
Now how do we make a request? Just asynchronously call `perform` method and provide your target api (eg. .getMyData) and response type(if needed) in order to decode/map network data into your custom type(eg. User). | ||
|
||
```Swift | ||
struct User: Codable { | ||
let userId: Int | ||
let id: Int | ||
let title: String | ||
let completed: Bool | ||
} | ||
|
||
Task { | ||
let response = try? await myProvider.perform(target: .getMyData, response: User.self) | ||
} | ||
``` | ||
|
||
|
||
__RequestInterceptor__: Networking module can mutate or inspect each url request before its being made. What you needs to do is create a type that confirm to the `RequestAdapter` and pass an instance of that type into NetworkProvider's init like below: | ||
|
||
```Swift | ||
struct NetworkEventMonitor: RequestAdapter { | ||
func adapt(_ urlRequest: URLRequest, for target: NetworkTargetType) async throws -> URLRequest { | ||
print("NetworkEvent received with url:\n\(urlRequest.url)") | ||
return urlRequest | ||
} | ||
} | ||
|
||
|
||
let interceptor = RequestInterceptor(adapters: [NetworkEventMonitor(), ...]) | ||
let myProvider = NetworkProvider<UserService>.init(requestInterceptor: interceptor) | ||
``` | ||
|
||
|
||
__DataResponseValidator__: You can also perform response validation before decoding/mapping in order to do that your type needs to confirm `DataResponseValidator` protocol which has single method requirement called validate . Inside that method you needs to make a decision about your response wether its valid or not and return your result like below: | ||
|
||
```Swift | ||
struct MyCustomDataResponseValidator: DataResponseValidator { | ||
func validate(_ data: Data, response urlResponse: URLResponse) -> Result<Void, NetworkError> { | ||
if let response = urlResponse as? HTTPURLResponse, response.statusCode == 401 { | ||
return .failure(.responseValidationFailed(error: NSError(domain: "Network", code: response.statusCode))) | ||
} | ||
return .success(Void()) | ||
} | ||
} | ||
|
||
// With builtin default validator | ||
let myProvider1 = NetworkProvider<UserService>.init(whiteHatResponseValidator: .default) | ||
// With custom validator | ||
let myProvider2 = NetworkProvider<UserService>.init(dataResponseValidator: MyCustomDataResponseValidator()) | ||
``` | ||
|
||
|
||
__Errors handling__: While making network requests it always possible that errors may occurred. You can catch any error thrown by perform method of NetworkProvider using traditional do {} catch {} statement. | ||
|
||
```Swift | ||
Task { | ||
do { | ||
let response = try await myProvider.perform(target: .getMyData, response: User.self) | ||
// Handle your response | ||
} catch let error as NetworkError { | ||
// Handle error | ||
switch error { | ||
case .decodingFailed(let error): | ||
<#code#> | ||
case .encodingFailed(let error): | ||
<#code#> | ||
case .underlaying(let error): | ||
<#code#> | ||
case .responseValidationFailed(let error): | ||
<#code#> | ||
} | ||
} | ||
} | ||
``` | ||
|
||
##References | ||
Alamofire routing: https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#routing-requests | ||
|
||
Moya: https://github.com/Moya/Moya |
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,24 @@ | ||
// | ||
// Error+NetworkError.swift | ||
// | ||
// | ||
// Created by Satish Vekariya on 29/04/2023. | ||
// | ||
|
||
import Foundation | ||
|
||
public extension Error { | ||
/// Convert error instance into `NetworkError` | ||
/// - Returns: Instance of `NetworkError` | ||
func toNetworkError() -> NetworkError { | ||
if let networkError = self as? NetworkError { | ||
return networkError | ||
} | ||
switch self { | ||
case let error as DecodingError: | ||
return .decodingFailed(error: error) | ||
default: | ||
return .underlaying(error: 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,33 @@ | ||
// | ||
// NetworkError.swift | ||
// | ||
// | ||
// Created by Satish Vekariya on 29/04/2023. | ||
// | ||
|
||
import Foundation | ||
|
||
public enum NetworkError: Error { | ||
case decodingFailed(error: Error) | ||
case encodingFailed(error: Error) | ||
case urlEncodingFailed(reason: String) | ||
case underlaying(error: Error) | ||
case responseValidationFailed(error: Error?) | ||
} | ||
|
||
extension NetworkError: LocalizedError { | ||
public var errorDescription: String? { | ||
switch self { | ||
case let .decodingFailed(error): | ||
return "Response could not be decoded because of error:\n\(error.localizedDescription)" | ||
case let .encodingFailed(error): | ||
return "Request could not be encoded because of error:\n\(error.localizedDescription)" | ||
case let .urlEncodingFailed(reason): | ||
return "Request could not be encoded because of:\n\(reason)" | ||
case let .underlaying(error): | ||
return "Underlaying error:\n\(error)" | ||
case let .responseValidationFailed(error): | ||
return "Response validation failed because of error:\n\(error?.localizedDescription ?? "unknown error")" | ||
} | ||
} | ||
} |
19 changes: 19 additions & 0 deletions
19
Sources/NetworkKing/Interceptor/Adapter/RequestAdapter.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,19 @@ | ||
// | ||
// RequestAdapter.swift | ||
// | ||
// | ||
// Created by Satish Vekariya on 29/04/2023. | ||
// | ||
|
||
import Foundation | ||
|
||
public protocol RequestAdapter { | ||
/// Inspects and adapts the specified `URLRequest` in some manner and return new `URLRequest`. | ||
/// | ||
/// - Parameters: | ||
/// - urlRequest: The `URLRequest` to adapt. | ||
/// - session: The `Session` that will execute the `URLRequest`. | ||
/// - target: The target of the request. | ||
/// - Returns: New `URLRequest` or throws. | ||
func adapt(_ urlRequest: URLRequest, for target: NetworkTargetType) async throws -> 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,39 @@ | ||
// | ||
// RequestInterceptor.swift | ||
// | ||
// | ||
// Created by Satish Vekariya on 29/04/2023. | ||
// | ||
|
||
import Foundation | ||
|
||
/// `RequestInterceptor` which can use multiple `RequestAdapter` | ||
public struct RequestInterceptor: RequestAdapter, RequestRetrier { | ||
/// All `RequestAdapter`s associated with the instance. These adapters will be run until one fails. | ||
public let adapters: [RequestAdapter] | ||
/// All `RequestRetrier`s associated with the instance. These retriers will be run one at a time until one triggers retry. | ||
public let retriers: [RequestRetrier] | ||
|
||
public init(adapters: [RequestAdapter] = [], retriers: [RequestRetrier] = []) { | ||
self.adapters = adapters | ||
self.retriers = retriers | ||
} | ||
|
||
public func adapt(_ urlRequest: URLRequest, for target: NetworkTargetType) async throws -> URLRequest { | ||
var urlRequest = urlRequest | ||
for adapter in adapters { | ||
urlRequest = try await adapter.adapt(urlRequest, for: target) | ||
} | ||
return urlRequest | ||
} | ||
|
||
public func retry(_ request: URLRequest, for target: NetworkTargetType, dueTo error: Error) async throws -> RetryResult { | ||
for retrier in retriers { | ||
let result = try await retrier.retry(request, for: target, dueTo: error) | ||
if result == .doNotRetry { | ||
break | ||
} | ||
} | ||
return .doNotRetry | ||
} | ||
} |
26 changes: 26 additions & 0 deletions
26
Sources/NetworkKing/Interceptor/RequestRetrier/RequestRetrier.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,26 @@ | ||
// | ||
// RequestRetrier.swift | ||
// | ||
// | ||
// Created by Satish Vekariya on 29/04/2023. | ||
// | ||
|
||
import Foundation | ||
|
||
/// A type that determines whether a request should be retried after being executed by the specified session manager and encountering an error. | ||
public protocol RequestRetrier { | ||
/// Determines whether the `URLRequest` should be retried by returning the `RetryResult` enum value. | ||
/// - Parameters: | ||
/// - request: `URLRequest` that failed due to the provided `Error`. | ||
/// - target: The target of the request. | ||
/// - error: `Error` encountered while executing the `URLRequest`. | ||
/// - Returns: An enum value to be returned when a retry decision has been determined. | ||
func retry(_ request: URLRequest, for target: NetworkTargetType, dueTo error: Error) async throws -> RetryResult | ||
} | ||
|
||
public enum RetryResult { | ||
/// Retry should be attempted immediately. | ||
case retry | ||
/// Do not retry. | ||
case doNotRetry | ||
} |
Oops, something went wrong.