Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BSU-0020] - Generic Async/Await Network Layer #39

Open
wants to merge 52 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
6575345
Add 'ApiManager' and a generic urlRequest method to return a 'URLRequ…
Jan 9, 2023
bc68732
fix test class compile errors
Jan 9, 2023
ca35d04
Fix 'Module was not compiled for testing' error
Jan 9, 2023
c46f3ca
Create 'BaseService' class conforms to 'BaseServiceProtocol' and impl…
Jan 10, 2023
0ea4afc
create AdessoServiceProtocol
Jan 11, 2023
862a189
add 'baseURL' static variable to the 'Configuration' class
Jan 11, 2023
e7b3be7
add 'AdessoServiceProtocol', 'BaseServiceProvider' and 'BaseEndpoint'
Jan 11, 2023
5a07dcb
make generic request method to take responseModel as an argument, so …
Jan 12, 2023
91af74b
add 'testable' keyword to 'BoilerPlateSwiftUITests' file previously d…
Jan 12, 2023
b3cd111
Make implicit dependency 'URLSession' explicit by defining a 'URLSess…
Jan 12, 2023
37fd488
Pass session parameter to all cascading request method invokes
Jan 12, 2023
47a4618
Guarantee that request method invokes URLSession 'data for request' o…
Jan 12, 2023
ef5f369
Remove unused build method in 'AdessoServiceProtocol'
Jan 12, 2023
7175b6f
Add example use case for AdessoServiceProtocol
Jan 12, 2023
7aace2a
Make session and decoder dependency of 'BaseService' constructor inje…
Jan 12, 2023
9425ec9
Add 'ExampleResponse' inside 'ResponseModels' group
Jan 13, 2023
fabd161
Add 'WebServiceProvider'
Jan 13, 2023
da49c99
Add 'ExampleRepository' and 'ExampleRemoteDataSource'
Jan 13, 2023
57e5aa1
Make the test code uses 'ExampleResponse' decodable type instead of S…
Jan 13, 2023
81ff01a
Fix SwiftLint dictionary colon error by removing the whitespace
Jan 13, 2023
7f254a0
Reorganize project files
Jan 16, 2023
7103775
Add 'authenticatedRequest' method, but 'prepareAuthenticatedRequest' …
Jan 16, 2023
5125821
Make the parameter requestObject inout so it can be modified inside t…
Jan 16, 2023
520228c
Request method throws an error when URLSession fails
Jan 18, 2023
c466507
Remove 'Result' return type from request methods, because async metho…
Jan 18, 2023
2d6fa51
Request command fails on non OK HTTPStatusCode
Jan 18, 2023
50241d1
Change expectation timeouts to 1 instead of 5 seconds
Jan 18, 2023
a0be858
Change the name of 'BaseService' to 'NetworkLoader'
Jan 18, 2023
390bf5f
Fix nonOKHTTPStatus code test succeeds on 200 status code
Jan 18, 2023
862a0f4
Change name of 'AdessoServiceProtocol' to 'BaseServiceProtocol'
Jan 18, 2023
187879e
Change SUT 'NetworkLoaderProtocol' instead of 'ExampleService' since …
Jan 18, 2023
ac893ad
Store arrays rather than optional values inside 'URLSessionSpy', so t…
Jan 19, 2023
c21bac8
Extract duplicated test code into 'expect' helper method to simplify …
Jan 19, 2023
66cd312
Fix all the swiftLint errors, clear 'NetworkLoaderTests', Make 'Adess…
Jan 19, 2023
2134fa8
Extract URLRequest creation responsibility to a helper method 'prepar…
Jan 19, 2023
5bc3d3b
Remove `AdessoError` equatable conformance and add it inside the test…
Jan 24, 2023
d15804b
Move `URLSessionSpy` creation inside the makeSUT helper factory metho…
Jan 25, 2023
00a9de5
Does not request upon creation
Jan 25, 2023
299574b
Rename failure test method names, to be more clear
Jan 25, 2023
3add40c
Make `ExampleResponse` a struct, since it doesn't have any behavior
Jan 25, 2023
1793619
Move `AdessoError` equatable conformance to the project target
Jan 25, 2023
e894e13
Delivers `HTTPError` depending on the http status code received from…
Jan 25, 2023
cdd1e32
Delivers error when the given url is invalid (AdessoError.badURL)
Jan 25, 2023
b9e7970
Delivers badURL error with the given url when the string is not conve…
Jan 26, 2023
700d6b5
Delivers notValidCode error when response couldn't be converted to HT…
Jan 26, 2023
d03e8e7
Add some extensions to Dictionary, Encodable, String, DecodingError, …
Jan 26, 2023
d79eb95
Delivers error on invalid data with successful response
Jan 30, 2023
66385a0
`Request` command succeeds on correct data, parse the json and return…
Jan 30, 2023
d04fd5e
Use test class's own `TestResponse` instead of `ExampleResponse` to p…
Jan 31, 2023
e3f1bef
Merge branch 'develop' into BSU-0020, conflicts resolved
Feb 2, 2023
3f59829
Merge branch 'develop' into BSU-0020
egesucu Feb 16, 2023
90dbc1b
Merge branch 'develop' into BSU-0020
egesucu Feb 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 203 additions & 8 deletions BoilerPlateSwiftUI.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
skipped = "NO"
testExecutionOrdering = "random">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2FCDE1072565722600203445"
Expand All @@ -52,16 +53,6 @@
ReferencedContainer = "container:BoilerPlateSwiftUI.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2FCDE1122565722600203445"
BuildableName = "BoilerPlateSwiftUIUITests.xctest"
BlueprintName = "BoilerPlateSwiftUIUITests"
ReferencedContainer = "container:BoilerPlateSwiftUI.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
Expand Down
4 changes: 4 additions & 0 deletions BoilerPlateSwiftUI/Configs/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,8 @@ final class Configuration {
#endif
}

