Sourcery для Swift: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Sourcery для Swift: Полное руководство по кодогенерации

1. Что это и зачем нужна?

Sourcery — это мощный инструмент кодогенерации для языка Swift, созданный компанией Krzysztof Zabłocki и его командой. Он позволяет автоматически генерировать шаблонный код (boilerplate code) на основе аннотаций и структуры ваших исходных файлов. Вместо того чтобы вручную писать повторяющиеся реализации протоколов, Equatable, Hashable, Codable, мок-объекты для тестирования или код для работы с Auto Layout, вы описываете правила генерации один раз, а Sourcery делает всё остальное.

Зачем это нужно? В реальных проектах на Swift значительная часть кода является механической и повторяющейся. Это не только отнимает время разработчика, но и является источником ошибок: забыли добавить новое поле в Equatable — получили баг. Sourcery решает эту проблему, беря на себя рутину. Он анализирует ваш код (AST — абстрактное синтаксическое дерево) и на основе шаблонов (Stencil или Swift-based) генерирует новые файлы. Это особенно полезно в больших проектах с десятками и сотнями моделей, ViewController'ов или сервисов.

Sourcery не является runtime-библиотекой — это инструмент времени компиляции. Вы запускаете его как часть процесса сборки (через Build Phase в Xcode или через SPM/Tuist), он генерирует файлы, а затем эти файлы компилируются вместе с остальным кодом. Такой подход гарантирует нулевой overhead в рантайме и полную предсказуемость.

2. Установка

Существует несколько способов установки Sourcery. Самый простой и рекомендуемый — через Homebrew:

brew install sourcery

Для проектов, использующих Swift Package Manager (SPM), можно добавить Sourcery как плагин. Однако чаще всего его используют как отдельный бинарник, вызываемый из скрипта сборки. Для установки через Mint (менеджер пакетов для Swift-инструментов):

mint install krzysztofzablocki/Sourcery

Также можно скачать готовый бинарник с GitHub Releases и поместить его в папку проекта. После установки убедитесь, что команда доступна:

sourcery --version

3. Быстрый старт — минимальный рабочий пример

Создадим простой пример: автоматическая генерация реализации протокола Equatable для структуры.

Шаг 1: Создайте файл с исходным кодом (например, Sources/Models/User.swift):

import Foundation

// sourcery: AutoEquatable
struct User {
    let id: Int
    let name: String
    let email: String
}

Обратите внимание на комментарий // sourcery: AutoEquatable — это аннотация, которая говорит Sourcery, что для этой структуры нужно сгенерировать реализацию Equatable.

Шаг 2: Создайте шаблон (например, Templates/AutoEquatable.stencil):

// Generated by Sourcery
// swiftlint:disable all

{% for type in types.all where type.annotations.AutoEquatable %}
extension {{ type.name }}: Equatable {
    static func == (lhs: {{ type.name }}, rhs: {{ type.name }}) -> Bool {
        {% for variable in type.storedVariables %}
        {% if not forloop.first %} && {% endif %}lhs.{{ variable.name }} == rhs.{{ variable.name }}
        {% endfor %}
    }
}
{% endfor %}

Шаг 3: Запустите Sourcery:

sourcery --sources Sources/Models --templates Templates --output Generated

Шаг 4: Результат — в папке Generated появится файл AutoEquatable.generated.swift:

// Generated by Sourcery
// swiftlint:disable all

extension User: Equatable {
    static func == (lhs: User, rhs: User) -> Bool {
        return lhs.id == rhs.id && lhs.name == rhs.name && lhs.email == rhs.email
    }
}

Готово! Теперь User автоматически соответствует Equatable.

4. Основные методы/функции/классы Sourcery

Sourcery предоставляет богатый API для работы с типами, переменными, методами и аннотациями. Ниже — ключевые элементы с примерами.

4.1. Type.all — Доступ ко всем типам

