From 81e87e4be1d4cd45d30eaaf245b237a77c5ba8ff Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 19 Apr 2023 01:04:40 +0200 Subject: [PATCH 1/7] Added the "parameters" property to the implemented endpoints. --- Sources/Endpoints/GetAmiiboEndpoint.swift | 11 +++++++++-- Sources/Endpoints/GetCharacterEndpoint.swift | 11 +++++++++-- Sources/Endpoints/GetGameSeriesEndpoint.swift | 11 +++++++++-- Sources/Endpoints/GetLastUpdatedEndpoint.swift | 5 +++-- Sources/Endpoints/GetSeriesEndpoint.swift | 11 +++++++++-- Sources/Endpoints/GetTypeEndpoint.swift | 11 +++++++++-- 6 files changed, 48 insertions(+), 12 deletions(-) diff --git a/Sources/Endpoints/GetAmiiboEndpoint.swift b/Sources/Endpoints/GetAmiiboEndpoint.swift index fb3046f..5d2a320 100644 --- a/Sources/Endpoints/GetAmiiboEndpoint.swift +++ b/Sources/Endpoints/GetAmiiboEndpoint.swift @@ -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 + } } diff --git a/Sources/Endpoints/GetCharacterEndpoint.swift b/Sources/Endpoints/GetCharacterEndpoint.swift index 8b08a7b..3646091 100644 --- a/Sources/Endpoints/GetCharacterEndpoint.swift +++ b/Sources/Endpoints/GetCharacterEndpoint.swift @@ -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 + } } diff --git a/Sources/Endpoints/GetGameSeriesEndpoint.swift b/Sources/Endpoints/GetGameSeriesEndpoint.swift index 033ba65..d347d25 100644 --- a/Sources/Endpoints/GetGameSeriesEndpoint.swift +++ b/Sources/Endpoints/GetGameSeriesEndpoint.swift @@ -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 + } + } diff --git a/Sources/Endpoints/GetLastUpdatedEndpoint.swift b/Sources/Endpoints/GetLastUpdatedEndpoint.swift index 1d7d0a2..70fc19f 100644 --- a/Sources/Endpoints/GetLastUpdatedEndpoint.swift +++ b/Sources/Endpoints/GetLastUpdatedEndpoint.swift @@ -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 } diff --git a/Sources/Endpoints/GetSeriesEndpoint.swift b/Sources/Endpoints/GetSeriesEndpoint.swift index f791566..0b6a91d 100644 --- a/Sources/Endpoints/GetSeriesEndpoint.swift +++ b/Sources/Endpoints/GetSeriesEndpoint.swift @@ -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 + } } diff --git a/Sources/Endpoints/GetTypeEndpoint.swift b/Sources/Endpoints/GetTypeEndpoint.swift index 677f0d2..99d4d4a 100644 --- a/Sources/Endpoints/GetTypeEndpoint.swift +++ b/Sources/Endpoints/GetTypeEndpoint.swift @@ -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 + } } -- 2.47.1 From ac9248b21d3fdaa6c7f24f9ffa1315e67fd27553 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 19 Apr 2023 01:37:45 +0200 Subject: [PATCH 2/7] Added the SwiftLibs package as a dependency to the test target in the Package file. --- Package.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 7ed4db2..26063c4 100644 --- a/Package.swift +++ b/Package.swift @@ -32,7 +32,8 @@ let package = Package( .testTarget( name: "AmiiboServiceTests", dependencies: [ - "AmiiboService" + "AmiiboService", + .product(name: "SwiftLibs", package: "swift-libs") ], path: "Tests" ), -- 2.47.1 From 267083726b48e5ac91b96401b3571da54d6eb9d0 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 19 Apr 2023 01:38:31 +0200 Subject: [PATCH 3/7] Implemented the AmiiboClient client and the AmiiboClientError error. --- Sources/Clients/AmiiboClient.swift | 66 ++++++++++++++++++++++++++ Sources/Errors/AmiiboClientError.swift | 4 ++ Tests/Clients/AmiiboClientTests.swift | 42 ++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 Sources/Clients/AmiiboClient.swift create mode 100644 Sources/Errors/AmiiboClientError.swift create mode 100644 Tests/Clients/AmiiboClientTests.swift diff --git a/Sources/Clients/AmiiboClient.swift b/Sources/Clients/AmiiboClient.swift new file mode 100644 index 0000000..170b829 --- /dev/null +++ b/Sources/Clients/AmiiboClient.swift @@ -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( + 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) + } + } + +} diff --git a/Sources/Errors/AmiiboClientError.swift b/Sources/Errors/AmiiboClientError.swift new file mode 100644 index 0000000..6e49f0c --- /dev/null +++ b/Sources/Errors/AmiiboClientError.swift @@ -0,0 +1,4 @@ +public enum AmiiboClientError: Error { + case responseCode(Int) + case responseCodeNotFound +} diff --git a/Tests/Clients/AmiiboClientTests.swift b/Tests/Clients/AmiiboClientTests.swift new file mode 100644 index 0000000..b143b72 --- /dev/null +++ b/Tests/Clients/AmiiboClientTests.swift @@ -0,0 +1,42 @@ +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 var client: AmiiboClient! + private var request: MockURLRequest! + private var response: MockURLResponse! + + // MARK: Setup + + override func setUp() async throws { + client = .init(configuration: configuration) + } + + override func tearDown() async throws { + client = nil + } + + // MARK: Tests + + func test_withSomething() async throws { + // GIVEN + // WHEN + // THEN + XCTFail("Not implemented yet") + } + +} -- 2.47.1 From bfc231b5ec3567f1c886681b9058883e19aed762 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 19 Apr 2023 16:07:52 +0200 Subject: [PATCH 4/7] Implemented the "dateOnly" and "dateAndTime" static formatter in the DateFormatter+Formatter extension. --- .../Extensions/DateFormatter+Formatter.swift | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 Sources/Extensions/DateFormatter+Formatter.swift diff --git a/Sources/Extensions/DateFormatter+Formatter.swift b/Sources/Extensions/DateFormatter+Formatter.swift new file mode 100644 index 0000000..fe71682 --- /dev/null +++ b/Sources/Extensions/DateFormatter+Formatter.swift @@ -0,0 +1,34 @@ +import Foundation + +extension DateFormatter { + + // MARK: Formatters + + static let dateOnly = { + let formatter = DateFormatter() + + formatter.timeZone = .init(secondsFromGMT: 0) + formatter.dateFormat = .Format.yearMonthDay + + return formatter + }() + + static let dateAndTime = { + let formatter = DateFormatter() + + formatter.timeZone = .init(secondsFromGMT: 0) + 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" + } +} -- 2.47.1 From 9a08825ee8c30ea3bd49dd03474c7e67dea416be Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 19 Apr 2023 16:08:51 +0200 Subject: [PATCH 5/7] Implemented the test cases in the AmiiboClientTests tests. --- Tests/Clients/AmiiboClientTests.swift | 111 +++++++++++++++++++++++- Tests/Helpers/Data+Seed.swift | 10 +++ Tests/Helpers/MockURLRequest+Init.swift | 12 +++ Tests/Helpers/TestEndpoint.swift | 17 ++++ Tests/Helpers/TestModel.swift | 5 ++ 5 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 Tests/Helpers/Data+Seed.swift create mode 100644 Tests/Helpers/MockURLRequest+Init.swift create mode 100644 Tests/Helpers/TestEndpoint.swift create mode 100644 Tests/Helpers/TestModel.swift diff --git a/Tests/Clients/AmiiboClientTests.swift b/Tests/Clients/AmiiboClientTests.swift index b143b72..1c20243 100644 --- a/Tests/Clients/AmiiboClientTests.swift +++ b/Tests/Clients/AmiiboClientTests.swift @@ -15,10 +15,10 @@ final class AmiiboClientTests: XCTestCase { return configuration }() + private let makeURLRequest = MakeURLRequestUseCase() + private let endpoint = TestEndpoint() private var client: AmiiboClient! - private var request: MockURLRequest! - private var response: MockURLResponse! // MARK: Setup @@ -32,11 +32,114 @@ final class AmiiboClientTests: XCTestCase { // MARK: Tests - func test_withSomething() async throws { + 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 - XCTFail("Not implemented yet") + 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) + } } } diff --git a/Tests/Helpers/Data+Seed.swift b/Tests/Helpers/Data+Seed.swift new file mode 100644 index 0000000..91b4c80 --- /dev/null +++ b/Tests/Helpers/Data+Seed.swift @@ -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) + } +} diff --git a/Tests/Helpers/MockURLRequest+Init.swift b/Tests/Helpers/MockURLRequest+Init.swift new file mode 100644 index 0000000..25d5abc --- /dev/null +++ b/Tests/Helpers/MockURLRequest+Init.swift @@ -0,0 +1,12 @@ +import Communications +import Foundation + +extension MockURLRequest { + + // MARK: Initialisers + + init(url: URL) { + self.init(method: .get, url: url) + } + +} diff --git a/Tests/Helpers/TestEndpoint.swift b/Tests/Helpers/TestEndpoint.swift new file mode 100644 index 0000000..c3f0755 --- /dev/null +++ b/Tests/Helpers/TestEndpoint.swift @@ -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 + +} diff --git a/Tests/Helpers/TestModel.swift b/Tests/Helpers/TestModel.swift new file mode 100644 index 0000000..c05a0f7 --- /dev/null +++ b/Tests/Helpers/TestModel.swift @@ -0,0 +1,5 @@ +import Foundation + +struct TestModel: Decodable { + let timestamp: Date? +} -- 2.47.1 From 2ce9a1e7f56c5a0d9ffa4053087f75a5d399d404 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 19 Apr 2023 16:24:38 +0200 Subject: [PATCH 6/7] Updated the SwiftLibs dependency to its latest release. --- Package.resolved | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 Package.resolved diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..6df9b48 --- /dev/null +++ b/Package.resolved @@ -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 +} -- 2.47.1 From 2ba3284fea46bffbc387f052e715560ea339509e Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 19 Apr 2023 16:26:18 +0200 Subject: [PATCH 7/7] Updated the static properties implemented the in the DateFormatter+Formatter extension to use the TimeZone extension from the Core library. --- Sources/Extensions/DateFormatter+Formatter.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/Extensions/DateFormatter+Formatter.swift b/Sources/Extensions/DateFormatter+Formatter.swift index fe71682..e209df5 100644 --- a/Sources/Extensions/DateFormatter+Formatter.swift +++ b/Sources/Extensions/DateFormatter+Formatter.swift @@ -1,3 +1,4 @@ +import Core import Foundation extension DateFormatter { @@ -7,7 +8,7 @@ extension DateFormatter { static let dateOnly = { let formatter = DateFormatter() - formatter.timeZone = .init(secondsFromGMT: 0) + formatter.timeZone = .gmt formatter.dateFormat = .Format.yearMonthDay return formatter @@ -16,7 +17,7 @@ extension DateFormatter { static let dateAndTime = { let formatter = DateFormatter() - formatter.timeZone = .init(secondsFromGMT: 0) + formatter.timeZone = .gmt formatter.dateFormat = .Format.dateAndTimeWithMicroseconds return formatter -- 2.47.1