JavaRush /Курсы /Swift SELF /Endpoint как enum + параметры

Endpoint как enum + параметры

Swift SELF
64 уровень , 0 лекция
Открыта

1. endpoint поверх URLComponents

Если честно, после знакомства с URLComponents у многих возникает ощущение: «Ну всё, проблема решена, давайте просто в каждом месте кода собирать URL и делать запрос». Это чувство понятно — и опасно. Опасно не потому, что URLComponents плохой, а потому что «собирать URL в каждом месте» очень быстро превращается в копипасту, расхождения в заголовках и тихие баги: в одном месте path начинается с /, в другом — нет, в одном месте вы добавили limit, в другом забыли.

Endpoint — это следующая ступень дисциплины. Мы договариваемся, что запросы к API у нас описываются как данные, в одном перечислении (enum). А уже потом отдельный builder превращает эти данные в URLRequest.

И вот тут появляется главный психологический бонус: вместо «давайте в этом месте тоже соберём URL» вы начинаете думать «а какой endpoint мы вызываем?». Это сильно проще для мозга. И для ревью кода тоже.

Endpoint как описание: метод + путь + query

Давайте аккуратно определим, что именно мы называем endpoint’ом в рамках курса.

Endpoint — это не сетевой вызов, и даже не URLRequest. Endpoint — это описание маршрута: каким HTTP-методом идём, какой path используем, какие query-параметры прикладываем. Его задача — быть понятным, типизированным и исчерпывающим.

Тут очень помогает enum с associated values. У enum есть два супер-свойства: он задаёт конечный список вариантов, а associated values позволяют каждому варианту носить свои параметры. Если вам нужна хорошая интуиция, можно думать, что кейс enum с associated values похож на «конструктор» или «статическую функцию», которая возвращает значение enum. Это довольно близкая модель: кейс без параметров — как статическое свойство, кейс с параметрами — как статическая функция.

И ещё важный нюанс читабельности: метки (labels) у associated values — часть имени «конструктора» кейса, поэтому они реально влияют на то, как читается код при создании значения. Это нам пригодится, чтобы endpoint’ы выглядели как предложение на английском: .searchBooks(query:page:limit:), а не .searchBooks("swift", 1, 20) (второе тоже работает, но читается хуже).

2. Базовые типы: HTTPMethod и EndpointBuildError

Перед тем как писать endpoint’ы, мы сделаем пару маленьких «строительных кирпичей». Тут легко переборщить с архитектурой, поэтому держим всё приземлённо: один enum для метода, один enum для ошибки сборки.

Начнём с метода. В файле Sources/Networking/HTTPMethod.swift (путь условный — подстройте под ваш проект) можно держать так:

import Foundation

enum HTTPMethod: String {
    case get = "GET"
}

Да, пока только "GET". Не потому что других не существует, а потому что наш учебный сценарий сейчас про чтение данных. Когда появится запись — добавим новые методы.

Теперь ошибка сборки запроса. Это не NetworkError, потому что сеть мы ещё не трогали — мы даже URL иногда собрать не можем. Это другая природа проблемы: «мы сами неправильно сконфигурировали endpoint / base URL».

import Foundation

enum EndpointBuildError: Error {
    case invalidURL
}

Соблазн сделать fatalError("URL is nil") будет возникать регулярно. У компилятора, конечно, нет кнопки «отобрать клавиатуру», но наша дисциплина такая: если можно обработать ошибку типом Error, мы так и делаем.

4. База API отдельно: scheme и host не должны расползаться

Когда студенты впервые делают endpoint’ы, они часто начинают зашивать host внутрь каждого кейса: мол, этот endpoint ходит на api.example.com, другой — на other.example.com. Иногда так и нужно, но в большинстве учебных проектов это создаёт хаос: хост меняется — и вы «чините» 20 мест.

Поэтому вводим маленький тип APIBase, который хранит базовые части адреса. Это похоже на «настройку окружения»: dev/prod, или просто единый источник истины.

import Foundation

struct APIBase {
    let scheme: String
    let host: String
}

Кстати, именно из-за таких «URL из кусочков» нам и нужен URLComponents. В Foundation эти типы — часть нормального инструментария, и URLComponents/URLQueryItem там существуют как отдельные сущности, а не как «самодельный парсер строки».

5. BooksEndpoint: один список запросов к Books API

