Skip to content

Commit

Permalink
Refactor data provider service to use new request coordinator protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
jjotaum committed Oct 4, 2022
1 parent 3cff7fc commit e387a41
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 76 deletions.
2 changes: 1 addition & 1 deletion Sources/AmuseKit/AmuseKit.swift
Expand Up @@ -9,7 +9,7 @@ import Foundation

protocol AmuseOption: RawRepresentable, Hashable, CaseIterable {}

public class AmuseKit {
public enum AmuseKit {
enum AmuseError: Error {
case missingDevToken
case missingUserToken
Expand Down
22 changes: 0 additions & 22 deletions Sources/AmuseKit/Networking/APIService.swift

This file was deleted.

16 changes: 8 additions & 8 deletions Sources/AmuseKit/Networking/DataProvider.swift
Expand Up @@ -16,19 +16,19 @@ public extension AmuseKit {
public typealias LibrarySearchTypes = Set<LibraryResourceType>

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
Expand Down Expand Up @@ -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<AmuseKit.SearchResponse, Error> {
Expand All @@ -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())
}


Expand All @@ -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())
}
}
}
62 changes: 62 additions & 0 deletions 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<T> = Result<T, Error>

public protocol RequestDecoder {
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T: Decodable
}

extension JSONDecoder: RequestDecoder { }

public protocol RequestCoordinator {
func dataTask<T: Codable>(request: URLRequest, decoder: RequestDecoder, completion: @escaping (RequestResult<T>) -> Void)
@available(iOS 15.0, *)
@available(macOS 12.0, *)
func data<T: Codable>(request: URLRequest, decoder: RequestDecoder) async throws -> RequestResult<T>
func dataTaskPublisher<T: Codable>(for request: URLRequest, decoder: RequestDecoder) throws -> AnyPublisher<T, Error>
}

extension URLSession: RequestCoordinator {
public func dataTask<T>(request: URLRequest, decoder: RequestDecoder, completion: @escaping (RequestResult<T>) -> 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<T>(request: URLRequest, decoder: RequestDecoder) async throws -> RequestResult<T> 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<T>(for request: URLRequest, decoder: RequestDecoder) throws -> AnyPublisher<T, Error> where T : Decodable, T : Encodable {
return dataTaskPublisher(for: request)
.tryMap { try decoder.decode(T.self, from: $0.data) }
.eraseToAnyPublisher()
}
}
38 changes: 0 additions & 38 deletions Tests/AmuseKitTests/Helpers/MockAPIService.swift

This file was deleted.

6 changes: 3 additions & 3 deletions Tests/AmuseKitTests/Helpers/MockDataProvider.swift
Expand Up @@ -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
}
Expand Down
72 changes: 72 additions & 0 deletions 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<T>(request: URLRequest, decoder: RequestDecoder, completion: @escaping (RequestResult<T>) -> 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<T>(request: URLRequest, decoder: RequestDecoder) async throws -> RequestResult<T> 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<T>(for request: URLRequest, decoder: RequestDecoder) throws -> AnyPublisher<T, Error> 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
}
}

8 changes: 4 additions & 4 deletions Tests/AmuseKitTests/Networking/DataProviderTests.swift
Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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: []))
}

Expand Down Expand Up @@ -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))
}

Expand Down

0 comments on commit e387a41

Please sign in to comment.