Implemented the LocationsClient client.

This commit is contained in:
Javier Cicchelli 2023-04-10 16:45:37 +02:00
parent c1a2acb248
commit c3e0d86870
5 changed files with 263 additions and 1 deletions

View File

@ -36,6 +36,7 @@ let package = Package(
.testTarget(
name: "LocationsTests",
dependencies: [
"APICore",
"Locations"
]
),

View File

@ -0,0 +1,15 @@
//
// Client.swift
// APICore
//
// Created by Javier Cicchelli on 10/04/2023.
// Copyright © 2023 Röck+Cöde. All rights reserved.
//
/// This protocol defines a client that will be making the API calls.
public protocol Client {
func request<Model: Decodable>(
endpoint: some Endpoint,
for model: Model.Type
) async throws -> Model
}

View File

@ -0,0 +1,66 @@
//
// LocationsClient.swift
// Locations
//
// Created by Javier Cicchelli on 10/04/2023.
// Copyright © 2023 Röck+Cöde. All rights reserved.
//
import APICore
import Foundation
struct LocationsClient {
// MARK: Properties
private let session: URLSession
private let decoder: JSONDecoder = .init()
private let makeURLRequest: MakeURLRequestUseCase = .init()
// MARK: Initialisers
init(configuration: URLSessionConfiguration = .default) {
self.session = .init(configuration: configuration)
}
}
// MARK: - Client
extension LocationsClient: Client {
// MARK: Functions
func request<Model: Decodable>(
endpoint: some Endpoint,
for model: Model.Type
) async throws -> Model {
let urlRequest = try makeURLRequest(endpoint: endpoint)
let (data, response) = try await session.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse else {
throw LocationsClientError.responseNotReturned
}
switch httpResponse.statusCode {
case 200:
return try decoder.decode(model, from: data)
case 400...499:
throw LocationsClientError.statusErrorClient
case 500...599:
throw LocationsClientError.statusErrorServer
default:
throw LocationsClientError.statusErrorUnexpected
}
}
}
// MARK: - Errors
public enum LocationsClientError: Error {
case responseNotReturned
case statusErrorClient
case statusErrorServer
case statusErrorUnexpected
}

View File

@ -6,10 +6,26 @@
// Copyright © 2023 Röck+Cöde. All rights reserved.
//
public struct Location {
public struct Location: Equatable {
// MARK: Properties
public let name: String?
public let latitude: Float
public let longitude: Float
// MARK: Initialisers
public init(
name: String? = nil,
latitude: Float,
longitude: Float
) {
self.name = name
self.latitude = latitude
self.longitude = longitude
}
}
// MARK: - Decodable

View File

@ -0,0 +1,164 @@
//
// LocationsClientTests.swift
// LocationsTests
//
// Created by Javier Cicchelli on 10/04/2023.
// Copyright © 2023 Röck+Cöde. All rights reserved.
//
import APICore
import XCTest
@testable import Locations
final class LocationsClientTests: XCTestCase {
// MARK: Properties
private let makeURLRequest = MakeURLRequestUseCase()
private let sessionConfiguration = {
let configuration = URLSessionConfiguration.default
configuration.protocolClasses = [MockURLProtocol.self]
return configuration
}()
private var client: LocationsClient!
private var url: URL!
private var data: Data!
// MARK: Setup
override func setUp() async throws {
client = .init(configuration: sessionConfiguration)
}
override func tearDown() async throws {
client = nil
}
// MARK: Tests
func test_request_withGetLocationsEndpoint_forLocations_whenResponseOK() async throws {
// GIVEN
let endpoint = GetLocationsEndpoint()
url = try makeURLRequest(endpoint: endpoint).url
data = .locations
MockURLProtocol.mockData[url] = MockURLResponse(
status: 200,
headers: [:],
data: data
)
// WHEN
let result = try await client.request(endpoint: endpoint, for: Locations.self)
// THEN
XCTAssertEqual(result, Locations(locations: [
.init(
name: "Amsterdam",
latitude: 52.3547498,
longitude: 4.8339215
),
.init(
name: "Mumbai",
latitude: 19.0823998,
longitude: 72.8111468
),
.init(
name: "Copenhagen",
latitude: 55.6713442,
longitude: 12.523785
),
.init(
latitude: 40.4380638,
longitude: -3.7495758
)
]))
}
func test_request_withGetLocationsEndpoint_forLocations_whenResponseClientError() async throws {
// GIVEN
let endpoint = GetLocationsEndpoint()
url = try makeURLRequest(endpoint: endpoint).url
data = .locations
MockURLProtocol.mockData[url] = MockURLResponse(
status: 404,
headers: [:],
data: data
)
// WHEN & THEN
do {
_ = try await client.request(endpoint: endpoint, for: Locations.self)
} catch LocationsClientError.statusErrorClient {
XCTAssertTrue(true)
} catch {
XCTAssertTrue(false)
}
}
func test_request_withGetLocationsEndpoint_forLocations_whenResponseServerError() async throws {
// GIVEN
let endpoint = GetLocationsEndpoint()
url = try makeURLRequest(endpoint: endpoint).url
data = .locations
MockURLProtocol.mockData[url] = MockURLResponse(
status: 500,
headers: [:],
data: data
)
// WHEN & THEN
do {
_ = try await client.request(endpoint: endpoint, for: Locations.self)
} catch LocationsClientError.statusErrorServer {
XCTAssertTrue(true)
} catch {
XCTAssertTrue(false)
}
}
func test_request_withGetLocationsEndpoint_forLocations_whenResponseUnexpectedError() async throws {
// GIVEN
let endpoint = GetLocationsEndpoint()
url = try makeURLRequest(endpoint: endpoint).url
data = .locations
MockURLProtocol.mockData[url] = MockURLResponse(
status: 302,
headers: [:],
data: data
)
// WHEN & THEN
do {
_ = try await client.request(endpoint: endpoint, for: Locations.self)
} catch LocationsClientError.statusErrorUnexpected {
XCTAssertTrue(true)
} catch {
XCTAssertTrue(false)
}
}
}
// MARK: - Models
private struct Locations: Decodable, Equatable {
public let locations: [Location]
}
// MARK: - String+Constants
private extension Data {
static let locations = "{\"locations\":[{\"name\":\"Amsterdam\",\"lat\":52.3547498,\"long\":4.8339215},{\"name\":\"Mumbai\",\"lat\":19.0823998,\"long\":72.8111468},{\"name\":\"Copenhagen\",\"lat\":55.6713442,\"long\":12.523785},{\"lat\":40.4380638,\"long\":-3.7495758}]}".data(using: .utf8)
}