Files
eatvio_app/api_contract

Регламент синхронизации 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, локально: make export-api-contract / make check-api-contract.
  • eatvio_app, CI: шаг API contract manifest check в .gitea/workflows/test.yml.
  • eatvio-web, локально (если рядом лежит копия eatvio_app): make sync-flutter-contract.
  • eatvio-web, CI: tests/Feature/Contract/FlutterApiContractTest.php в общем прогоне.

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

Каждая модель: 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 не проверяется (неклассифицированные типы), чтобы не давать ложных срабатываний.