static var baseURL: String {
""
}

}
40 changes: 40 additions & 0 deletions BoilerPlateSwiftUI/Network/Base/BaseServiceProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// BaseServiceProtocol.swift
// BoilerPlateSwiftUI
//
// Created by Saglam, Fatih on 11.01.2023.
// Copyright © 2023 Adesso Turkey. All rights reserved.
//

import Foundation

protocol BaseServiceProtocol {
associatedtype Endpoint: TargetEndpointProtocol

var networkLoader: NetworkLoaderProtocol { get }

func request<T: Decodable>(with requestObject: RequestObject, responseModel: T.Type) async throws -> T
func authenticatedRequest<T: Decodable>(with requestObject: RequestObject, responseModel: T.Type) async throws -> T
}

extension BaseServiceProtocol {

func request<T: Decodable>(with requestObject: RequestObject, responseModel: T.Type) async throws -> T {
try await networkLoader.request(with: requestObject, responseModel: responseModel)
}

func build(endpoint: Endpoint) -> String {
endpoint.path
}

func authenticatedRequest<T: Decodable>(with requestObject: RequestObject, responseModel: T.Type) async throws -> T {
var requestObject = requestObject
return try await networkLoader.request(with: prepareAuthenticatedRequest(with: &requestObject), responseModel: responseModel)
}

private func prepareAuthenticatedRequest(with requestObject: inout RequestObject) -> RequestObject {
// TODO: - handle authenticatedRequest with urlSession

return requestObject
}
}
14 changes: 14 additions & 0 deletions BoilerPlateSwiftUI/Network/Base/NetworkLoader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// NetworkLoader.swift
// BoilerPlateSwiftUI
//
// Created by Saglam, Fatih on 10.01.2023.
// Copyright © 2023 Adesso Turkey. All rights reserved.
//

import Foundation

class NetworkLoader: NetworkLoaderProtocol {
var session: URLSessionProtocol = URLSession.shared
var decoder: JSONDecoder = JSONDecoder()
}
47 changes: 47 additions & 0 deletions BoilerPlateSwiftUI/Network/Base/NetworkLoaderProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// NetworkLoaderProtocol.swift
// BoilerPlateSwiftUI
//
// Created by Saglam, Fatih on 10.01.2023.
// Copyright © 2023 Adesso Turkey. All rights reserved.
//

