diff --git a/Sources/AmuseKit/AmuseKit.swift b/Sources/AmuseKit/AmuseKit.swift index 036be0c..5ad7c53 100644 --- a/Sources/AmuseKit/AmuseKit.swift +++ b/Sources/AmuseKit/AmuseKit.swift @@ -9,7 +9,7 @@ import Foundation protocol AmuseOption: RawRepresentable, Hashable, CaseIterable {} -public class AmuseKit { +public enum AmuseKit { enum AmuseError: Error { case missingDevToken case missingUserToken diff --git a/Sources/AmuseKit/Networking/APIService.swift b/Sources/AmuseKit/Networking/APIService.swift deleted file mode 100644 index 36a9247..0000000 --- a/Sources/AmuseKit/Networking/APIService.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// APIService.swift -// AmuseKit -// -// Created by Jota Uribe on 9/08/20. -// - -import Foundation -import Combine - -public protocol APIService { - func publisher(with request: URLRequest) throws -> AnyPublisher -} - -extension URLSession: APIService { - public func publisher(with request: URLRequest) throws -> AnyPublisher where T : Decodable, T : Encodable { - return dataTaskPublisher(for: request) - .tryMap { try JSONDecoder().decode(T.self, from: $0.data) } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() - } -} diff --git a/Sources/AmuseKit/Networking/DataProvider.swift b/Sources/AmuseKit/Networking/DataProvider.swift index 34136fb..fff1f0f 100644 --- a/Sources/AmuseKit/Networking/DataProvider.swift +++ b/Sources/AmuseKit/Networking/DataProvider.swift @@ -16,19 +16,19 @@ public extension AmuseKit { public typealias LibrarySearchTypes = Set private var storage: StorageService - private let service: APIService + private let requestCoordinator: RequestCoordinator private var userCountryCode: String = "us" public init(_ storageConfiguration: StorageConfiguration, - service: APIService = URLSession.shared) { + requestCoordinator: RequestCoordinator = URLSession.shared) { self.storage = KeyChainService(storageConfiguration) - self.service = service + self.requestCoordinator = requestCoordinator } init(storage: StorageService, - service: APIService) { + requestCoordinator: RequestCoordinator) { self.storage = storage - self.service = service + self.requestCoordinator = requestCoordinator } // MARK: - Public Accessors Methods @@ -57,7 +57,7 @@ public extension AmuseKit { ) request.setValue("Bearer \(developerToken)", forHTTPHeaderField: "Authorization") request.setValue(storage.userToken, forHTTPHeaderField: "Music-User-Token") - return try service.publisher(with: request) + return try requestCoordinator.dataTaskPublisher(for: request, decoder: JSONDecoder()) } public func catalogSearch(_ resourceTypes: CatalogSearchTypes = .all, limit: Int = 25, searchTerm: String) throws -> AnyPublisher { @@ -74,7 +74,7 @@ public extension AmuseKit { var request = try Router.library(resourceType.rawValue).asURLRequest([]) request.setValue("Bearer \(developerToken)", forHTTPHeaderField: "Authorization") request.setValue(storage.userToken, forHTTPHeaderField: "Music-User-Token") - return try service.publisher(with: request) + return try requestCoordinator.dataTaskPublisher(for: request, decoder: JSONDecoder()) } @@ -96,7 +96,7 @@ public extension AmuseKit { ] var request = try router.asURLRequest(queryItems) request.setValue("Bearer \(developerToken)", forHTTPHeaderField: "Authorization") - return try service.publisher(with: request) + return try requestCoordinator.dataTaskPublisher(for: request, decoder: JSONDecoder()) } } } diff --git a/Sources/AmuseKit/Networking/RequestCoordinator.swift b/Sources/AmuseKit/Networking/RequestCoordinator.swift new file mode 100644 index 0000000..4a68411 --- /dev/null +++ b/Sources/AmuseKit/Networking/RequestCoordinator.swift @@ -0,0 +1,62 @@ +// +// RequestCoordinator.swift +// AmuseKit +// +// Created by Jota Uribe on 30/09/22. +// + +import Combine +import Foundation + +public typealias RequestResult = Result + +public protocol RequestDecoder { + func decode(_ type: T.Type, from data: Data) throws -> T where T: Decodable +} + +extension JSONDecoder: RequestDecoder { } + +public protocol RequestCoordinator { + func dataTask(request: URLRequest, decoder: RequestDecoder, completion: @escaping (RequestResult) -> Void) + @available(iOS 15.0, *) + @available(macOS 12.0, *) + func data(request: URLRequest, decoder: RequestDecoder) async throws -> RequestResult + func dataTaskPublisher(for request: URLRequest, decoder: RequestDecoder) throws -> AnyPublisher +} + +extension URLSession: RequestCoordinator { + public func dataTask(request: URLRequest, decoder: RequestDecoder, completion: @escaping (RequestResult) -> Void) where T : Decodable, T : Encodable { + dataTask(with: request) { data, response, error in + if let error = error { + completion(.failure(error)) + } + + guard let data = data else { + completion(.failure(URLError(.badServerResponse))) + return + } + + do { + let decodedData = try decoder.decode(T.self, from: data) + completion(.success(decodedData)) + } catch { + completion(.failure(error)) + } + }.resume() + } + + @available(iOS 15.0, *) + @available(macOS 12.0, *) + public func data(request: URLRequest, decoder: RequestDecoder) async throws -> RequestResult where T : Decodable, T : Encodable { + let data: (Data, URLResponse) = try await data(for: request) + return Result { + try decoder.decode(T.self, from: data.0) + } + } + + public func dataTaskPublisher(for request: URLRequest, decoder: RequestDecoder) throws -> AnyPublisher where T : Decodable, T : Encodable { + return dataTaskPublisher(for: request) + .tryMap { try decoder.decode(T.self, from: $0.data) } + .eraseToAnyPublisher() + } +} diff --git a/Tests/AmuseKitTests/Helpers/MockAPIService.swift b/Tests/AmuseKitTests/Helpers/MockAPIService.swift deleted file mode 100644 index 5c2b5a1..0000000 --- a/Tests/AmuseKitTests/Helpers/MockAPIService.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// MockAPIService.swift -// AmuseKit -// -// Created by Jota Uribe on 22/03/21. -// - -import Combine -import Foundation -@testable import AmuseKit - -enum MockAPIServiceError: Error { - case invalidFilePath(endpoint: String) - case invalidFileFormat -} - -struct MockAPIService: APIService { - - var resourceName: String = "" - - func publisher(with request: URLRequest) throws -> AnyPublisher where T : Decodable, T : Encodable { - guard let filePath = Bundle.module.path(forResource: resourceName, ofType: "json") else { - throw MockAPIServiceError.invalidFilePath(endpoint: resourceName) - } - - let publisher = filePath.publisher.collect().map { String($0) } - .setFailureType(to: MockAPIServiceError.self) - .tryMap({ path -> T in - guard let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) else { - throw MockAPIServiceError.invalidFileFormat - } - return try JSONDecoder().decode(T.self, from: data) - }) - .eraseToAnyPublisher() - return publisher - } -} - diff --git a/Tests/AmuseKitTests/Helpers/MockDataProvider.swift b/Tests/AmuseKitTests/Helpers/MockDataProvider.swift index 1320baf..b26056d 100644 --- a/Tests/AmuseKitTests/Helpers/MockDataProvider.swift +++ b/Tests/AmuseKitTests/Helpers/MockDataProvider.swift @@ -10,11 +10,11 @@ extension AmuseKit.DataProvider { static func mock(resourceName: String) -> AmuseKit.DataProvider { - var service: MockAPIService = MockAPIService() - service.resourceName = resourceName + var requestCoordinator: MockRequestCoordinator = MockRequestCoordinator() + requestCoordinator.resourceName = resourceName var mock: AmuseKit.DataProvider mock = .init(storage: MockStorageService(), - service: service) + requestCoordinator: requestCoordinator) mock.setDeveloperToken("A1D2E3V4T5O6K7E8N9") return mock } diff --git a/Tests/AmuseKitTests/Helpers/MockRequestCoordinator.swift b/Tests/AmuseKitTests/Helpers/MockRequestCoordinator.swift new file mode 100644 index 0000000..ee18308 --- /dev/null +++ b/Tests/AmuseKitTests/Helpers/MockRequestCoordinator.swift @@ -0,0 +1,72 @@ +// +// MockAPIService.swift +// AmuseKit +// +// Created by Jota Uribe on 22/03/21. +// + +import Combine +import Foundation +@testable import AmuseKit + +enum MockAPIServiceError: Error { + case invalidFilePath(endpoint: String) + case invalidFileFormat +} + +struct MockRequestCoordinator: RequestCoordinator { + var resourceName: String = "" + + func dataTask(request: URLRequest, decoder: RequestDecoder, completion: @escaping (RequestResult) -> Void) where T : Decodable, T : Encodable { + guard let filePath = Bundle.module.path(forResource: resourceName, ofType: "json") else { + completion(.failure(MockAPIServiceError.invalidFilePath(endpoint: resourceName))) + return + } + + guard let data = try? Data(contentsOf: URL(fileURLWithPath: filePath), options: .mappedIfSafe) else { + completion(.failure(MockAPIServiceError.invalidFileFormat)) + return + } + + do { + let response = try decoder.decode(T.self, from: data) + completion(.success(response)) + } catch { + completion(.failure(error)) + } + } + + func data(request: URLRequest, decoder: RequestDecoder) async throws -> RequestResult where T : Decodable, T : Encodable { + guard let filePath = Bundle.module.path(forResource: resourceName, ofType: "json") else { + throw MockAPIServiceError.invalidFilePath(endpoint: resourceName) + } + guard let data = try? Data(contentsOf: URL(fileURLWithPath: filePath), options: .mappedIfSafe) else { + throw MockAPIServiceError.invalidFileFormat + } + + do { + let response = try decoder.decode(T.self, from: data) + return .success(response) + } catch { + return .failure(error) + } + } + + func dataTaskPublisher(for request: URLRequest, decoder: RequestDecoder) throws -> AnyPublisher where T : Decodable, T : Encodable { + guard let filePath = Bundle.module.path(forResource: resourceName, ofType: "json") else { + throw MockAPIServiceError.invalidFilePath(endpoint: resourceName) + } + + let publisher = filePath.publisher.collect().map { String($0) } + .setFailureType(to: MockAPIServiceError.self) + .tryMap({ path -> T in + guard let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) else { + throw MockAPIServiceError.invalidFileFormat + } + return try decoder.decode(T.self, from: data) + }) + .eraseToAnyPublisher() + return publisher + } +} + diff --git a/Tests/AmuseKitTests/Networking/DataProviderTests.swift b/Tests/AmuseKitTests/Networking/DataProviderTests.swift index 9c7d96d..7d311e0 100644 --- a/Tests/AmuseKitTests/Networking/DataProviderTests.swift +++ b/Tests/AmuseKitTests/Networking/DataProviderTests.swift @@ -22,7 +22,7 @@ final class DataProviderTests: XCTestCase { let developerToken = "A1U2S3E4R5T6O7K8E9N0" let mockStorage = MockStorageService() let sut: AmuseKit.DataProvider = .init(storage: mockStorage, - service: MockAPIService()) + requestCoordinator: MockRequestCoordinator()) sut.setDeveloperToken(developerToken) XCTAssertEqual(mockStorage.developerToken, developerToken) } @@ -31,7 +31,7 @@ final class DataProviderTests: XCTestCase { let userToken = "A1U2S3E4R5T6O7K8E9N0" let mockStorage = MockStorageService() let sut: AmuseKit.DataProvider = .init(storage: mockStorage, - service: MockAPIService()) + requestCoordinator: MockRequestCoordinator()) sut.setUserToken(userToken) XCTAssertEqual(mockStorage.userToken, userToken) } @@ -57,7 +57,7 @@ final class DataProviderTests: XCTestCase { func test_catalogRequest_withMissingDeveloperToken_throwsError() throws { let sut: AmuseKit.DataProvider = .init(storage: MockStorageService(), - service: MockAPIService()) + requestCoordinator: MockRequestCoordinator()) XCTAssertThrowsError(try sut.catalog(.albums, ids: [])) } @@ -125,7 +125,7 @@ final class DataProviderTests: XCTestCase { func test_libraryRequest_withMissingDeveloperToken_throwsError() throws { let sut: AmuseKit.DataProvider = .init(storage: MockStorageService(), - service: MockAPIService()) + requestCoordinator: MockRequestCoordinator()) XCTAssertThrowsError(try sut.library(.albums)) }