Skip to content

Commit

Permalink
Package init
Browse files Browse the repository at this point in the history
  • Loading branch information
satish committed Apr 29, 2023
0 parents commit 4ca5c08
Show file tree
Hide file tree
Showing 29 changed files with 1,160 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .gitignore
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
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>
29 changes: 29 additions & 0 deletions Package.swift
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"]),
]
)
128 changes: 128 additions & 0 deletions README.md
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
24 changes: 24 additions & 0 deletions Sources/NetworkKing/Erorrs/Error+NetworkError.swift
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)
}
}
}
33 changes: 33 additions & 0 deletions Sources/NetworkKing/Erorrs/NetworkError.swift
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 Sources/NetworkKing/Interceptor/Adapter/RequestAdapter.swift
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
}
39 changes: 39 additions & 0 deletions Sources/NetworkKing/Interceptor/RequestInterceptor.swift
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
}
}
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
}
Loading

0 comments on commit 4ca5c08

Please sign in to comment.