[Feature] Client #3
14
Package.resolved
Normal file
14
Package.resolved
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "swift-libs",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/rock-n-code/swift-libs.git",
|
||||
"state" : {
|
||||
"revision" : "2ba3e33a0e8c0ffc2b69efa906b03364a9d53a51",
|
||||
"version" : "0.1.2"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
@ -32,7 +32,8 @@ let package = Package(
|
||||
.testTarget(
|
||||
name: "AmiiboServiceTests",
|
||||
dependencies: [
|
||||
"AmiiboService"
|
||||
"AmiiboService",
|
||||
.product(name: "SwiftLibs", package: "swift-libs")
|
||||
],
|
||||
path: "Tests"
|
||||
),
|
||||
|
66
Sources/Clients/AmiiboClient.swift
Normal file
66
Sources/Clients/AmiiboClient.swift
Normal file
@ -0,0 +1,66 @@
|
||||
import Communications
|
||||
import Foundation
|
||||
|
||||
struct AmiiboClient {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
private let session: URLSession
|
||||
|
||||
private let decoder = JSONDecoder()
|
||||
private let makeURLRequest = MakeURLRequestUseCase()
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
init(configuration: URLSessionConfiguration) {
|
||||
session = .init(configuration: configuration)
|
||||
}
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func setDateDecodingStrategy(_ dateDecodingStrategy: JSONDecoder.DateDecodingStrategy) {
|
||||
decoder.dateDecodingStrategy = dateDecodingStrategy
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Client
|
||||
|
||||
extension AmiiboClient: Client {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func request<Model>(
|
||||
endpoint: some Endpoint,
|
||||
as model: Model.Type
|
||||
) async throws -> Model where Model : Decodable {
|
||||
let urlRequest = try makeURLRequest(endpoint: endpoint)
|
||||
let (data, response) = try await session.data(for: urlRequest)
|
||||
|
||||
try check(response)
|
||||
|
||||
return try decoder.decode(model, from: data)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension AmiiboClient {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func check(_ response: URLResponse) throws {
|
||||
guard
|
||||
let urlResponse = response as? HTTPURLResponse,
|
||||
let responseCode = HTTPResponseCode(rawValue: urlResponse.statusCode)
|
||||
else {
|
||||
throw AmiiboClientError.responseCodeNotFound
|
||||
}
|
||||
|
||||
guard responseCode == .ok else {
|
||||
throw AmiiboClientError.responseCode(responseCode.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -7,10 +7,17 @@ struct GetAmiiboEndpoint: Endpoint {
|
||||
|
||||
let scheme: String = .Scheme.https
|
||||
let host: String = .Host.amiiboApi
|
||||
let port: Int?
|
||||
let port: Int? = nil
|
||||
let path: String = .Path.type
|
||||
let parameters: Parameters
|
||||
let method: HTTPRequestMethod = .get
|
||||
let headers: [String : String] = [:]
|
||||
let body: Data?
|
||||
let body: Data? = nil
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
init(parameters: Parameters) {
|
||||
self.parameters = parameters
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,10 +7,17 @@ struct GetCharacterEndpoint: Endpoint {
|
||||
|
||||
let scheme: String = .Scheme.https
|
||||
let host: String = .Host.amiiboApi
|
||||
let port: Int?
|
||||
let port: Int? = nil
|
||||
let path: String = .Path.character
|
||||
let parameters: Parameters
|
||||
let method: HTTPRequestMethod = .get
|
||||
let headers: [String : String] = [:]
|
||||
let body: Data?
|
||||
let body: Data? = nil
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
init(parameters: Parameters) {
|
||||
self.parameters = parameters
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,10 +7,17 @@ struct GetGameSeriesEndpoint: Endpoint {
|
||||
|
||||
let scheme: String = .Scheme.https
|
||||
let host: String = .Host.amiiboApi
|
||||
let port: Int?
|
||||
let port: Int? = nil
|
||||
let path: String = .Path.gameSeries
|
||||
let parameters: Parameters
|
||||
let method: HTTPRequestMethod = .get
|
||||
let headers: [String : String] = [:]
|
||||
let body: Data?
|
||||
let body: Data? = nil
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
init(parameters: Parameters) {
|
||||
self.parameters = parameters
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,10 +7,11 @@ struct GetLastUpdatedEndpoint: Endpoint {
|
||||
|
||||
let scheme: String = .Scheme.https
|
||||
let host: String = .Host.amiiboApi
|
||||
let port: Int?
|
||||
let port: Int? = nil
|
||||
let path: String = .Path.lastUpdated
|
||||
let parameters: Parameters = [:]
|
||||
let method: HTTPRequestMethod = .get
|
||||
let headers: [String : String] = [:]
|
||||
let body: Data?
|
||||
let body: Data? = nil
|
||||
|
||||
}
|
||||
|
@ -7,10 +7,17 @@ struct GetSeriesEndpoint: Endpoint {
|
||||
|
||||
let scheme: String = .Scheme.https
|
||||
let host: String = .Host.amiiboApi
|
||||
let port: Int?
|
||||
let port: Int? = nil
|
||||
let path: String = .Path.series
|
||||
let parameters: Parameters
|
||||
let method: HTTPRequestMethod = .get
|
||||
let headers: [String : String] = [:]
|
||||
let body: Data?
|
||||
let body: Data? = nil
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
init(parameters: Parameters) {
|
||||
self.parameters = parameters
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,10 +7,17 @@ struct GetTypeEndpoint: Endpoint {
|
||||
|
||||
let scheme: String = .Scheme.https
|
||||
let host: String = .Host.amiiboApi
|
||||
let port: Int?
|
||||
let port: Int? = nil
|
||||
let path: String = .Path.type
|
||||
let parameters: Parameters
|
||||
let method: HTTPRequestMethod = .get
|
||||
let headers: [String : String] = [:]
|
||||
let body: Data?
|
||||
let body: Data? = nil
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
init(parameters: Parameters) {
|
||||
self.parameters = parameters
|
||||
}
|
||||
|
||||
}
|
||||
|
4
Sources/Errors/AmiiboClientError.swift
Normal file
4
Sources/Errors/AmiiboClientError.swift
Normal file
@ -0,0 +1,4 @@
|
||||
public enum AmiiboClientError: Error {
|
||||
case responseCode(Int)
|
||||
case responseCodeNotFound
|
||||
}
|
35
Sources/Extensions/DateFormatter+Formatter.swift
Normal file
35
Sources/Extensions/DateFormatter+Formatter.swift
Normal file
@ -0,0 +1,35 @@
|
||||
import Core
|
||||
import Foundation
|
||||
|
||||
extension DateFormatter {
|
||||
|
||||
// MARK: Formatters
|
||||
|
||||
static let dateOnly = {
|
||||
let formatter = DateFormatter()
|
||||
|
||||
formatter.timeZone = .gmt
|
||||
formatter.dateFormat = .Format.yearMonthDay
|
||||
|
||||
return formatter
|
||||
}()
|
||||
|
||||
static let dateAndTime = {
|
||||
let formatter = DateFormatter()
|
||||
|
||||
formatter.timeZone = .gmt
|
||||
formatter.dateFormat = .Format.dateAndTimeWithMicroseconds
|
||||
|
||||
return formatter
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
// MARK: - String+Format
|
||||
|
||||
private extension String {
|
||||
enum Format {
|
||||
static let yearMonthDay = "yyyy-MM-dd"
|
||||
static let dateAndTimeWithMicroseconds = "yyyy-MM-dd'T'HH:mm:ss.SSS"
|
||||
}
|
||||
}
|
145
Tests/Clients/AmiiboClientTests.swift
Normal file
145
Tests/Clients/AmiiboClientTests.swift
Normal file
@ -0,0 +1,145 @@
|
||||
import Communications
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import AmiiboService
|
||||
|
||||
final class AmiiboClientTests: XCTestCase {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
private let configuration: URLSessionConfiguration = {
|
||||
let configuration = URLSessionConfiguration.ephemeral
|
||||
|
||||
configuration.protocolClasses = [MockURLProtocol.self]
|
||||
|
||||
return configuration
|
||||
}()
|
||||
private let makeURLRequest = MakeURLRequestUseCase()
|
||||
private let endpoint = TestEndpoint()
|
||||
|
||||
private var client: AmiiboClient!
|
||||
|
||||
// MARK: Setup
|
||||
|
||||
override func setUp() async throws {
|
||||
client = .init(configuration: configuration)
|
||||
}
|
||||
|
||||
override func tearDown() async throws {
|
||||
client = nil
|
||||
}
|
||||
|
||||
// MARK: Tests
|
||||
|
||||
func test_request_withEndpointAndModel_whenDataDoesMatchModel() async throws {
|
||||
// GIVEN
|
||||
let url = try XCTUnwrap(try makeURLRequest(endpoint: endpoint).url)
|
||||
|
||||
MockURLProtocol.mockData[.init(url: url)] = .init(
|
||||
status: .ok,
|
||||
data: .Seed.dataWithoutTimestamp
|
||||
)
|
||||
|
||||
// WHEN
|
||||
let model = try await client.request(
|
||||
endpoint: endpoint,
|
||||
as: TestModel.self
|
||||
)
|
||||
|
||||
// THEN
|
||||
XCTAssertNotNil(model)
|
||||
XCTAssertNil(model.timestamp)
|
||||
}
|
||||
|
||||
func test_request_withEndpointAndModel_whenDataDoesMatchModel_andDateDecodingStrategy() async throws {
|
||||
// GIVEN
|
||||
let url = try XCTUnwrap(try makeURLRequest(endpoint: endpoint).url)
|
||||
|
||||
MockURLProtocol.mockData[.init(url: url)] = .init(
|
||||
status: .ok,
|
||||
data: .Seed.dataWithDateAndTime
|
||||
)
|
||||
|
||||
client.setDateDecodingStrategy(.formatted(.dateAndTime))
|
||||
|
||||
// WHEN
|
||||
let model = try await client.request(
|
||||
endpoint: endpoint,
|
||||
as: TestModel.self
|
||||
)
|
||||
|
||||
// THEN
|
||||
XCTAssertNotNil(model)
|
||||
XCTAssertNotNil(model.timestamp)
|
||||
}
|
||||
|
||||
func test_request_withEndpointAndModel_whenDataDoesNotMatchModel() async throws {
|
||||
// GIVEN
|
||||
let url = try XCTUnwrap(try makeURLRequest(endpoint: endpoint).url)
|
||||
|
||||
MockURLProtocol.mockData[.init(url: url)] = .init(
|
||||
status: .ok,
|
||||
data: .Seed.dataUnrelated
|
||||
)
|
||||
|
||||
// WHEN & THEN
|
||||
do {
|
||||
let _ = try await client.request(
|
||||
endpoint: endpoint,
|
||||
as: TestModel.self
|
||||
)
|
||||
} catch is DecodingError {
|
||||
XCTAssertTrue(true)
|
||||
} catch {
|
||||
XCTAssertTrue(false)
|
||||
}
|
||||
}
|
||||
|
||||
func test_request_withEndpointAndModel_whenDateDecodingStrategyNotCorrectlySet() async throws {
|
||||
// GIVEN
|
||||
let url = try XCTUnwrap(try makeURLRequest(endpoint: endpoint).url)
|
||||
|
||||
MockURLProtocol.mockData[.init(url: url)] = .init(
|
||||
status: .ok,
|
||||
data: .Seed.dataWithDateAndTime
|
||||
)
|
||||
|
||||
client.setDateDecodingStrategy(.formatted(.dateOnly))
|
||||
|
||||
// WHEN & THEN
|
||||
do {
|
||||
let _ = try await client.request(
|
||||
endpoint: endpoint,
|
||||
as: TestModel.self
|
||||
)
|
||||
} catch is DecodingError {
|
||||
XCTAssertTrue(true)
|
||||
} catch {
|
||||
XCTAssertTrue(false)
|
||||
}
|
||||
}
|
||||
|
||||
func test_request_withEndpointAndModel_whenResponseCodeIsNotOK() async throws {
|
||||
// GIVEN
|
||||
let url = try XCTUnwrap(try makeURLRequest(endpoint: endpoint).url)
|
||||
|
||||
MockURLProtocol.mockData[.init(url: url)] = .init(
|
||||
status: .notFound,
|
||||
data: .Seed.dataWithoutTimestamp
|
||||
)
|
||||
|
||||
// WHEN & THEN
|
||||
do {
|
||||
let _ = try await client.request(
|
||||
endpoint: endpoint,
|
||||
as: TestModel.self
|
||||
)
|
||||
} catch is AmiiboClientError {
|
||||
XCTAssertTrue(true)
|
||||
} catch {
|
||||
XCTAssertTrue(false)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
10
Tests/Helpers/Data+Seed.swift
Normal file
10
Tests/Helpers/Data+Seed.swift
Normal file
@ -0,0 +1,10 @@
|
||||
import Foundation
|
||||
|
||||
extension Data {
|
||||
enum Seed {
|
||||
static let dataUnrelated = "{\"something\":\"Something goes in here...\"}".data(using: .utf8)
|
||||
static let dataWithoutTimestamp = "{\"timestamp\":null}".data(using: .utf8)
|
||||
static let dataWithDateOnly = "{\"timestamp\":\"2023-03-23\"}".data(using: .utf8)
|
||||
static let dataWithDateAndTime = "{\"timestamp\":\"2023-03-23T13:11:20.382254\"}".data(using: .utf8)
|
||||
}
|
||||
}
|
12
Tests/Helpers/MockURLRequest+Init.swift
Normal file
12
Tests/Helpers/MockURLRequest+Init.swift
Normal file
@ -0,0 +1,12 @@
|
||||
import Communications
|
||||
import Foundation
|
||||
|
||||
extension MockURLRequest {
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
init(url: URL) {
|
||||
self.init(method: .get, url: url)
|
||||
}
|
||||
|
||||
}
|
17
Tests/Helpers/TestEndpoint.swift
Normal file
17
Tests/Helpers/TestEndpoint.swift
Normal file
@ -0,0 +1,17 @@
|
||||
import Communications
|
||||
import Foundation
|
||||
|
||||
struct TestEndpoint: Endpoint {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
let scheme: String = "http"
|
||||
let host: String = "www.something.com"
|
||||
let port: Int? = nil
|
||||
let path: String = "/path/to/endpoint"
|
||||
let parameters: Parameters = [:]
|
||||
let method: HTTPRequestMethod = .get
|
||||
let headers: [String : String] = [:]
|
||||
let body: Data? = nil
|
||||
|
||||
}
|
5
Tests/Helpers/TestModel.swift
Normal file
5
Tests/Helpers/TestModel.swift
Normal file
@ -0,0 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
struct TestModel: Decodable {
|
||||
let timestamp: Date?
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user