Данная статья является продолжением статьи, в которой @ZergsLaw уже подробно описал, зачем мы начали делать EasyP и почему мы старались по возможности сохранять общие форматы с Buf. Поэтому повторяться не будем и сразу перейдем к тому, как использовать EasyP в своих проектах.

Установка

Начнем, конечно же, с установки EasyP:

  • Установка последней версии с GitHub:

    go install github.com/easyp-tech/easyp/cmd/easyp@latest
  • Недавно добавили возможность установки через brew:

    brew install easyp-tech/tap/easyp

Линтинг

Холивары в PR по поводу отступов, форматирования и т. п., в идеале, решаются стилевыми гайдами. Но есть ли они у всех команд?
У питонистов есть PEP, но даже он не покрывает все вопросы.
Да и даже при наличии styleguide где-то на вики тратить время на анализ соответствия кода в PR — не лучшая идея.

Уже давно всё это автоматизировано линтерами, легко встраиваемыми в CI/CD.
Но почему-то я часто сталкиваюсь с удивлением на вопрос: «А какой линтер для Protobuf используете?».
При этом для НЕ protobuf линтеры, естественно, использовали, а для protobuf — почему-то нет.
Хотя навести беспорядок в proto-файлах ничуть не сложнее.

Допустим, есть некий сервис:

service ServiceAPI {
  rpc example_one(google.protobuf.Empty) returns (google.protobuf.Empty);
}

Потом приходит другой разработчик и добавляет ещё один эндпоинт:

service ServiceAPI {
  rpc example_one(google.protobuf.Empty) returns (google.protobuf.Empty);
  rpc ExampleTwo(google.protobuf.Empty) returns (google.protobuf.Empty);
}

Код валидный, генерация не выдаст ошибок, но общая стилистика уже нарушена.

Или другой, более критичный пример:

enum Example {
  example_one = 0;
  ExampleOne = 1;
}

Кроме нарушения стилистики, получим ошибку при генерации:

Enum name ExampleOne has the same name as example_one if you ignore case and strip out the enum name prefix (if any). (If you are using allow_alias, please assign the same number to each enum value name.)

Хотелось бы автоматизировать поиск подобных проблем — например, в CI сразу подсвечивать ошибки, экономя время на ревью.

И тут на сцену выходит линтер EasyP.

Давайте взглянем на его конфигурацию.

YAML-конфиг:

lint:
  use:
    - MINIMAL

Здесь можно указать как набор правил списком, так и название группы: MINIMAL, BASIC, DEFAULT, COMMENTS или UNARY_RPC.
Для простоты названия правил мы позаимствовали у Buf :)

Запускаем EasyP:

easyp lint --path .

Если есть ошибки, получаем вывод:

super_api/v1/services.proto:11:3:CORPUS_UNSPECIFIED enum zero value suffix is not valid (ENUM_ZERO_VALUE_SUFFIX)

Формат сообщения включает название объекта с ошибкой и правило.
Сейчас в планах — добавление формата code-quality.

Но иногда нужно отключить срабатывание линтера на определённую строку (причины выходят за рамки статьи). Для этого есть два пути:

  1. Используем buf:lint:ignore $ИМЯ_ПРАВИЛА (для обратной совместимости с Buf).

  2. Или nolint: $ИМЯ_ПРАВИЛА.

Но кроме несогласованного описания API, есть другая, более серьёзная проблема...

Обратная совместимость

Рассмотрим ситуацию: есть сервис, уже работающий в проде.
Но бизнес не стоит на месте, и в один день приходит PO и говорит: «Нам нужно добавить логин по почте».

Ок, идём в наш proto-файл:

message LoginRequest {
  string username = 1;
  string password = 2;
}

Сейчас можно использовать только username. Добавляем поле с почтой.
Логика такая: пользователь может указывать как username, так и email, поэтому делаем так, чтобы одно поле могло принимать два значения:

message LoginRequest {
  oneof login {
    string username = 1;
    string email = 2;
  }
  string password = 2;
}

Коммитим, отправляем на ревью, деплоим.
И что получаем?

Причём это реальный случай на одном из проектов. Несколько человек посмотрели PR с этим кодом и пропустили.
В общем, человеческий фактор — и никуда от него не деться.

Но у нас же есть автоматизированные средства для анализа стиля, форматирования и т. п.
Что, если сделать автоматическую проверку обратной совместимости API, чтобы исключить человеческий фактор? Машину не обманешь, и даже если будет false-positive, это лишний повод перепроверить, и шанс ошибок уменьшится.

Как автоматизировать? Какие этапы проверки?
Все изменения в основную ветку должны проходить через Pull Request, после успешного прохождения тестов, линтера и ревью (спасибо, кэп).
Добавим сюда проверку на обратную совместимость: если что-то сломалось — падаем, не даём влить PR.
При этом сравниваем текущий код с кодом из основной ветки.

easyp breaking

В конфиге EasyP указываем ветку для сравнения:

breaking:
  against_git_ref: master