import Foundation

protocol URLSessionProtocol {
func data(for request: URLRequest, delegate: URLSessionTaskDelegate?) async throws -> (Data, URLResponse)
}

extension URLSession: URLSessionProtocol { }

protocol NetworkLoaderProtocol {
var session: URLSessionProtocol { get set }
var decoder: JSONDecoder { get set }

func request<T: Decodable>(with requestObject: RequestObject, responseModel: T.Type) async throws -> T
}

extension NetworkLoaderProtocol {
func request<T: Decodable>(with requestObject: RequestObject, responseModel: T.Type) async throws -> T {
let (data, response) = try await session.data(for: prepareURLRequest(with: requestObject), delegate: nil)
let successCodeRange = 200...299
guard let statusCode = (response as? HTTPURLResponse)?.statusCode else { throw AdessoError.badResponse }
guard successCodeRange.contains(statusCode) else { throw AdessoError.httpError(status: HTTPStatus(rawValue: statusCode) ?? .notValidCode) }
do {
let decodedData = try decoder.decode(responseModel, from: data)
return decodedData
} catch {
throw AdessoError.mappingFailed(data: data)
}
}

private func prepareURLRequest(with requestObject: RequestObject) throws -> URLRequest {
guard let url = URL(string: requestObject.url) else { throw AdessoError.badURL(requestObject.url) }
var request = URLRequest(url: url)
request.httpMethod = requestObject.method.rawValue
request.allHTTPHeaderFields = requestObject.headers
request.httpBody = requestObject.body

return request
}
}
20 changes: 20 additions & 0 deletions BoilerPlateSwiftUI/Network/Base/NetworkLoaderProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// NetworkLoaderProvider.swift
// BoilerPlateSwiftUI
//
// Created by Saglam, Fatih on 11.01.2023.
// Copyright © 2023 Adesso Turkey. All rights reserved.
//

import Foundation

class NetworkLoaderProvider {

static let shared: NetworkLoaderProvider = NetworkLoaderProvider()

let networkLoader: NetworkLoaderProtocol

private init() {
networkLoader = NetworkLoader()
}
}
20 changes: 20 additions & 0 deletions BoilerPlateSwiftUI/Network/Endpoints/BaseEndpoint.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// BaseEndpoint.swift
// BoilerPlateSwiftUI
//
// Created by Saglam, Fatih on 11.01.2023.
// Copyright © 2023 Adesso Turkey. All rights reserved.
//

import Foundation

enum BaseEndpoint: TargetEndpointProtocol {
case base

var path: String {
switch self {
case .base:
return Configuration.baseURL
}
}
}
54 changes: 54 additions & 0 deletions BoilerPlateSwiftUI/Network/Entities/AdessoError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// AdessoError.swift
// BoilerPlateSwiftUI
//
// Created by Saglam, Fatih on 10.01.2023.
// Copyright © 2023 Adesso Turkey. All rights reserved.
//

import Foundation

enum AdessoError: Error, Equatable {
case httpError(status: HTTPStatus, data: Data? = nil)
case badURL(_ url: String)
case unknown(error: NSError)
case customError(_ code: Int, _ message: String, _ data: Data? = nil)
case mappingFailed(data: Data? = nil)
case badResponse

var errorCode: Int {
switch self {
case .httpError(let error, _):
return error.rawValue
case .unknown(let error):
return error.code
case .customError(let code, _, _):
return code
case .mappingFailed:
return 0
case .badResponse:
return 0
case .badURL:
return 0
}
}

var response: ErrorResponse? {
getResponse()
}
}