Сигнатура (в шаблоне): types.all

Что делает: Возвращает массив всех типов (структур, классов, перечислений, протоколов), найденных в исходных файлах. Это отправная точка для большинства шаблонов.

Пример:

// Сгенерировать список всех типов
{% for type in types.all %}
// - {{ type.name }} ({{ type.kind }})
{% endfor %}

4.2. Type.annotations — Чтение аннотаций

Сигнатура (в шаблоне): type.annotations["YourAnnotation"]

Что делает: Позволяет читать пользовательские аннотации, заданные в комментариях // sourcery:. Аннотации могут быть простыми (ключ) или со значениями (key=value).

Пример:

// В исходном коде: // sourcery: AutoCodable, isImportant = true
{% if type.annotations.AutoCodable %}
extension {{ type.name }}: Codable {}
{% endif %}

{% if type.annotations.isImportant %}
// Важный тип: {{ type.name }}
{% endif %}

4.3. Type.storedVariables — Хранимые переменные

Сигнатура (в шаблоне): type.storedVariables

Что делает: Возвращает массив только хранимых переменных (stored properties), исключая вычисляемые (computed). Критически важно для генерации Equatable, Codable, Hashable.

Пример:

// Генерация инициализатора
extension {{ type.name }} {
    init({{ type.storedVariables | map:"name" | join:", " }}) {
        {% for var in type.storedVariables %}
        self.{{ var.name }} = {{ var.name }}
        {% endfor %}
    }
}

4.4. Type.allVariables — Все переменные

Сигнатура (в шаблоне): type.allVariables

Что делает: Возвращает массив всех переменных, включая вычисляемые (computed properties). Полезно для генерации протоколов, которые требуют и хранимые, и вычисляемые свойства.

Пример:

// Генерация протокола с описанием всех свойств
protocol Described {
    var propertyList: String { get }
}

extension {{ type.name }}: Described {
    var propertyList: String {
        return """
        {% for var in type.allVariables %}
        - {{ var.name }}: {{ var.typeName }}
        {% endfor %}
        """
    }
}

4.5. Type.methods — Методы типа

Сигнатура (в шаблоне): type.methods

Что делает: Возвращает массив методов, объявленных в типе. Позволяет генерировать обёртки, моки или декораторы.

Пример:

// Генерация логгера для всех методов
extension {{ type.name }} {
    {% for method in type.methods %}
    func logged{{ method.name|capitalize }}({{ method.parameters|map:"name"|join:", " }}) -> {{ method.returnTypeName }} {
        print("Calling {{ method.name }}")
        return self.{{ method.name }}({{ method.parameters|map:"name"|join:", " }})
    }
    {% endfor %}
}

4.6. Variable.typeName — Имя типа переменной

Сигнатура (в шаблоне): variable.typeName

Что делает: Возвращает строковое представление типа переменной (например, String, Int?, [String]). Используется для генерации корректных объявлений.

Пример:

// Генерация фабричного метода с дефолтными значениями
extension {{ type.name }} {
    static func makeDefault() -> {{ type.name }} {
        return {{ type.name }}(
            {% for var in type.storedVariables %}
            {{ var.name }}: {{ var.typeName.defaultValue }}{% if not forloop.last %},{% endif %}
            {% endfor %}
        )
    }
}

4.7. Type.inheritedTypes — Наследуемые типы

Сигнатура (в шаблоне): type.inheritedTypes

Что делает: Возвращает массив строк с именами протоколов или суперклассов, от которых наследуется тип. Позволяет генерировать код только для типов, соответствующих определённым протоколам.

Пример:

// Генерация мока только для протоколов
{% for type in types.all where type.kind == "protocol" %}
class Mock{{ type.name }}: {{ type.name }} {
    {% for method in type.methods %}
    var {{ method.name }}Called = false
    func {{ method.name }}({{ method.parameters|map:"name"|join:", " }}) -> {{ method.returnTypeName }} {
        {{ method.name }}Called = true
        // return default value
        return {{ method.returnTypeName.defaultValue }}
    }
    {% endfor %}
}
{% endfor %}

