Files
eatvio_app/api_contract/README.md

9.4 KiB
Raw Blame History

Регламент синхронизации API-контракта (eatvio-web ↔ eatvio_app)

Стороны и артефакты

Репозиторий Артефакт Роль
eatvio_app tool/export_api_contract.dart генератор: парсит freezed-декларации из исходников (не из *.g.dart)
eatvio_app api_contract/flutter_model_contract.json источник истины: требования мобильных моделей к JSON (required/optional ключи)
eatvio-web tests/Fixtures/Contract/flutter_model_contract.json синхронизированная копия, по ней CI бэка валидирует API-ресурсы
eatvio-web tests/Feature/Contract/FlutterApiContractTest.php контрактные тесты: worst-case сериализация ресурса ↔ манифест

Контракт односторонний по данным: бэкенд отдаёт JSON → flutter читает (fromJson). Экстремальный случай — nullable-колонка на бэке при required-поле во flutter: ровно он дал продовой баг FLUTTER-1BX (satiety: null → TypeError в _$MealModelFromJson, дневник мобильных клиентов не работал, КБЖУ-информер показал нули).

Направление 1: backend → frontend (бэк меняет ответ API)

  1. Дев меняет API-ресурс в eatvio-web (например, делает поле nullable / удаляет ключ).
  2. FlutterApiContractTest падает, если новое поведение нарушает манифест — это и есть фильтр обратной несовместимости.
  3. Дальше два осознанных пути:
    • Сохранить совместимость (предпочтительно): вернуть значение/дефолт на бэке (?? 0, как с time → '00:00:00'). Манифест не меняется.
    • Ломать сознательно: мержится mobile-first — сначала в eatvio_app поле становится nullable/опциональным, выходит релиз приложения; затем PR в eatvio-web меняет ресурс и копию манифеста. Пока старые версии приложения в рынке — бэк держит старый формат.
  4. Коммит копии манифеста в eatvio-web без изменения ресурса бессмысленен и не проходит ревью: манифест меняется только вместе с ресурсом.

Направление 2: frontend → backend (фронт ужесточает требования)

  1. Дев меняет freezed-модель в eatvio_app (например, добавляет required int nutrition).
  2. CI eatvio_app (test.yml, шаг API contract manifest check) регенерирует манифест и диффает: устарел → PR не проходит.
  3. Дев обновляет манифест (make export-api-contract) и в том же PR синхронизирует копию в eatvio-web (tests/Fixtures/Contract/).
  4. CI eatvio-web прогоняет FlutterApiContractTest против новой копии: если бэк не выдаёт новые required-ключи — падает уже там, и бэк добавляет гарантии до релиза приложения.
  5. Порядок релизов: бэк докручивается первым (он назад-совместим), затем выходит приложение.

Правила совместимости (шпаргалка)

Изменение на бэке Ломает приложение? Действие
Поле остаётся non-null нет ничего
Добавлен новый ключ нет (flutter игнорирует лишнее) ничего; добавить в модель отдельно при необходимости
non-null → nullable / ключ удалён да mobile-first: релиз приложения, потом бэк
flutter: int → int? нет make export-api-contract + синк манифеста
flutter: добавлен required-ключ бэк может не выполнять синк манифеста → бэк добавляет ключ/дефолт до релиза

Автоматика

Где Что Когда срабатывает
eatvio_app, CI шаг API contract manifest check (test.yml) каждый PR: манифест обязан быть регенерирован из исходников
eatvio_app, CI contract-sync.yml push в main с изменениями api_contract/** → авто-PR в eatvio-web с новой копией манифеста
eatvio_app, локально make export-api-contract / make check-api-contract руками при работе над моделями
eatvio-web, CI FlutterApiContractTest (12 тестов) каждый PR: worst-case сериализация ресурсов ↔ манифест
eatvio-web, CI contract-drift-check.yaml еженощно: сравнение локальной копии манифеста с eatvio_app@main, при дрейфе — issue
eatvio-web, локально make sync-flutter-contract руками, если авто-PR не пришёл

Секреты: CONTRACT_SYNC_TOKEN — PAT с write на eatvio/eatvio-web (для contract-sync) и read eatvio/eatvio_app + issues на eatvio/eatvio-web (для drift-check).

Как команды держат друг друга в рабочем состоянии (три слоя)

  1. Предотвращение (до мержа). Контрактные тесты на обеих сторонах: flutter не смержит модели без регенерации манифеста; бэкенд не смержит ресурс, отдающий null/не тот тип там, где приложение требует non-null.
  2. Синхронизация (при мерже). Авто-PR из contract-sync.yml приносит новую копию манифеста в бэкенд; CI бэкенда на этом PR показывает, выполняет ли сервер новые требования. Ночной drift-check ловит всё, что проскочило мимо автоматики.
  3. Обнаружение (после релиза). Sentry: группа TypeError ... in _$XxxFromJson (проект flutter) = нарушение контракта в проде. Такой issue должен ссылаться на контрактную пару и закрываться фиксом ресурса + контрак тестом.

Ритуалы:

  • Меняешь модель в приложении → make export-api-contract в том же PR; манифест уедет в бэкенд автоматически.
  • Меняешь API-ресурс → прогоняй контрактный тест; красный = либо делай совместимо (дефолты), либо mobile-first релиз.
  • WARNING генератора о коллизии имён моделей (Step, Users, ...) — повод переименовать Dart-классы: под одним именем могут скрываться разные контракты.

Формат манифеста

Каждая модель: required / optional (JSON-ключи), types (Dart-тип поля) и kinds (JSON-вид значения, по которому бэкенд-чекер валидирует payload):

kind Dart-типы Ожидание в JSON
number int, double, num число (int или float)
string String строка
boolean bool строго true/false (int 0/1 — нарушение)
list List<...> массив
map Map<...> объект
model вложенная freezed-модель объект
enum Dart enum строка-имя или int
any / unknown dynamic и прочее не проверяется

Это закрывает второй класс мобильных крашей: жёсткие касты (json['from_diet'] as bool, json['quantity'] as num) роняют приложение даже на non-null значениях — например, если PHP отдаст tinyint 0/1 вместо bool или строку "150" вместо числа.

Границы подхода

  • Манифест покрывает ответы API (fromJson). Контракт запросов (retrofit → FormRequest) пока не формализован.
  • Новый ключ бэкенда не требует изменения манифеста — flutter молча игнорирует лишнее; расширять модели — добровольно.
  • kinds: unknown не проверяется (неклассифицированные типы), чтобы не давать ложных срабатываний.