Теперь самое вкусное: делаем endpoint’ы. В учебном приложении LibraryCLI мы часто работаем с книгами, поэтому пусть сетевой API тоже «про книги»: поиск и детали.

Создадим BooksEndpoint:

import Foundation

enum BooksEndpoint {
    case search(query: String, page: Int, limit: Int)
    case details(id: Int)
}

Смотрите, как приятно читается создание значений:

let ep1 = BooksEndpoint.search(query: "swift", page: 1, limit: 20)
let ep2 = BooksEndpoint.details(id: 42)

Это и есть та самая идея «endpoint как данные»: мы пока ничего не выполняем, просто описываем намерение.

Метод запроса как вычисляемое свойство

Почти всегда метод зависит от кейса. Сейчас у нас оба "GET", но всё равно оформим правильно — чтобы позже не переписывать всё.

import Foundation

extension BooksEndpoint {
    var method: HTTPMethod {
        .get
    }
}

Да, switch не нужен — пока что.

path: строка, но по правилам

path остаётся строкой, потому что у URL путь — это реально строковый компонент. Но мы вводим правило: path всегда начинается с /. Одно правило — минус пять багов.

import Foundation

extension BooksEndpoint {
    var path: String {
        switch self {
        case .search:
            return "/v1/books"
        case .details(let id):
            return "/v1/books/\(id)"
        }
    }
}

Обратите внимание: мы не вставляем сюда query (?page=...). Никогда. Query — отдельным механизмом.

queryItems: параметры и контроль «пустого ?»

Query параметры — это как специи: если сыпать их везде без меры, блюдо превращается в… ну, в строку URL, склеенную руками.

Мы вернём nil для кейсов без query (например, детали по id), и массив URLQueryItem для поиска.

import Foundation

extension BooksEndpoint {
    var queryItems: [URLQueryItem]? {
        switch self {
        case let .search(query, page, limit):
            return [
                URLQueryItem(name: "q", value: query),
                URLQueryItem(name: "page", value: String(page)),
                URLQueryItem(name: "limit", value: String(limit))
            ]
        case .details:
            return nil
        }
    }
}

На этом этапе мы сделали важную вещь: все правила маршрутизации в одном месте. Если API решит, что параметр поиска должен называться "query", а не "q", вы меняете это в одном месте, и всё.

6. Builder: превращаем endpoint в URLRequest

Сейчас у нас есть endpoint, но выполнять его мы не умеем — и не должны в этом же типе. Наша цель: сделать builder, который знает, как собрать URLRequest. Тогда любое место в коде сможет взять endpoint и получить «готовый к отправке» запрос.

Сделаем метод makeRequest(base:). Можно оформить как extension на endpoint, можно отдельным типом RequestBuilder. Для начала проще extension: меньше сущностей — меньше путаницы.

import Foundation

extension BooksEndpoint {
    func makeRequest(base: APIBase) throws -> URLRequest {
        var c = URLComponents()
        c.scheme = base.scheme
        c.host = base.host
        c.path = path
        c.queryItems = queryItems

        guard let url = c.url else { throw EndpointBuildError.invalidURL }
        return URLRequest(url: url)
    }
}

Это уже полезно, но у запроса ещё нет метода и заголовков. Дополним: метод ставим из method, заголовки пока добавим минимально-одинаковые (например, "Accept").

import Foundation

extension BooksEndpoint {
    func makeRequest(base: APIBase) throws -> URLRequest {
        var c = URLComponents()
        c.scheme = base.scheme
        c.host = base.host
        c.path = path
        c.queryItems = queryItems

        guard let url = c.url else { throw EndpointBuildError.invalidURL }

        var r = URLRequest(url: url)
        r.httpMethod = method.rawValue
        r.setValue("application/json", forHTTPHeaderField: "Accept")
        return r
    }
}

Почему это важно держать централизованно? Потому что заголовки — это классическая зона «а тут забыл» или «а тут случайно другой». Builder — ваш единый источник истины: в нём все запросы получаются одинаково «аккуратными».

7. Пример использования: CLI видит только endpoint и URLRequest

Теперь давайте представим, что в LibraryCLI у нас есть команда «поиск книг» (или похожая). Важно: CLI-слой не обязан знать, как устроен URL. CLI-слой должен сформировать endpoint и попросить request.

Сборка может выглядеть примерно так:

import Foundation

