Implemented the APIClient client.
This commit is contained in:
parent
3a87c57371
commit
74a22d5844
@ -0,0 +1,27 @@
|
||||
//
|
||||
// DateFormatter+Formatter.swift
|
||||
// APIServices
|
||||
//
|
||||
// Created by Javier Cicchelli on 04/12/2022.
|
||||
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension DateFormatter {
|
||||
static let iso8601 = {
|
||||
let dateFormatter = DateFormatter()
|
||||
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
|
||||
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
static let isoZulu = {
|
||||
let dateFormatter = DateFormatter()
|
||||
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
|
||||
|
||||
return dateFormatter
|
||||
}()
|
||||
}
|
@ -31,3 +31,7 @@ extension Item: Decodable {
|
||||
case contentType
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Equatable
|
||||
|
||||
extension Item: Equatable {}
|
||||
|
@ -15,3 +15,7 @@ public struct Me {
|
||||
// MARK: - Decodable
|
||||
|
||||
extension Me: Decodable {}
|
||||
|
||||
// MARK: - Equatable
|
||||
|
||||
extension Me: Equatable {}
|
||||
|
113
Libraries/Sources/APIService/Structs/APIClient.swift
Normal file
113
Libraries/Sources/APIService/Structs/APIClient.swift
Normal file
@ -0,0 +1,113 @@
|
||||
//
|
||||
// APIClient.swift
|
||||
// APIService
|
||||
//
|
||||
// Created by Javier Cicchelli on 04/12/2022.
|
||||
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct APIClient {
|
||||
private let session: URLSession
|
||||
private let decoder: JSONDecoder
|
||||
private let makeURLRequest: MakeURLRequestUseCase
|
||||
}
|
||||
|
||||
// MARK: - Initialisers
|
||||
|
||||
extension APIClient {
|
||||
init(configuration: URLSessionConfiguration = .default) {
|
||||
self.session = .init(configuration: configuration)
|
||||
self.decoder = .init()
|
||||
self.makeURLRequest = .init()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Client
|
||||
|
||||
extension APIClient: Client {
|
||||
func request<Model>(
|
||||
endpoint: some Endpoint,
|
||||
model: Model.Type
|
||||
) async throws -> Model where Model : Decodable {
|
||||
let urlRequest = try makeURLRequest(endpoint: endpoint)
|
||||
let (data, response) = try await session.data(for: urlRequest)
|
||||
|
||||
guard
|
||||
let httpResponse = response as? HTTPURLResponse,
|
||||
let responseStatus = ResponseStatus(rawValue: httpResponse.statusCode)
|
||||
else {
|
||||
throw APIClientError.responseNotReturned
|
||||
}
|
||||
|
||||
switch responseStatus {
|
||||
case .ok,
|
||||
.created:
|
||||
return try await decode(data, as: model)
|
||||
case .noContent:
|
||||
throw APIClientError.notSupported
|
||||
case .badRequest:
|
||||
throw APIClientError.itemAlreadyExist
|
||||
case .notFound:
|
||||
throw APIClientError.itemDoesNotExist
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func request(
|
||||
endpoint: some Endpoint
|
||||
) async throws -> Data {
|
||||
let urlRequest = try makeURLRequest(endpoint: endpoint)
|
||||
let (data, response) = try await session.data(for: urlRequest)
|
||||
|
||||
guard
|
||||
let httpResponse = response as? HTTPURLResponse,
|
||||
let responseStatus = ResponseStatus(rawValue: httpResponse.statusCode)
|
||||
else {
|
||||
throw APIClientError.responseNotReturned
|
||||
}
|
||||
|
||||
switch responseStatus {
|
||||
case .ok,
|
||||
.noContent:
|
||||
return data
|
||||
case .created:
|
||||
throw APIClientError.notSupported
|
||||
case .badRequest:
|
||||
throw APIClientError.itemIsNotFile
|
||||
case .notFound:
|
||||
throw APIClientError.itemDoesNotExist
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension APIClient {
|
||||
func decode<Model: Decodable>(
|
||||
_ data: Data,
|
||||
as model: Model.Type
|
||||
) async throws -> Model {
|
||||
do {
|
||||
decoder.dateDecodingStrategy = .formatted(.isoZulu)
|
||||
|
||||
return try decoder.decode(model, from: data)
|
||||
} catch {
|
||||
decoder.dateDecodingStrategy = .iso8601
|
||||
|
||||
return try decoder.decode(model, from: data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Errors
|
||||
|
||||
public enum APIClientError: Error {
|
||||
case responseNotReturned
|
||||
case itemIsNotFile
|
||||
case itemAlreadyExist
|
||||
case itemDoesNotExist
|
||||
case notSupported
|
||||
}
|
@ -0,0 +1,337 @@
|
||||
//
|
||||
// APIClient+RequestTests.swift
|
||||
// APIServiceTests
|
||||
//
|
||||
// Created by Javier Cicchelli on 04/12/2022.
|
||||
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import APIService
|
||||
|
||||
final class APIClientRequestTests: XCTestCase {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
private let makeURLRequest = MakeURLRequestUseCase()
|
||||
private let dateFormatter = DateFormatter.iso8601
|
||||
private let sessionConfiguration = {
|
||||
let configuration = URLSessionConfiguration.default
|
||||
|
||||
configuration.protocolClasses = [MockURLProtocol.self]
|
||||
|
||||
return configuration
|
||||
}()
|
||||
|
||||
private var client: APIClient!
|
||||
private var url: URL!
|
||||
private var data: Data!
|
||||
|
||||
// MARK: Setup
|
||||
|
||||
override func setUp() async throws {
|
||||
client = .init(configuration: sessionConfiguration)
|
||||
}
|
||||
|
||||
// MARK: Request cases
|
||||
|
||||
func test_withSomeEndpoint_andSomeModel_whenResponseStatusOk() async throws {
|
||||
// GIVEN
|
||||
let endpoint = GetMeEndpoint(
|
||||
username: "username",
|
||||
password: "password"
|
||||
)
|
||||
|
||||
url = try makeURLRequest(endpoint: endpoint).url
|
||||
data = "{\"firstName\":\"Noel\",\"lastName\":\"Flantier\",\"rootItem\":{\"id\":\"4b8e41fd4a6a89712f15bbf102421b9338cfab11\",\"parentId\":\"\",\"name\":\"dossierTest\",\"isDir\":true,\"modificationDate\":\"2021-11-29T10:57:13Z\"}}\n".data(using: .utf8)
|
||||
|
||||
MockURLProtocol.mockData[url] = MockURLResponse(
|
||||
status: .ok,
|
||||
headers: [:],
|
||||
data: data
|
||||
)
|
||||
|
||||
// WHEN
|
||||
let result = try await client.request(endpoint: endpoint, model: Me.self)
|
||||
|
||||
// THEN
|
||||
XCTAssertEqual(result, Me(
|
||||
firstName: "Noel",
|
||||
lastName: "Flantier",
|
||||
rootItem: .init(
|
||||
idParent: "",
|
||||
id: "4b8e41fd4a6a89712f15bbf102421b9338cfab11",
|
||||
name: "dossierTest",
|
||||
isDirectory: true,
|
||||
lastModifiedAt: dateFormatter.date(from: "2021-11-29T10:57:13Z")!,
|
||||
size: nil,
|
||||
contentType: nil
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
func test_withSomeEndpoint_andSomeModel_whenResponseStatusCreated() async throws {
|
||||
// GIVEN
|
||||
let endpoint = GetMeEndpoint(
|
||||
username: "username",
|
||||
password: "password"
|
||||
)
|
||||
|
||||
url = try makeURLRequest(endpoint: endpoint).url
|
||||
data = "{\"firstName\":\"Noel\",\"lastName\":\"Flantier\",\"rootItem\":{\"id\":\"4b8e41fd4a6a89712f15bbf102421b9338cfab11\",\"parentId\":\"\",\"name\":\"dossierTest\",\"isDir\":true,\"modificationDate\":\"2021-11-29T10:57:13Z\"}}\n".data(using: .utf8)
|
||||
|
||||
MockURLProtocol.mockData[url] = MockURLResponse(
|
||||
status: .created,
|
||||
headers: [:],
|
||||
data: data
|
||||
)
|
||||
|
||||
// WHEN
|
||||
let result = try await client.request(endpoint: endpoint, model: Me.self)
|
||||
|
||||
// THEN
|
||||
XCTAssertEqual(result, Me(
|
||||
firstName: "Noel",
|
||||
lastName: "Flantier",
|
||||
rootItem: .init(
|
||||
idParent: "",
|
||||
id: "4b8e41fd4a6a89712f15bbf102421b9338cfab11",
|
||||
name: "dossierTest",
|
||||
isDirectory: true,
|
||||
lastModifiedAt: dateFormatter.date(from: "2021-11-29T10:57:13Z")!,
|
||||
size: nil,
|
||||
contentType: nil
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
func test_withSomeEndpoint_andSomeModel_whenResponseStatusNoContent() async throws {
|
||||
// GIVEN
|
||||
let endpoint = GetMeEndpoint(
|
||||
username: "username",
|
||||
password: "password"
|
||||
)
|
||||
|
||||
url = try makeURLRequest(endpoint: endpoint).url
|
||||
|
||||
MockURLProtocol.mockData[url] = MockURLResponse(
|
||||
status: .noContent,
|
||||
headers: [:],
|
||||
data: nil
|
||||
)
|
||||
|
||||
// WHEN & THEN
|
||||
do {
|
||||
_ = try await client.request(endpoint: endpoint, model: Me.self)
|
||||
} catch APIClientError.notSupported {
|
||||
XCTAssertTrue(true)
|
||||
} catch {
|
||||
XCTAssertTrue(false)
|
||||
}
|
||||
}
|
||||
|
||||
func test_withSomeEndpoint_andSomeModel_whenResponseStatusBadRequest() async throws {
|
||||
// GIVEN
|
||||
let endpoint = GetMeEndpoint(
|
||||
username: "username",
|
||||
password: "password"
|
||||
)
|
||||
|
||||
url = try makeURLRequest(endpoint: endpoint).url
|
||||
|
||||
MockURLProtocol.mockData[url] = MockURLResponse(
|
||||
status: .badRequest,
|
||||
headers: [:],
|
||||
data: nil
|
||||
)
|
||||
|
||||
// WHEN & THEN
|
||||
do {
|
||||
_ = try await client.request(endpoint: endpoint, model: Me.self)
|
||||
} catch APIClientError.itemAlreadyExist {
|
||||
XCTAssertTrue(true)
|
||||
} catch {
|
||||
XCTAssertTrue(false)
|
||||
}
|
||||
}
|
||||
|
||||
func test_withSomeEndpoint_andSomeModel_whenResponseStatusNotFound() async throws {
|
||||
// GIVEN
|
||||
let endpoint = GetMeEndpoint(
|
||||
username: "username",
|
||||
password: "password"
|
||||
)
|
||||
|
||||
url = try makeURLRequest(endpoint: endpoint).url
|
||||
|
||||
MockURLProtocol.mockData[url] = MockURLResponse(
|
||||
status: .notFound,
|
||||
headers: [:],
|
||||
data: nil
|
||||
)
|
||||
|
||||
// WHEN & THEN
|
||||
do {
|
||||
_ = try await client.request(endpoint: endpoint, model: Me.self)
|
||||
} catch APIClientError.itemDoesNotExist {
|
||||
XCTAssertTrue(true)
|
||||
} catch {
|
||||
XCTAssertTrue(false)
|
||||
}
|
||||
}
|
||||
|
||||
func test_withSomeEndpoint_andSomeModel_whenResponseDataIncorrect() async throws {
|
||||
// GIVEN
|
||||
let endpoint = GetMeEndpoint(
|
||||
username: "username",
|
||||
password: "password"
|
||||
)
|
||||
|
||||
url = try makeURLRequest(endpoint: endpoint).url
|
||||
data = "{\"firstName\":\"Noel\",\"lastName\":\"Flantier\",\"rootItem\":{\"id\":\"4b8e41fd4a6a89712f15bbf102421b9338cfab11\"\"isDir\":true}}\n".data(using: .utf8)
|
||||
|
||||
MockURLProtocol.mockData[url] = MockURLResponse(
|
||||
status: .ok,
|
||||
headers: [:],
|
||||
data: data
|
||||
)
|
||||
|
||||
// WHEN & THEN
|
||||
do {
|
||||
_ = try await client.request(endpoint: endpoint, model: Me.self)
|
||||
} catch is DecodingError {
|
||||
XCTAssertTrue(true)
|
||||
} catch {
|
||||
XCTAssertTrue(false)
|
||||
}
|
||||
}
|
||||
|
||||
func test_withSomeEndpoint_whenResponseStatusOk() async throws {
|
||||
// GIVEN
|
||||
let endpoint = GetDataEndpoint(
|
||||
itemId: UUID().uuidString,
|
||||
username: "username",
|
||||
password: "password"
|
||||
)
|
||||
|
||||
url = try makeURLRequest(endpoint: endpoint).url
|
||||
data = "This is just some dummy data for testing purposes".data(using: .utf8)
|
||||
|
||||
MockURLProtocol.mockData[url] = MockURLResponse(
|
||||
status: .ok,
|
||||
headers: [:],
|
||||
data: data
|
||||
)
|
||||
|
||||
// WHEN
|
||||
let result = try await client.request(endpoint: endpoint)
|
||||
|
||||
// THEN
|
||||
XCTAssertEqual(result, data)
|
||||
}
|
||||
|
||||
func test_withSomeEndpoint_whenResponseStatusCreated() async throws {
|
||||
// GIVEN
|
||||
let endpoint = GetDataEndpoint(
|
||||
itemId: UUID().uuidString,
|
||||
username: "username",
|
||||
password: "password"
|
||||
)
|
||||
|
||||
url = try makeURLRequest(endpoint: endpoint).url
|
||||
|
||||
MockURLProtocol.mockData[url] = MockURLResponse(
|
||||
status: .created,
|
||||
headers: [:],
|
||||
data: nil
|
||||
)
|
||||
|
||||
// WHEN & THEN
|
||||
do {
|
||||
try await client.request(endpoint: endpoint)
|
||||
} catch APIClientError.notSupported {
|
||||
XCTAssertTrue(true)
|
||||
} catch {
|
||||
XCTAssertTrue(false)
|
||||
}
|
||||
}
|
||||
|
||||
func test_withSomeEndpoint_whenResponseStatusNoContent() async throws {
|
||||
// GIVEN
|
||||
let endpoint = GetDataEndpoint(
|
||||
itemId: UUID().uuidString,
|
||||
username: "username",
|
||||
password: "password"
|
||||
)
|
||||
|
||||
url = try makeURLRequest(endpoint: endpoint).url
|
||||
data = .init()
|
||||
|
||||
MockURLProtocol.mockData[url] = MockURLResponse(
|
||||
status: .noContent,
|
||||
headers: [:],
|
||||
data: nil
|
||||
)
|
||||
|
||||
// WHEN
|
||||
let result = try await client.request(endpoint: endpoint)
|
||||
|
||||
// THEN
|
||||
XCTAssertEqual(result, data)
|
||||
}
|
||||
|
||||
func test_withSomeEndpoint_whenResponseStatusBadRequest() async throws {
|
||||
// GIVEN
|
||||
let endpoint = GetDataEndpoint(
|
||||
itemId: UUID().uuidString,
|
||||
username: "username",
|
||||
password: "password"
|
||||
)
|
||||
|
||||
url = try makeURLRequest(endpoint: endpoint).url
|
||||
|
||||
MockURLProtocol.mockData[url] = MockURLResponse(
|
||||
status: .badRequest,
|
||||
headers: [:],
|
||||
data: nil
|
||||
)
|
||||
|
||||
// WHEN & THEN
|
||||
do {
|
||||
try await client.request(endpoint: endpoint)
|
||||
} catch APIClientError.itemIsNotFile {
|
||||
XCTAssertTrue(true)
|
||||
} catch {
|
||||
XCTAssertTrue(false)
|
||||
}
|
||||
}
|
||||
|
||||
func test_withSomeEndpoint_whenResponseStatusNotFound() async throws {
|
||||
// GIVEN
|
||||
let endpoint = GetDataEndpoint(
|
||||
itemId: UUID().uuidString,
|
||||
username: "username",
|
||||
password: "password"
|
||||
)
|
||||
|
||||
url = try makeURLRequest(endpoint: endpoint).url
|
||||
|
||||
MockURLProtocol.mockData[url] = MockURLResponse(
|
||||
status: .notFound,
|
||||
headers: [:],
|
||||
data: nil
|
||||
)
|
||||
|
||||
// WHEN & THEN
|
||||
do {
|
||||
try await client.request(endpoint: endpoint)
|
||||
} catch APIClientError.itemDoesNotExist {
|
||||
XCTAssertTrue(true)
|
||||
} catch {
|
||||
XCTAssertTrue(false)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user