Универсальный веб-сервис

Практически всегда запросы к веб-сервисам имеют однотипную логику, поддающуюся унификации через дженерики. Одним из вариантов такого подхода я бы и хотел поделиться.

Основная логика ляжет в родительский класс Webservice:

import Foundation

class Webservice {
    enum Method {
        case get([URLQueryItem]?)
        case post(Data?)

        var name: String {
            switch self {
            case .get:
                return "GET"

            case .post:
                return "POST"
            }
        }
    }

    struct Header {
        let name: String
        let value: String

        static let jsonContentType = Header(
            name: "Content-Type",
            value: "application/json; charset=UTF-8"
        )
    }

    let baseURL: URL
    let urlSession: URLSession
    let jsonDecoder: JSONDecoder

    init(
        baseURL: URL,
        urlSession: URLSession = .shared,
        jsonDecoder: JSONDecoder = JSONDecoder()
    ) {
        self.baseURL = baseURL
        self.urlSession = urlSession
        self.jsonDecoder = jsonDecoder
    }

    func load<Entity: Codable, Path: CustomStringConvertible>(
        _ type: Entity.Type,
        path: Path,
        method: Method = .get(nil),
        headers: [Header] = [.jsonContentType]
    ) async throws -> Entity {
        var urlRequest: URLRequest

        guard let url = URL(string: path.description, relativeTo: baseURL) else {
            throw URLError(.badURL)
        }

        switch method {
        case .get(let queryItems):
            var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true)

            if urlComponents?.queryItems == nil {
                urlComponents?.queryItems = queryItems
            } else {
                urlComponents?.queryItems?.append(contentsOf: queryItems ?? [])
            }

            guard let url = urlComponents?.url else {
                throw URLError(.badURL)
            }

            urlRequest = URLRequest(url: url)

        case .post(let data):
            urlRequest = URLRequest(url: url)
            urlRequest.httpBody = data
        }

        urlRequest.httpMethod = method.name

        for header in headers {
            urlRequest.addValue(header.value, forHTTPHeaderField: header.name)
        }

        let (data, response) = try await urlSession.data(for: urlRequest)

        guard let httpResponse = response as? HTTPURLResponse,
              (200...299).contains(httpResponse.statusCode) else {
            throw URLError(.badServerResponse)
        }

        return try jsonDecoder.decode(Entity.self, from: data)
    }
}

А конкретные методы определяются уже в веб-сервисах, наследующихся от него. В данном случае PhotosWebservice и метод загрузки фотографий:

import Foundation

class PhotoWebservice: Webservice {
    enum Path: CustomStringConvertible {
        case photos
        case user(id: Int)

        var description: String {
            switch self {
            case .photos: return "photos"
            case .user(let id): return "user/\(id)"
            }
        }
    }

    func loadPhotos() async throws -> [Photo] {
        return try await load([Photo].self, path: Path.photos)
    }
}