4.8. Type.composedType — Композитные типы

Сигнатура (в шаблоне): type.composedType

Что делает: Возвращает информацию о композитных типах (например, Codable & Hashable). Полезно при работе с протоколами, которые объединяют несколько требований.

Пример:

// Генерация для типов, которые являются Codable & Hashable
{% for type in types.all %}
{% if type.composedType.name == "Codable & Hashable" %}
extension {{ type.name }}: CustomStringConvertible {
    var description: String {
        return "{{ type.name }}(id: \(hashValue))"
    }
}
{% endif %}
{% endfor %}

5. Реальный практический пример (150+ строк кода)

Создадим систему для автоматической генерации сетевого слоя (API клиента) на основе моделей. Это типичная задача в iOS-разработке.

Исходная модель (Sources/Models/APIEndpoint.swift):

import Foundation

// sourcery: AutoEndpoint
struct GetUserEndpoint {
    let userId: Int
    // sourcery: path = "/users/{{ userId }}"
    let method: String = "GET"
}

// sourcery: AutoEndpoint
struct CreateUserEndpoint {
    let name: String
    let email: String
    // sourcery: path = "/users"
    let method: String = "POST"
    // sourcery: body
    let body: [String: Any] = [:]
}

Шаблон (Templates/AutoEndpoint.stencil):

// Generated by Sourcery
// swiftlint:disable all

import Foundation

{% for type in types.all where type.annotations.AutoEndpoint %}
extension {{ type.name }}: APIRequest {
    var path: String {
        {% for var in type.allVariables %}
        {% if var.annotations.path %}
        return "{{ var.annotations.path }}"
        {% endif %}
        {% endfor %}
        return "/{{ type.name|lowercase }}"
    }

    var httpMethod: String {
        return {{ type.name }}().method
    }

    var queryItems: [URLQueryItem]? {
        {% if type.storedVariables %}
        var items: [URLQueryItem] = []
        {% for var in type.storedVariables %}
        {% if var.name != "method" and var.name != "body" %}
        items.append(URLQueryItem(name: "{{ var.name }}", value: "\({{ var.name }})"))
        {% endif %}
        {% endfor %}
        return items.isEmpty ? nil : items
        {% else %}
        return nil
        {% endif %}
    }

    var httpBody: Data? {
        {% for var in type.allVariables %}
        {% if var.annotations.body %}
        return try? JSONSerialization.data(withJSONObject: {{ var.name }}, options: [])
        {% endif %}
        {% endfor %}
        return nil
    }
}
{% endfor %}

// Протокол, которому будут соответствовать все сгенерированные типы
protocol APIRequest {
    var path: String { get }
    var httpMethod: String { get }
    var queryItems: [URLQueryItem]? { get }
    var httpBody: Data? { get }
}

Результат генерации (Generated/AutoEndpoint.generated.swift):

// Generated by Sourcery
// swiftlint:disable all

import Foundation

extension GetUserEndpoint: APIRequest {
    var path: String {
        return "/users/\(userId)"
    }

    var httpMethod: String {
        return GetUserEndpoint().method
    }

    var queryItems: [URLQueryItem]? {
        var items: [URLQueryItem] = []
        items.append(URLQueryItem(name: "userId", value: "\(userId)"))
        return items.isEmpty ? nil : items
    }

    var httpBody: Data? {
        return nil
    }
}

extension CreateUserEndpoint: APIRequest {
    var path: String {
        return "/users"
    }

    var httpMethod: String {
        return CreateUserEndpoint().method
    }

    var queryItems: [URLQueryItem]? {
        var items: [URLQueryItem] = []
        items.append(URLQueryItem(name: "name",                    

Рекомендаций