Теперь рассмотрим наш сценарий:

  1. Исходный message:

    message LoginRequest {
      string username = 1;
      string password = 2;
    }
  2. Наши изменения:

    message LoginRequest {
      oneof login {
        string username = 1;
        string email = 2;
      }
      string password = 2;
    }
  3. Запускаем EasyP и получаем:

    messages/v1/messages.proto:15:1: Previously present field "1" with name "username" on message "LoginRequest" was deleted. (BREAKING_CHECK)

Вот! EasyP «даёт по рукам» — PR уже не влить, а значит, и прод не сломать.
Либо делай изменения без нарушения обратной совместимости, либо создавай новую версию API.

Как это работает под капотом?

EasyP делает «слепки» исходников: один слепок — текущий код, второй — код из ветки, с которой сравниваем.
Затем запускается процесс сравнения: EasyP составляет «карту» всех proto-файлов со всеми вложениями: сообщениями, сервисами, enum’ами и т. п.
При этом учитывается пакет, в котором находится объект: например, если есть два сообщения с одинаковым именем, но в разных пакетах, EasyP это поймёт и не будет ругаться.

Также важно, что в рамках одного пакета, если мы переместили сообщение в другой файл (например, при рефакторинге), но не меняли теги и названия полей (то есть не нарушили обратную совместимость), EasyP это поймёт и не будет считать это ошибкой.
К слову, Buf на такое ругается и говорит: «Переделывай», что странно, ведь обратная совместимость не нарушена.

Пакетный менеджер

Некоторые внутренние детали работы EasyP уже описаны в статье Эдгара. Здесь же поговорим про конфигурацию и использование.

Список зависимостей можно указать в конфиге в секции deps:

deps:
  - github.com/googleapis/googleapis
  - github.com/bufbuild/protovalidate@v0.3.1
  - github.com/grpc-ecosystem/grpc-gateway@v2.19.1

По аналогии с go mod, указываем ссылку на Git-репозиторий, а после @ — версию (это может быть тег или хэш коммита).
Если версия не указана, EasyP использует последний коммит из дефолтной ветки.

EasyP фиксирует зависимости в lock-файле.

Команды:

  • easyp download — скачивает зависимости из lock-файла (если его нет — ставит версии из YAML).

  • easyp update — игнорирует lock файл, ставит версии из YAML и перезаписывает lock файл.

  • easyp vendor — копирует зависимости в локальную директорию проекта.

По умолчанию кеш хранится в ~/.easyp, но это можно изменить через переменную окружения EASYPPATH.

Но всё это не имело бы смысла без генерации кода.

Генератор кода

Вся мощь Protobuf раскрывается благодаря плагинам.
Использование разных языков, генерация документации и другие хотелки легко решаются с их помощью.

Пример команды с protoc:

protoc --go_out=. \
  --go_opt=paths=source_relative \ 
  --go-grpc_out=. \
  --go-grpc_opt=paths=source_relative \ 
  --java_out=gen/proto/java \ 
  --rust_out=gen/proto/rust \ 
  --swift_out=gen/proto/swift \ 
  helloworld/helloworld.proto

А вот конфигурация EasyP:

generate:
  plugins:
    - name: go
      out: .
      opts:
        paths: source_relative
    - name: go-grpc
      out: .
      opts:
        paths: source_relative
        require_unimplemented_servers: false
    - name: grpc-gateway
      out: .
      opts:
        paths: source_relative
    - name: openapiv2
      out: .
      opts:
        simple_operation_ids: false
        generate_unbound_methods: false
    - name: validate-go
      out: .
      opts:
        paths: source_relative

Указываем имя плагина и его настройки (аналогично параметрам для protoc).
Под капотом, кстати, вызывается именно protoc.

Но с генерацией это ещё не всё. EasyP должен знать, где находятся исходные proto-файлы.
Они могут быть как локальными, так и в удалённом репозитории:

Удалённый репозиторий:

generate:
  inputs:
    - git_repo:
        url: "URL TO REMOTE REPO"
        sub_directory: "DIR WITH PROTO FILES"

Локальные файлы:

generate:
  inputs:
    - directory: "WHERE YOUR PROTO FILES ARE"

Автодополнение

Чтобы не запоминать все команды (их пока не так много ?), можно настроить автодополнение.
Поддерживаются bash и zsh.

Для zsh:
Добавить в .zshrc:

source <(easyp completion zsh)

Для bash:
Установить bash-completion, затем добавить в .bashrc:

source <(easyp completion bash)

Ставьте звёзды на GitHub!
Вступайте в нашу группу в TG — с радостью ответим на все вопросы!

Комментарии (3)


  1. MrAwesome
    29.05.2025 11:38

    Спасибо за статью, давно слежу за проектом, давно хотел потрогать руками замену бафу.


  1. dos
    29.05.2025 11:38

    При изменении имени файла в сгенерированных клиентских файлах изменится путь импорта, поэтому как бы buf правильно ругается на нарушение обратной совместимости


    1. hound406 Автор
      29.05.2025 11:38

      Спасибо за комментарий, хорошее замечание.