Данная статья является продолжением статьи, в которой @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.
Но иногда нужно отключить срабатывание линтера на определённую строку (причины выходят за рамки статьи). Для этого есть два пути:
Используем
buf:lint:ignore $ИМЯ_ПРАВИЛА
(для обратной совместимости с Buf).Или
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
Теперь рассмотрим наш сценарий:
-
Исходный message:
message LoginRequest { string username = 1; string password = 2; }
-
Наши изменения:
message LoginRequest { oneof login { string username = 1; string email = 2; } string password = 2; }
-
Запускаем 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 — с радостью ответим на все вопросы!
MrAwesome
Спасибо за статью, давно слежу за проектом, давно хотел потрогать руками замену бафу.