From 85ce05faf25dbad843009f3ba99c19a89f110105 Mon Sep 17 00:00:00 2001 From: Victor Bodinaud Date: Tue, 28 May 2024 17:40:16 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20global=20configuration,=20ref?= =?UTF-8?q?acto=20&=20updated=20testing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/Hermes/Hermes.swift | 73 +++++++++++++----------- Sources/Hermes/HermesConfiguration.swift | 18 ++++++ Tests/HermesTests/HermesTests.swift | 52 ++++++++++------- 3 files changed, 91 insertions(+), 52 deletions(-) create mode 100644 Sources/Hermes/HermesConfiguration.swift diff --git a/Sources/Hermes/Hermes.swift b/Sources/Hermes/Hermes.swift index 5844b44..2f2832b 100644 --- a/Sources/Hermes/Hermes.swift +++ b/Sources/Hermes/Hermes.swift @@ -66,74 +66,83 @@ public struct Resource { } } -public struct Hermes { +public final class Hermes { + public static let shared = Hermes() + private var config: HermesConfiguration - public init() { } + private init(config: HermesConfiguration = HermesConfiguration()) { + self.config = config + } + + public func configure(_ config: HermesConfiguration) { + self.config = config + } private var defaultHeaders: [String: String] { var headers = ["Content-Type": "application/json"] - let defaults = UserDefaults.standard - guard let token = defaults.string(forKey: "authToken") else { - return headers + if let token = UserDefaults.standard.string(forKey: "authToken") { + headers["Authorization"] = "Bearer \(token)" } - - headers["Authorization"] = "Bearer \(token)" - + headers.merge(config.defaultHeaders) { (_, new) in new } return headers } public func load(_ resource: Resource) async throws -> T { var request = URLRequest(url: resource.url) + request.httpMethod = resource.method.name switch resource.method { - case .get(let queryItems): + case .get(let queryItems): + if let queryItems = queryItems { var components = URLComponents(url: resource.url, resolvingAgainstBaseURL: false) components?.queryItems = queryItems - guard let url = components?.url else { - throw NetworkError.badRequest + request.url = components?.url } - - request = URLRequest(url: url) - case .post(let data), .put(let data), .patch(let data): - request.httpMethod = resource.method.name request.httpBody = data - case .delete: - request.httpMethod = resource.method.name + break } + request.allHTTPHeaderFields = defaultHeaders + let configuration = URLSessionConfiguration.default configuration.httpAdditionalHeaders = defaultHeaders let session = URLSession(configuration: configuration) let (data, response) = try await session.data(for: request) - guard let response = response as? HTTPURLResponse else { + guard let httpResponse = response as? HTTPURLResponse else { throw NetworkError.invalidResponse } - if response.statusCode != 200 { - switch response.statusCode { - case 404: - throw NetworkError.notFound - case 500: - throw NetworkError.serverError(response.description) - default: - throw NetworkError.invalidResponse - } - } + try validate(response: httpResponse) let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase + decoder.keyDecodingStrategy = config.decodingStrategy - guard let result = try? decoder.decode(resource.modelType, from: data) else { + do { + return try decoder.decode(resource.modelType, from: data) + } catch { throw NetworkError.decodingError } - - return result + } + + private func validate(response: HTTPURLResponse) throws { + switch response.statusCode { + case 200...299: + return + case 400: + throw NetworkError.badRequest + case 404: + throw NetworkError.notFound + case 500: + throw NetworkError.serverError("Internal Server Error") + default: + throw NetworkError.invalidResponse + } } } diff --git a/Sources/Hermes/HermesConfiguration.swift b/Sources/Hermes/HermesConfiguration.swift new file mode 100644 index 0000000..7e74159 --- /dev/null +++ b/Sources/Hermes/HermesConfiguration.swift @@ -0,0 +1,18 @@ +// +// HermesConfiguration.swift +// +// +// Created by Victor on 28/05/2024. +// + +import Foundation + +public struct HermesConfiguration { + var defaultHeaders: [String: String] = [:] + var decodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys + + public init(defaultHeaders: [String: String] = [:], decodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys) { + self.defaultHeaders = defaultHeaders + self.decodingStrategy = decodingStrategy + } +} diff --git a/Tests/HermesTests/HermesTests.swift b/Tests/HermesTests/HermesTests.swift index 8dbd80c..c304585 100644 --- a/Tests/HermesTests/HermesTests.swift +++ b/Tests/HermesTests/HermesTests.swift @@ -1,14 +1,24 @@ -import XCTest @testable import Hermes +import XCTest final class HermesTests: XCTestCase { + override func setUp() { + super.setUp() + configureHermes() + } + + private func configureHermes() { + let defaultHeaders = ["Custom-Header": "HeaderValue"] + let decodingStrategy = JSONDecoder.KeyDecodingStrategy.convertFromSnakeCase + let config = HermesConfiguration(defaultHeaders: defaultHeaders, decodingStrategy: decodingStrategy) + Hermes.shared.configure(config) + } func testGetRequest() async throws { - let hermes = Hermes() let queryParams = [URLQueryItem(name: "foo", value: "bar")] let resource = Resource(url: URL(string: "https://httpbin.org/get")!, method: .get(queryParams), modelType: GetResponse.self) - let getResponse = try await hermes.load(resource) + let getResponse = try await Hermes.shared.load(resource) XCTAssertEqual(getResponse.url, "https://httpbin.org/get?foo=bar") XCTAssertEqual(getResponse.args.count, 1) @@ -16,52 +26,48 @@ final class HermesTests: XCTestCase { } func testPostRequest() async throws { - let hermes = Hermes() let postDatas = try? JSONEncoder().encode(["foo": "bar"]) let resource = Resource(url: URL(string: "https://httpbin.org/post")!, method: .post(postDatas), modelType: PostResponse.self) - let postResponse = try await hermes.load(resource) + let postResponse = try await Hermes.shared.load(resource) XCTAssertEqual(postResponse.url, "https://httpbin.org/post") XCTAssertEqual(postResponse.args.count, 0) XCTAssertNotNil(postResponse.json) - XCTAssertEqual(postResponse.json!.count, 1) + XCTAssertEqual(postResponse.json?.count, 1) XCTAssertGreaterThan(postResponse.headers.count, 0) } func testPutRequest() async throws { - let hermes = Hermes() let postDatas = try? JSONEncoder().encode(["foo": "bar"]) let resource = Resource(url: URL(string: "https://httpbin.org/put")!, method: .put(postDatas), modelType: PostResponse.self) - let putResponse = try await hermes.load(resource) + let putResponse = try await Hermes.shared.load(resource) XCTAssertEqual(putResponse.url, "https://httpbin.org/put") XCTAssertEqual(putResponse.args.count, 0) XCTAssertNotNil(putResponse.json) - XCTAssertEqual(putResponse.json!.count, 1) + XCTAssertEqual(putResponse.json?.count, 1) XCTAssertGreaterThan(putResponse.headers.count, 0) } func testPatchRequest() async throws { - let hermes = Hermes() let postDatas = try? JSONEncoder().encode(["foo": "bar"]) let resource = Resource(url: URL(string: "https://httpbin.org/patch")!, method: .patch(postDatas), modelType: PostResponse.self) - let patchResponse = try await hermes.load(resource) + let patchResponse = try await Hermes.shared.load(resource) XCTAssertEqual(patchResponse.url, "https://httpbin.org/patch") XCTAssertEqual(patchResponse.args.count, 0) XCTAssertNotNil(patchResponse.json) - XCTAssertEqual(patchResponse.json!.count, 1) + XCTAssertEqual(patchResponse.json?.count, 1) XCTAssertGreaterThan(patchResponse.headers.count, 0) } func testDeleteRequest() async throws { - let hermes = Hermes() let resource = Resource(url: URL(string: "https://httpbin.org/delete")!, method: .delete, modelType: PostResponse.self) - let deleteResponse = try await hermes.load(resource) + let deleteResponse = try await Hermes.shared.load(resource) XCTAssertEqual(deleteResponse.url, "https://httpbin.org/delete") XCTAssertEqual(deleteResponse.args.count, 0) @@ -70,35 +76,41 @@ final class HermesTests: XCTestCase { } func testNotFound() async throws { - let hermes = Hermes() let resource = Resource(url: URL(string: "https://httpbin.org/status/404")!, method: .get(), modelType: GetResponse.self) do { - let _ = try await hermes.load(resource) + let _ = try await Hermes.shared.load(resource) + XCTFail("Expected to throw NetworkError.notFound") } catch NetworkError.notFound { XCTAssert(true) + } catch { + XCTFail("Unexpected error: \(error)") } } func testServerError() async throws { - let hermes = Hermes() let resource = Resource(url: URL(string: "https://httpbin.org/status/500")!, method: .get(), modelType: GetResponse.self) do { - let _ = try await hermes.load(resource) + let _ = try await Hermes.shared.load(resource) + XCTFail("Expected to throw NetworkError.serverError") } catch NetworkError.serverError(_) { XCTAssert(true) + } catch { + XCTFail("Unexpected error: \(error)") } } func testDecodeError() async throws { - let hermes = Hermes() let resource = Resource(url: URL(string: "https://httpbin.org/get")!, method: .get(), modelType: DummyResponse.self) do { - let _ = try await hermes.load(resource) + let _ = try await Hermes.shared.load(resource) + XCTFail("Expected to throw NetworkError.decodingError") } catch NetworkError.decodingError { XCTAssert(true) + } catch { + XCTFail("Unexpected error: \(error)") } } } -- 2.49.1