From c548e36c7baca68137b64b3a90c66770e9c1097c Mon Sep 17 00:00:00 2001 From: samgilmore <30483214+samgilmore@users.noreply.github.com> Date: Tue, 16 Jul 2024 11:59:58 -0400 Subject: [PATCH 1/4] Create basic protocols and implementations Added network and request protocols as well as base implementations for each. --- Sources/SwiftNetKit/BaseRequest.swift | 32 ++++++++ Sources/SwiftNetKit/NetworkService.swift | 76 +++++++++++++++++-- .../Protocols/NetworkServiceProtocol.swift | 14 ++++ .../Protocols/RequestProtocol.swift | 41 ++++++++++ .../NetworkServiceTests.swift | 48 ++++++++++-- 5 files changed, 200 insertions(+), 11 deletions(-) create mode 100644 Sources/SwiftNetKit/BaseRequest.swift create mode 100644 Sources/SwiftNetKit/Protocols/NetworkServiceProtocol.swift create mode 100644 Sources/SwiftNetKit/Protocols/RequestProtocol.swift diff --git a/Sources/SwiftNetKit/BaseRequest.swift b/Sources/SwiftNetKit/BaseRequest.swift new file mode 100644 index 0000000..3a9211b --- /dev/null +++ b/Sources/SwiftNetKit/BaseRequest.swift @@ -0,0 +1,32 @@ +// +// BaseRequest.swift +// +// +// Created by Sam Gilmore on 7/16/24. +// + +import Foundation + +public struct BaseRequest: RequestProtocol { + typealias ResponseType = Response + + let url: URL + let method: MethodType + let parameters: [String : Any]? + let headers: [String : String]? + let body: Data? + + init( + url: URL, + method: MethodType, + parameters: [String : Any]? = nil, + headers: [String : String]? = nil, + body: Data? = nil + ) { + self.url = url + self.method = method + self.parameters = parameters + self.headers = headers + self.body = body + } +} diff --git a/Sources/SwiftNetKit/NetworkService.swift b/Sources/SwiftNetKit/NetworkService.swift index 1787da6..ea52854 100644 --- a/Sources/SwiftNetKit/NetworkService.swift +++ b/Sources/SwiftNetKit/NetworkService.swift @@ -7,24 +7,42 @@ import Foundation -public struct NetworkService { +public struct NetworkService: NetworkServiceProtocol { public init() {} - public func get(from url: URL, decodeTo type: T.Type) async throws -> T { + func start(_ request: Request) async throws -> Request.ResponseType { do { - let (data, response) = try await URLSession.shared.data(from: url) + var urlRequest = URLRequest(url: request.url) + + if let parameters = request.parameters { + let queryItems = parameters.map { key, value in + URLQueryItem(name: key, value: "\(value)") + } + var urlComponents = URLComponents(url: request.url, resolvingAgainstBaseURL: false) + urlComponents?.queryItems = queryItems + urlRequest.url = urlComponents?.url + } + + urlRequest.httpMethod = request.method.stringValue + urlRequest.allHTTPHeaderFields = request.headers + + if let body = request.body { + urlRequest.httpBody = body + } + + let (data, response) = try await URLSession.shared.data(for: urlRequest) guard let httpResponse = response as? HTTPURLResponse else { throw NetworkError.invalidResponse } - guard httpResponse.statusCode == 200 else { + guard (200..<300).contains(httpResponse.statusCode) else { throw NetworkError.serverError(statusCode: httpResponse.statusCode) } do { - let decodedObject = try JSONDecoder().decode(T.self, from: data) + let decodedObject = try JSONDecoder().decode(Request.ResponseType.self, from: data) return decodedObject } catch { throw NetworkError.decodingFailed @@ -33,4 +51,52 @@ public struct NetworkService { throw NetworkError.requestFailed(error: error) } } + + func start(_ request: Request, completion: @escaping (Result) -> Void) { + var urlRequest = URLRequest(url: request.url) + + if let parameters = request.parameters { + let queryItems = parameters.map { key, value in + URLQueryItem(name: key, value: "\(value)") + } + var urlComponents = URLComponents(url: request.url, resolvingAgainstBaseURL: false) + urlComponents?.queryItems = queryItems + urlRequest.url = urlComponents?.url + } + + urlRequest.httpMethod = request.method.stringValue + urlRequest.allHTTPHeaderFields = request.headers + + if let body = request.body { + urlRequest.httpBody = body + } + + URLSession.shared.dataTask(with: urlRequest) { data, response, error in + if let error = error { + completion(.failure(NetworkError.requestFailed(error: error))) + return + } + + guard let httpResponse = response as? HTTPURLResponse else { + completion(.failure(NetworkError.invalidResponse)) + return + } + + guard (200..<300).contains(httpResponse.statusCode) else { + completion(.failure(NetworkError.serverError(statusCode: httpResponse.statusCode))) + return + } + + if let data = data { + do { + let decodedObject = try JSONDecoder().decode(Request.ResponseType.self, from: data) + completion(.success(decodedObject)) + } catch { + completion(.failure(NetworkError.decodingFailed)) + } + } else { + completion(.failure(NetworkError.invalidResponse)) + } + }.resume() + } } diff --git a/Sources/SwiftNetKit/Protocols/NetworkServiceProtocol.swift b/Sources/SwiftNetKit/Protocols/NetworkServiceProtocol.swift new file mode 100644 index 0000000..e2e6d50 --- /dev/null +++ b/Sources/SwiftNetKit/Protocols/NetworkServiceProtocol.swift @@ -0,0 +1,14 @@ +// +// NetworkServiceProtocol.swift +// +// +// Created by Sam Gilmore on 7/16/24. +// + +protocol NetworkServiceProtocol { + // Async/Await + func start(_ request: Request) async throws -> Request.ResponseType + + // Completion Closure + func start(_ request: Request, completion: @escaping (Result) -> Void) +} diff --git a/Sources/SwiftNetKit/Protocols/RequestProtocol.swift b/Sources/SwiftNetKit/Protocols/RequestProtocol.swift new file mode 100644 index 0000000..ea76d47 --- /dev/null +++ b/Sources/SwiftNetKit/Protocols/RequestProtocol.swift @@ -0,0 +1,41 @@ +// +// RequestProtocol.swift +// +// +// Created by Sam Gilmore on 7/16/24. +// + +import Foundation + +enum MethodType { + case get + case post + case delete + case put + case patch + + var stringValue: String { + switch self { + case .get: + return "GET" + case .post: + return "POST" + case .delete: + return "DELETE" + case .put: + return "PUT" + case .patch: + return "PATCH" + } + } +} + +protocol RequestProtocol { + associatedtype ResponseType: Decodable + + var url: URL { get } + var method: MethodType { get } + var parameters: [String: Any]? { get } + var headers: [String: String]? { get } + var body: Data? { get } +} diff --git a/Tests/SwiftNetKitTests/NetworkServiceTests.swift b/Tests/SwiftNetKitTests/NetworkServiceTests.swift index aef6ffd..582643a 100644 --- a/Tests/SwiftNetKitTests/NetworkServiceTests.swift +++ b/Tests/SwiftNetKitTests/NetworkServiceTests.swift @@ -15,13 +15,49 @@ final class NetworkServiceTests: XCTestCase { super.tearDown() } - func testGetSuccess() async throws { - do { - let post: Post = try await networkService.get(from: getURL, decodeTo: Post.self) - XCTAssertEqual(post.id, 1) - } catch { - XCTFail("Should succeed, but failed with error: \(error)") + func testGetSuccessAsyncAwait() { + let expectation = XCTestExpectation(description: "Fetch data successfully") + + Task { + do { + let baseRequest = BaseRequest( + url: self.getURL, + method: .get + ) + let post: Post = try await self.networkService.start(baseRequest) + XCTAssertEqual(post.userId, 1) + XCTAssertEqual(post.id, 1) + + expectation.fulfill() + } catch { + XCTFail("Failed with error: \(error)") + } } + + wait(for: [expectation], timeout: 5.0) + } + + func testGetSuccessClosure() { + let expectation = XCTestExpectation(description: "Fetch data successfully") + + let baseRequest = BaseRequest( + url: self.getURL, + method: .get + ) + + networkService.start(baseRequest) { result in + switch result { + case .success(let post): + XCTAssertEqual(post.userId, 1) + XCTAssertEqual(post.id, 1) + + expectation.fulfill() + case .failure(let error): + XCTFail("Failed with error: \(error)") + } + } + + wait(for: [expectation], timeout: 5.0) } } From 7542e30fce6b9db1d63d4bc1eca9cb4a3550ecd6 Mon Sep 17 00:00:00 2001 From: samgilmore <30483214+samgilmore@users.noreply.github.com> Date: Tue, 16 Jul 2024 13:18:39 -0400 Subject: [PATCH 2/4] Condense enum + abstract URLRequest --- Sources/SwiftNetKit/BaseRequest.swift | 22 ++++++++++++ Sources/SwiftNetKit/NetworkService.swift | 36 ++----------------- .../Protocols/RequestProtocol.swift | 29 +++++---------- 3 files changed, 32 insertions(+), 55 deletions(-) diff --git a/Sources/SwiftNetKit/BaseRequest.swift b/Sources/SwiftNetKit/BaseRequest.swift index 3a9211b..e15a526 100644 --- a/Sources/SwiftNetKit/BaseRequest.swift +++ b/Sources/SwiftNetKit/BaseRequest.swift @@ -29,4 +29,26 @@ public struct BaseRequest: RequestProtocol { self.headers = headers self.body = body } + + func buildURLRequest() -> URLRequest { + var urlRequest = URLRequest(url: self.url) + + if let parameters = self.parameters { + let queryItems = parameters.map { key, value in + URLQueryItem(name: key, value: "\(value)") + } + var urlComponents = URLComponents(url: self.url, resolvingAgainstBaseURL: false) + urlComponents?.queryItems = queryItems + urlRequest.url = urlComponents?.url + } + + urlRequest.httpMethod = self.method.rawValue + urlRequest.allHTTPHeaderFields = self.headers + + if let body = self.body { + urlRequest.httpBody = body + } + + return urlRequest + } } diff --git a/Sources/SwiftNetKit/NetworkService.swift b/Sources/SwiftNetKit/NetworkService.swift index ea52854..70ded88 100644 --- a/Sources/SwiftNetKit/NetworkService.swift +++ b/Sources/SwiftNetKit/NetworkService.swift @@ -13,23 +13,7 @@ public struct NetworkService: NetworkServiceProtocol { func start(_ request: Request) async throws -> Request.ResponseType { do { - var urlRequest = URLRequest(url: request.url) - - if let parameters = request.parameters { - let queryItems = parameters.map { key, value in - URLQueryItem(name: key, value: "\(value)") - } - var urlComponents = URLComponents(url: request.url, resolvingAgainstBaseURL: false) - urlComponents?.queryItems = queryItems - urlRequest.url = urlComponents?.url - } - - urlRequest.httpMethod = request.method.stringValue - urlRequest.allHTTPHeaderFields = request.headers - - if let body = request.body { - urlRequest.httpBody = body - } + let urlRequest = request.buildURLRequest() let (data, response) = try await URLSession.shared.data(for: urlRequest) @@ -53,23 +37,7 @@ public struct NetworkService: NetworkServiceProtocol { } func start(_ request: Request, completion: @escaping (Result) -> Void) { - var urlRequest = URLRequest(url: request.url) - - if let parameters = request.parameters { - let queryItems = parameters.map { key, value in - URLQueryItem(name: key, value: "\(value)") - } - var urlComponents = URLComponents(url: request.url, resolvingAgainstBaseURL: false) - urlComponents?.queryItems = queryItems - urlRequest.url = urlComponents?.url - } - - urlRequest.httpMethod = request.method.stringValue - urlRequest.allHTTPHeaderFields = request.headers - - if let body = request.body { - urlRequest.httpBody = body - } + let urlRequest = request.buildURLRequest() URLSession.shared.dataTask(with: urlRequest) { data, response, error in if let error = error { diff --git a/Sources/SwiftNetKit/Protocols/RequestProtocol.swift b/Sources/SwiftNetKit/Protocols/RequestProtocol.swift index ea76d47..1b3b63a 100644 --- a/Sources/SwiftNetKit/Protocols/RequestProtocol.swift +++ b/Sources/SwiftNetKit/Protocols/RequestProtocol.swift @@ -7,27 +7,12 @@ import Foundation -enum MethodType { - case get - case post - case delete - case put - case patch - - var stringValue: String { - switch self { - case .get: - return "GET" - case .post: - return "POST" - case .delete: - return "DELETE" - case .put: - return "PUT" - case .patch: - return "PATCH" - } - } +enum MethodType: String { + case get = "GET" + case post = "POST" + case delete = "DELETE" + case put = "PUT" + case patch = "PATCH" } protocol RequestProtocol { @@ -38,4 +23,6 @@ protocol RequestProtocol { var parameters: [String: Any]? { get } var headers: [String: String]? { get } var body: Data? { get } + + func buildURLRequest() -> URLRequest } From 4c260b023902a26ac9978d2d6f959ddd9bd8e917 Mon Sep 17 00:00:00 2001 From: samgilmore <30483214+samgilmore@users.noreply.github.com> Date: Wed, 17 Jul 2024 15:51:50 -0400 Subject: [PATCH 3/4] Convert URLSession to stored variable --- Sources/SwiftNetKit/NetworkService.swift | 10 +++++++--- .../SwiftNetKit/Protocols/NetworkServiceProtocol.swift | 4 ++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Sources/SwiftNetKit/NetworkService.swift b/Sources/SwiftNetKit/NetworkService.swift index 70ded88..8f9b38d 100644 --- a/Sources/SwiftNetKit/NetworkService.swift +++ b/Sources/SwiftNetKit/NetworkService.swift @@ -9,13 +9,17 @@ import Foundation public struct NetworkService: NetworkServiceProtocol { - public init() {} + public let session: URLSession + + public init(session: URLSession = .shared) { + self.session = session + } func start(_ request: Request) async throws -> Request.ResponseType { do { let urlRequest = request.buildURLRequest() - let (data, response) = try await URLSession.shared.data(for: urlRequest) + let (data, response) = try await session.data(for: urlRequest) guard let httpResponse = response as? HTTPURLResponse else { throw NetworkError.invalidResponse @@ -39,7 +43,7 @@ public struct NetworkService: NetworkServiceProtocol { func start(_ request: Request, completion: @escaping (Result) -> Void) { let urlRequest = request.buildURLRequest() - URLSession.shared.dataTask(with: urlRequest) { data, response, error in + session.dataTask(with: urlRequest) { data, response, error in if let error = error { completion(.failure(NetworkError.requestFailed(error: error))) return diff --git a/Sources/SwiftNetKit/Protocols/NetworkServiceProtocol.swift b/Sources/SwiftNetKit/Protocols/NetworkServiceProtocol.swift index e2e6d50..0d08a64 100644 --- a/Sources/SwiftNetKit/Protocols/NetworkServiceProtocol.swift +++ b/Sources/SwiftNetKit/Protocols/NetworkServiceProtocol.swift @@ -5,7 +5,11 @@ // Created by Sam Gilmore on 7/16/24. // +import Foundation + protocol NetworkServiceProtocol { + var session: URLSession { get } + // Async/Await func start(_ request: Request) async throws -> Request.ResponseType From b9a5d359e6dfa6544919aa25e166de44b3c6691a Mon Sep 17 00:00:00 2001 From: samgilmore <30483214+samgilmore@users.noreply.github.com> Date: Wed, 17 Jul 2024 16:14:42 -0400 Subject: [PATCH 4/4] Abstract configuration into enum --- Sources/SwiftNetKit/Models/MethodType.swift | 14 ++++++++++++++ Sources/SwiftNetKit/Models/NetworkError.swift | 2 -- .../SwiftNetKit/Models/SessionConfiguration.swift | 12 ++++++++++++ Sources/SwiftNetKit/NetworkService.swift | 13 ++++++++++--- .../SwiftNetKit/Protocols/RequestProtocol.swift | 8 -------- 5 files changed, 36 insertions(+), 13 deletions(-) create mode 100644 Sources/SwiftNetKit/Models/MethodType.swift create mode 100644 Sources/SwiftNetKit/Models/SessionConfiguration.swift diff --git a/Sources/SwiftNetKit/Models/MethodType.swift b/Sources/SwiftNetKit/Models/MethodType.swift new file mode 100644 index 0000000..9ae3ccf --- /dev/null +++ b/Sources/SwiftNetKit/Models/MethodType.swift @@ -0,0 +1,14 @@ +// +// MethodType.swift +// +// +// Created by Sam Gilmore on 7/17/24. +// + +enum MethodType: String { + case get = "GET" + case post = "POST" + case delete = "DELETE" + case put = "PUT" + case patch = "PATCH" +} diff --git a/Sources/SwiftNetKit/Models/NetworkError.swift b/Sources/SwiftNetKit/Models/NetworkError.swift index 264ddad..c692e0d 100644 --- a/Sources/SwiftNetKit/Models/NetworkError.swift +++ b/Sources/SwiftNetKit/Models/NetworkError.swift @@ -5,8 +5,6 @@ // Created by Sam Gilmore on 7/16/24. // -import Foundation - public enum NetworkError: Error { case invalidResponse case decodingFailed diff --git a/Sources/SwiftNetKit/Models/SessionConfiguration.swift b/Sources/SwiftNetKit/Models/SessionConfiguration.swift new file mode 100644 index 0000000..91a3398 --- /dev/null +++ b/Sources/SwiftNetKit/Models/SessionConfiguration.swift @@ -0,0 +1,12 @@ +// +// SessionConfiguration.swift +// +// +// Created by Sam Gilmore on 7/17/24. +// + +public enum SessionConfiguration { + case `default` + case ephemeral + case background(String) +} diff --git a/Sources/SwiftNetKit/NetworkService.swift b/Sources/SwiftNetKit/NetworkService.swift index 8f9b38d..eff9c97 100644 --- a/Sources/SwiftNetKit/NetworkService.swift +++ b/Sources/SwiftNetKit/NetworkService.swift @@ -9,10 +9,17 @@ import Foundation public struct NetworkService: NetworkServiceProtocol { - public let session: URLSession + internal let session: URLSession - public init(session: URLSession = .shared) { - self.session = session + public init(configuration: SessionConfiguration = .default) { + switch configuration { + case .default: + self.session = URLSession(configuration: .default) + case .ephemeral: + self.session = URLSession(configuration: .ephemeral) + case .background(let identifier): + self.session = URLSession(configuration: .background(withIdentifier: identifier)) + } } func start(_ request: Request) async throws -> Request.ResponseType { diff --git a/Sources/SwiftNetKit/Protocols/RequestProtocol.swift b/Sources/SwiftNetKit/Protocols/RequestProtocol.swift index 1b3b63a..cee694e 100644 --- a/Sources/SwiftNetKit/Protocols/RequestProtocol.swift +++ b/Sources/SwiftNetKit/Protocols/RequestProtocol.swift @@ -7,14 +7,6 @@ import Foundation -enum MethodType: String { - case get = "GET" - case post = "POST" - case delete = "DELETE" - case put = "PUT" - case patch = "PATCH" -} - protocol RequestProtocol { associatedtype ResponseType: Decodable