extension AdessoError {
private func getResponse() -> ErrorResponse? {
switch self {
case .httpError(_, let data), .customError(_, _, let data):
if let data = data {
let response = try? JSONDecoder().decode(ErrorResponse.self, from: data)
return response
}
return nil
case .badResponse, .mappingFailed, .unknown, .badURL:
return nil
}
}
}
15 changes: 15 additions & 0 deletions BoilerPlateSwiftUI/Network/Entities/ErrorResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// ErrorResponse.swift
// BoilerPlateSwiftUI
//
// Created by Saglam, Fatih on 10.01.2023.
// Copyright © 2023 Adesso Turkey. All rights reserved.
//

import Foundation

class ErrorResponse: Decodable {
var code: Int?
var message: String?
var messages: [String: String]?
}
88 changes: 88 additions & 0 deletions BoilerPlateSwiftUI/Network/Entities/HTTPStatus.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//
// HTTPStatus.swift
// BoilerPlateSwiftUI
//
// Created by Saglam, Fatih on 10.01.2023.
// Copyright © 2023 Adesso Turkey. All rights reserved.
//

import Foundation

enum HTTPStatus: Int, Error {
// Default
case notValidCode = 0

// 1xx Informational
case `continue` = 100
case switchingProtocols = 101
case processing = 102

// 2xx Success
case okay = 200
case created = 201
case accepted = 202
case nonAuthoritativeInformation = 203
case noContent = 204
case resetContent = 205
case partialContent = 206
case multiStatus = 207
case alreadyReported = 208
case IMUsed = 226

// 3xx Redirection
case multipleChoices = 300
case movedPermanently = 301
case found = 302
case seeOther = 303
case notModified = 304
case useProxy = 305
case switchProxy = 306
case temporaryRedirect = 307
case permenantRedirect = 308

// 4xx Client Error
case badRequest = 400
case unauthorized = 401
case paymentRequired = 402
case forbidden = 403
case notFound = 404
case methodNotAllowed = 405
case notAcceptable = 406
case proxyAuthenticationRequired = 407
case timeout = 408
case conflict = 409
case gone = 410
case lengthRequired = 411
case preconditionFailed = 412
case payloadTooLarge = 413
case requestURITooLong = 414
case unsupportedMediaType = 415
case requestedRangeNotSatisfiable = 416
case expectationFailed = 417
case teapot = 418
case misdirectedRequest = 421
case unprocessableEntity = 422
case locked = 423
case failedDependency = 424
case upgradeRequired = 426
case preconditionRequired = 428
case tooManyRequests = 429
case requestHeaderFieldsTooLarge = 431
case connectionClosedWithoutResponse = 444
case unavailableForLegalReasons = 451
case clientClosedRequest = 499

// 5xx Server Error
case internalServerError = 500
case notImplemented = 501
case badGateway = 502
case serviceUnavailable = 503
case gatewayTimeout = 504
case httpVersionNotSupported = 505
case variantAlsoNegotiates = 506
case insufficientStorage = 507
case loopDetected = 508
case notExtended = 510
case networkAuthenticationRequired = 511
case networkConnectTimeoutError = 599
}
37 changes: 37 additions & 0 deletions BoilerPlateSwiftUI/Network/Entities/RequestObject.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// RequestObject.swift
// BoilerPlateSwiftUI
//
// Created by Saglam, Fatih on 10.01.2023.
// Copyright © 2023 Adesso Turkey. All rights reserved.
//

import Foundation

struct RequestObject {
var url: String
let method: HTTPMethod
var data: Encodable?
var headers: [String: String]?
var body: Data?

init(url: String,
method: HTTPMethod = .get,
data: Encodable? = nil,
headers: [String: String] = [:],
body: Data? = nil) {
self.url = url
self.method = method
self.data = data
self.headers = headers
self.body = body
}
}

enum HTTPMethod: String {
case delete = "DELETE"
case get = "GET"
case patch = "PATCH"
case post = "POST"
case put = "PUT"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// HTTPURLResponseExtensions.swift
// BoilerPlateSwiftUI
//
// Created by Saglam, Fatih on 10.01.2023.
// Copyright © 2023 Adesso Turkey. All rights reserved.
//

import Foundation

extension HTTPURLResponse {
var httpStatus: HTTPStatus? {
HTTPStatus(rawValue: statusCode)
}
}
Loading