let base = APIBase(scheme: "https", host: "api.example.com")
let ep = BooksEndpoint.search(query: "swift basics", page: 1, limit: 20)

let request = try ep.makeRequest(base: base)
print(request.url?.absoluteString ?? "nil")

Вывод (примерно):

// https://api.example.com/v1/books?q=swift%20basics&page=1&limit=20

И вот тут вы ловите удовольствие: пробелы закодированы сами, &/? нигде руками не писали, а формат запроса единый.

8. Единая точка сборки: зачем и как это спасает проект

Когда проект маленький, кажется, что можно «и так». Но наш курс как раз про то, чтобы писать код, который не развалится, когда вы добавите 10 команд и 15 запросов.

Единый builder защищает вас сразу от нескольких бед.

Во-первых, он убирает размазывание базовых правил по проекту. Если в одном месте вы случайно делаете c.path = "v1/books" без слэша, вы получите не тот URL или nil — и будете долго грустить. Когда сборка в одном месте, такие правила проще закрепить и проверить.

Во-вторых, builder делает поведение заголовков предсказуемым. В реальных API бывает, что без "Accept": "application/json" сервер отдаёт HTML-страницу (и вы потом «декодируете» её в DTO и удивляетесь). Централизованный builder снижает вероятность таких сюрпризов.

В-третьих, builder помогает отделять слои: endpoint — это описание маршрута, builder — это сборка запроса, а сеть — это выполнение. Мы сейчас сознательно не смешиваем эти роли, потому что дальше по курсу вам будет легче тестировать и поддерживать код.

Схема потока: как это течёт по коду

Чтобы зафиксировать в голове «кто за что отвечает», удобно один раз увидеть это как поток.

flowchart LR
    CLI["CLI-команда<br/>(парсинг аргументов)"] --> EP["BooksEndpoint<br/>(enum + параметры)"]
    EP --> RB["makeRequest(base:)<br/>(builder)"]
    RB --> REQ["URLRequest"]
    REQ --> NET["Сетевой слой<br/>(выполнение)"]

В этой лекции мы «закрываем» первые три узла: CLI знает, какой endpoint нужен, endpoint хранит параметры, builder делает URLRequest. Сеть пока оставляем за кадром.

9. Типичные ошибки

Ошибка №1: добавлять query прямо в path.
Очень хочется написать return "/v1/books?page=\(page)", потому что «быстрее». Потом появляется второй параметр, третий, пробелы, амперсанды — и вы внезапно пишете мини-браузер руками. Лечится просто: path — только путь, параметры — только queryItems.

Ошибка №2: не учитывать, что components.url — это URL?.
Иногда кажется, что если scheme/host/path заполнены, URL обязан собраться. На практике можно ошибиться в path, передать пустой host, или случайно оставить scheme пустым. Поэтому guard let url = c.url else { throw ... } — это не «мелочь», а часть контракта сборки.

Ошибка №3: смешивать ошибку сборки запроса с сетевой ошибкой.
Если URL не собрался, сеть ещё даже не началась. Это другой класс проблем. Когда вы всё называете «NetworkError», потом в логах и обработке ошибок возникает каша: вы лечите «нет интернета» там, где надо было исправить host.

Ошибка №4: держать host внутри endpoint’ов.
Иногда кажется логичным: «этот кейс ходит туда, тот — сюда». Но если у вас один API (а в учебном проекте почти всегда так), лучше держать APIBase отдельно. Тогда смена окружения (например, тестовый хост) не превращается в квест «найди 20 строк».

Ошибка №5: делать endpoint «умным» и пытаться выполнить сеть внутри enum.
enum endpoint’ов должен быть максимально простым: метод/путь/параметры. Как только вы начинаете делать внутри него URLSession, вы теряете разделение ролей, и код становится сложно тестировать и расширять. Пусть endpoint будет «описанием», а не «действием».

1
Задача
Swift SELF, 64 уровень, 0 лекция
Недоступна
Служебный пинг
Служебный пинг
1
Задача
Swift SELF, 64 уровень, 0 лекция
Недоступна
Поиск книг
Поиск книг
1
Задача
Swift SELF, 64 уровень, 0 лекция
Недоступна
Плохая база
Плохая база
1
Задача
Swift SELF, 64 уровень, 0 лекция
Недоступна
Гибкий каталог
Гибкий каталог
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