[Feature] Client #3

Merged
javier merged 7 commits from feature/client into main 2023-04-19 14:33:49 +00:00
16 changed files with 358 additions and 13 deletions

14
Package.resolved Normal file
View 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
}

View File

@ -32,7 +32,8 @@ let package = Package(
.testTarget( .testTarget(
name: "AmiiboServiceTests", name: "AmiiboServiceTests",
dependencies: [ dependencies: [
"AmiiboService" "AmiiboService",
.product(name: "SwiftLibs", package: "swift-libs")
], ],
path: "Tests" path: "Tests"
), ),

View 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)
}
}
}

View File

@ -7,10 +7,17 @@ struct GetAmiiboEndpoint: Endpoint {
let scheme: String = .Scheme.https let scheme: String = .Scheme.https
let host: String = .Host.amiiboApi let host: String = .Host.amiiboApi
let port: Int? let port: Int? = nil
let path: String = .Path.type let path: String = .Path.type
let parameters: Parameters
let method: HTTPRequestMethod = .get let method: HTTPRequestMethod = .get
let headers: [String : String] = [:] let headers: [String : String] = [:]
let body: Data? let body: Data? = nil
// MARK: Initialisers
init(parameters: Parameters) {
self.parameters = parameters
}
} }

View File

@ -7,10 +7,17 @@ struct GetCharacterEndpoint: Endpoint {
let scheme: String = .Scheme.https let scheme: String = .Scheme.https
let host: String = .Host.amiiboApi let host: String = .Host.amiiboApi
let port: Int? let port: Int? = nil
let path: String = .Path.character let path: String = .Path.character
let parameters: Parameters
let method: HTTPRequestMethod = .get let method: HTTPRequestMethod = .get
let headers: [String : String] = [:] let headers: [String : String] = [:]
let body: Data? let body: Data? = nil
// MARK: Initialisers
init(parameters: Parameters) {
self.parameters = parameters
}
} }

View File

@ -7,10 +7,17 @@ struct GetGameSeriesEndpoint: Endpoint {
let scheme: String = .Scheme.https let scheme: String = .Scheme.https
let host: String = .Host.amiiboApi let host: String = .Host.amiiboApi
let port: Int? let port: Int? = nil
let path: String = .Path.gameSeries let path: String = .Path.gameSeries
let parameters: Parameters
let method: HTTPRequestMethod = .get let method: HTTPRequestMethod = .get
let headers: [String : String] = [:] let headers: [String : String] = [:]
let body: Data? let body: Data? = nil
// MARK: Initialisers
init(parameters: Parameters) {
self.parameters = parameters
}
} }

View File

@ -7,10 +7,11 @@ struct GetLastUpdatedEndpoint: Endpoint {
let scheme: String = .Scheme.https let scheme: String = .Scheme.https
let host: String = .Host.amiiboApi let host: String = .Host.amiiboApi
let port: Int? let port: Int? = nil
let path: String = .Path.lastUpdated let path: String = .Path.lastUpdated
let parameters: Parameters = [:]
let method: HTTPRequestMethod = .get let method: HTTPRequestMethod = .get
let headers: [String : String] = [:] let headers: [String : String] = [:]
let body: Data? let body: Data? = nil
} }

View File

@ -7,10 +7,17 @@ struct GetSeriesEndpoint: Endpoint {
let scheme: String = .Scheme.https let scheme: String = .Scheme.https
let host: String = .Host.amiiboApi let host: String = .Host.amiiboApi
let port: Int? let port: Int? = nil
let path: String = .Path.series let path: String = .Path.series
let parameters: Parameters
let method: HTTPRequestMethod = .get let method: HTTPRequestMethod = .get
let headers: [String : String] = [:] let headers: [String : String] = [:]
let body: Data? let body: Data? = nil
// MARK: Initialisers
init(parameters: Parameters) {
self.parameters = parameters
}
} }

View File

@ -7,10 +7,17 @@ struct GetTypeEndpoint: Endpoint {
let scheme: String = .Scheme.https let scheme: String = .Scheme.https
let host: String = .Host.amiiboApi let host: String = .Host.amiiboApi
let port: Int? let port: Int? = nil
let path: String = .Path.type let path: String = .Path.type
let parameters: Parameters
let method: HTTPRequestMethod = .get let method: HTTPRequestMethod = .get
let headers: [String : String] = [:] let headers: [String : String] = [:]
let body: Data? let body: Data? = nil
// MARK: Initialisers
init(parameters: Parameters) {
self.parameters = parameters
}
} }

View File

@ -0,0 +1,4 @@
public enum AmiiboClientError: Error {
case responseCode(Int)
case responseCodeNotFound
}

View 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"
}
}

View 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)
}
}
}

View 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)
}
}

View File

@ -0,0 +1,12 @@
import Communications
import Foundation
extension MockURLRequest {
// MARK: Initialisers
init(url: URL) {
self.init(method: .get, url: url)
}
}

View 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
}

View File

@ -0,0 +1,5 @@
import Foundation
struct TestModel: Decodable {
let timestamp: Date?
}