diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 516f806a..11c48f67 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -160,6 +160,15 @@ jobs: flutter pub get dart run build_runner build --delete-conflicting-outputs + # Контракт манифеста API: манифест обязан быть регенерирован из исходников freezed-моделей. + # Diff после регенерации = PR менял модели, но не обновил api_contract/flutter_model_contract.json. + # Обновлённый манифест нужно также синхронизировать в eatvio-web: tests/Fixtures/Contract/. + - name: API contract manifest check + run: | + dart run tool/export_api_contract.dart api_contract/flutter_model_contract.json + git diff --exit-code api_contract/flutter_model_contract.json \ + || (echo "::error::api_contract/flutter_model_contract.json устарел. Регенерируй манифест (make export-api-contract) и синхронизируй его в eatvio-web: tests/Fixtures/Contract/flutter_model_contract.json" && exit 1) + - name: Native Analyze run: flutter analyze diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..935349f7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,76 @@ +# AGENTS.md + +Flutter/Dart mobile app (iOS + Android, Russian-market nutrition/consulting product). +Gitea-hosted; CI is Gitea Actions in `.gitea/workflows/` (not GitHub Actions, though it references `github.com/actions/*`). + +## Toolchain (trust these over README — README version numbers are stale) + +- CI runs **Flutter 3.41.2 stable**, **Java/JDK 21** (`.gitea/workflows/test.yml`). +- `pubspec.yaml` requires **Dart SDK ^3.10.4**; app version is `1.3.23+105` (README claims 1.3.0+79 — wrong). +- Build/test/lint via Flutter directly or the `Makefile`. The repo has a `package.json` **only** to wire a git pre-push hook via `simple-git-hooks`; it is not a Node project. + +## CRITICAL: generated code is NOT committed + +`.gitignore` excludes `*.g.dart`, `*.freezed.dart`, `*.gr.dart`, `*.mocks.dart`. After any checkout / pull / model change you **must regenerate** before `flutter analyze`, `flutter test`, or any build, or they fail on missing files: + +```bash +dart run build_runner build --delete-conflicting-outputs +# or: make build-generated-files +``` + +Generators in use: `freezed`, `json_serializable`, `retrofit_generator`, `mockito` (`build_runner`). +Exception: `lib/generated/` (l10n output from `intl_utils` / `flutter_intl`) **is** committed — don't regenerate-and-clobber it casually. + +## `.env` (required for running the app, NOT for unit tests) + +- The app loads `.env` at startup (`lib/core/app_initializer.dart` → `dotenv.load`) and ships it as a Flutter asset (`pubspec.yaml` assets). +- There is **no `env.example` in the repo** despite README/`ENV_SETUP.md` saying `cp env.example .env`. Locally you must obtain `.env` from the team. In CI it is checked out from a **private `eatvio/eatvio_app_env` repo**. +- `flutter test` does **not** need `.env`: tests seed env via `dotenv.loadFromString(...)` (see `test/core/app_config_test.dart`). Access values only through `lib/core/env_config.dart` (`EnvConfig`). +- Build types: `AppBuildType { prod, tests, dev }` (`lib/core/app_initializer.dart`). `prod` → `BASE_URL`; `tests`/`dev` → `DEV_BASE_URL`. `main.dart` hardcodes `prod`. + +## Commands + +```bash +make run-analyse # flutter analyze +make run-tests # flutter test +make run-checks # analyze THEN test (also the pre-push hook) +make build-generated-files # build_runner --delete-conflicting-outputs +make clean-coverage # strips generated files from coverage/lcov.info +``` + +Run a single test file / single test: +```bash +flutter test test/core/app_config_test.dart +flutter test test/core/app_config_test.dart --plain-name "creates config with prod build type" +``` + +Coverage (must be run with the flag): `flutter test --coverage`, then `make clean-coverage`. `analyze_coverage.py` / `analyze_coverage_by_uncovered.py` summarize `coverage/lcov.info`. + +## CI gate (binding constraint on PRs) + +`.gitea/workflows/test.yml` runs on every PR, in order: +1. `flutter clean` → `flutter pub get` → `dart run build_runner build --delete-conflicting-outputs` +2. `flutter analyze` +3. `flutter test --coverage` +4. **Coverage must be ≥ 70%** (generated files excluded) or the job fails — keep this in mind when adding untested code. +5. `flutter build apk --release --dart-define=HIDE_PLANS=false --dart-define=BUILD_DATE=...` + +Note: other workflow files (`build.yml`, `docker-run-tests.yml`) are simpler/older and do **not** run `build_runner`; `test.yml` is the authoritative one. + +## Architecture map + +- Entry: `lib/main.dart` → `AppInitializer.initialize` (env → DI → Sentry → Firebase → analytics) → runs app under Sentry. +- DI: `get_it` (`sl` global), registered in `lib/core/locator_service.dart` via `lib/core/di/*_registrations.dart` (app_config, external, service, repository, cubit). +- State: `flutter_bloc` (Cubits/Blocs). Global providers declared in `lib/core/app_config.dart` (`globalBlocProviders`). +- Routing: `go_router` (`lib/core/app_router.dart`), with deep-link/push handling in `lib/core/notifications/`. +- Networking: `dio` + `retrofit` (→ `.gr.dart`); auth token interceptor in `lib/core/interceptors/`. +- Models: `freezed` + `json_serializable` (→ `.freezed.dart` / `.g.dart`). +- Realtime: Pusher channels via `lib/core/websockets/` (`WsManager`). +- Features: ~40 self-contained folders under `lib/features//`, conventionally `.../presentation/bloc/_cubit/_cubit.dart`. Tests mirror `lib/` under `test/` with fixtures in `test/fixtures/`. +- Localization: Russian only (`ru`); strings via generated `S` class from `lib/generated/l10n.dart`. + +## Conventions / gotchas + +- `analysis_options.yaml` enables `flutter_lints` + extra `prefer_const_*` rules and **excludes generated + l10n files** from analysis. `dart_code_metrics` is also configured (cyclomatic ≤20, ≤50 lines executable, ≤4 params, nesting ≤5) but metrics exclude `test/**`. +- Don't commit secrets: `.env`, `android/key.properties`, `*.jks` keystores are all gitignored. +- Comments and many code identifiers are in Russian (UI strings, doc comments like `/// Загрузка переменных окружения`); match surrounding style. diff --git a/Makefile b/Makefile index 96b60dc2..240aaf3e 100644 --- a/Makefile +++ b/Makefile @@ -11,3 +11,14 @@ build-generated-files: clean-coverage: @./scripts/remove_generated_from_coverage.sh + +# Регенерация манифеста API-контракта из исходников freezed-моделей. +# Манифест — источник требований мобильных моделей для eatvio-web +# (tests/Fixtures/Contract/flutter_model_contract.json). +export-api-contract: + dart run tool/export_api_contract.dart api_contract/flutter_model_contract.json + +# Проверка актуальности манифеста (для CI): регенерирует и диффает с закоммиченным. +check-api-contract: export-api-contract + @git diff --exit-code api_contract/flutter_model_contract.json \ + || (echo "api_contract/flutter_model_contract.json устарел: регенерируй (make export-api-contract) и синхронизируй в eatvio-web tests/Fixtures/Contract/" && exit 1) diff --git a/api_contract/README.md b/api_contract/README.md new file mode 100644 index 00000000..d7b36cb4 --- /dev/null +++ b/api_contract/README.md @@ -0,0 +1,72 @@ +# Регламент синхронизации 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` не проверяется (неклассифицированные типы), чтобы не давать ложных срабатываний. diff --git a/api_contract/flutter_model_contract.json b/api_contract/flutter_model_contract.json new file mode 100644 index 00000000..a7a1c8a5 --- /dev/null +++ b/api_contract/flutter_model_contract.json @@ -0,0 +1,3938 @@ +{ + "AboutPlanState": { + "required": [ + "isLoading", + "chosenOptionIndex" + ], + "optional": [ + "error", + "planModel" + ], + "types": { + "isLoading": "bool", + "error": "String", + "planModel": "PlanModel", + "chosenOptionIndex": "int" + }, + "kinds": { + "isLoading": "boolean", + "error": "string", + "planModel": "model", + "chosenOptionIndex": "number" + } + }, + "AuthResultEntity": { + "required": [ + "accessToken", + "userId", + "clientId" + ], + "optional": [ + "refreshToken", + "firstName", + "lastName", + "email", + "phone", + "gender", + "birthDate" + ], + "types": { + "accessToken": "String", + "userId": "String", + "clientId": "String", + "refreshToken": "String", + "firstName": "String", + "lastName": "String", + "email": "String", + "phone": "String", + "gender": "GenderEnum", + "birthDate": "String" + }, + "kinds": { + "accessToken": "string", + "userId": "string", + "clientId": "string", + "refreshToken": "string", + "firstName": "string", + "lastName": "string", + "email": "string", + "phone": "string", + "gender": "enum", + "birthDate": "string" + } + }, + "Author": { + "required": [ + "author_slug", + "author_name", + "author_profile_icon", + "author_is_me" + ], + "optional": [ + "is_entity_subscribed_by_me", + "author_place_in_rating", + "author_roles" + ], + "types": { + "author_slug": "String", + "author_name": "String", + "author_profile_icon": "String", + "author_is_me": "bool", + "is_entity_subscribed_by_me": "bool", + "author_place_in_rating": "int", + "author_roles": "List" + }, + "kinds": { + "author_slug": "string", + "author_name": "string", + "author_profile_icon": "string", + "author_is_me": "boolean", + "is_entity_subscribed_by_me": "boolean", + "author_place_in_rating": "number", + "author_roles": "list" + } + }, + "BadgeModel": { + "required": [ + "id", + "title", + "description", + "image", + "type" + ], + "optional": [], + "types": { + "id": "int", + "title": "String", + "description": "String", + "image": "String", + "type": "String" + }, + "kinds": { + "id": "number", + "title": "string", + "description": "string", + "image": "string", + "type": "string" + } + }, + "BarcodeProductModel": { + "required": [], + "optional": [ + "Количество/вес", + "Рынок сбыта", + "Калорийность", + "Белки", + "Жиры", + "Углеводы" + ], + "types": { + "Количество/вес": "String", + "Рынок сбыта": "String", + "Калорийность": "double", + "Белки": "double", + "Жиры": "double", + "Углеводы": "double" + }, + "kinds": { + "Количество/вес": "string", + "Рынок сбыта": "string", + "Калорийность": "number", + "Белки": "number", + "Жиры": "number", + "Углеводы": "number" + } + }, + "BarcodeTaskStatusModel": { + "required": [], + "optional": [ + "status", + "task_id", + "data" + ], + "types": { + "status": "String", + "task_id": "String", + "data": "BarcodeProductModel" + }, + "kinds": { + "status": "string", + "task_id": "string", + "data": "model" + } + }, + "BlockedUsersModel": { + "required": [ + "data" + ], + "optional": [], + "types": { + "data": "List" + }, + "kinds": { + "data": "list" + } + }, + "BlogPostCreateCredentialsModel": { + "required": [ + "title", + "description", + "video_url" + ], + "optional": [ + "status" + ], + "types": { + "title": "String", + "description": "String", + "video_url": "String", + "status": "String" + }, + "kinds": { + "title": "string", + "description": "string", + "video_url": "string", + "status": "string" + } + }, + "BlogPostCreateState": { + "required": [], + "optional": [ + "isLoading", + "isSuccess", + "errorMessage", + "titleError", + "descriptionError", + "videoUrlError", + "imagesError", + "formError" + ], + "types": { + "isLoading": "bool", + "isSuccess": "bool", + "errorMessage": "String", + "titleError": "String", + "descriptionError": "String", + "videoUrlError": "String", + "imagesError": "String", + "formError": "String" + }, + "kinds": { + "isLoading": "boolean", + "isSuccess": "boolean", + "errorMessage": "string", + "titleError": "string", + "descriptionError": "string", + "videoUrlError": "string", + "imagesError": "string", + "formError": "string" + } + }, + "CalendarModel": { + "required": [ + "date", + "is_current_day_included_in_the_chosen_diet_progression" + ], + "optional": [ + "diary" + ], + "types": { + "date": "String", + "is_current_day_included_in_the_chosen_diet_progression": "bool", + "diary": "Object" + }, + "kinds": { + "date": "string", + "is_current_day_included_in_the_chosen_diet_progression": "boolean", + "diary": "any" + } + }, + "CanceledModerationModel": { + "required": [ + "title", + "status" + ], + "optional": [ + "additional" + ], + "types": { + "title": "String", + "status": "ModerationStatus", + "additional": "List" + }, + "kinds": { + "title": "string", + "status": "enum", + "additional": "list" + } + }, + "CertPhotosModel": { + "required": [ + "url" + ], + "optional": [], + "types": { + "url": "String" + }, + "kinds": { + "url": "string" + } + }, + "CertificatesModel": { + "required": [ + "title", + "date" + ], + "optional": [], + "types": { + "title": "String", + "date": "String" + }, + "kinds": { + "title": "string", + "date": "string" + } + }, + "ChatMessageModel": { + "required": [ + "id", + "data", + "author_id", + "updated_at", + "read_by_to_user" + ], + "optional": [ + "media_data", + "urls_media_info", + "room_id", + "type", + "messageState", + "uuid" + ], + "types": { + "id": "int", + "data": "String", + "media_data": "List", + "urls_media_info": "List>", + "author_id": "int", + "updated_at": "DateTime", + "read_by_to_user": "bool", + "room_id": "String", + "type": "ChatMessageType", + "messageState": "MessageState", + "uuid": "String" + }, + "kinds": { + "id": "number", + "data": "string", + "media_data": "list", + "urls_media_info": "list", + "author_id": "number", + "updated_at": "unknown", + "read_by_to_user": "boolean", + "room_id": "string", + "type": "enum", + "messageState": "enum", + "uuid": "string" + } + }, + "ChatMessagesModel": { + "required": [ + "data", + "meta" + ], + "optional": [], + "types": { + "data": "List", + "meta": "ChatMetaModel" + }, + "kinds": { + "data": "list", + "meta": "model" + } + }, + "ChatMetaModel": { + "required": [ + "last_page" + ], + "optional": [], + "types": { + "last_page": "int" + }, + "kinds": { + "last_page": "number" + } + }, + "ChatRoom": { + "required": [ + "id", + "users", + "unread_messages_count" + ], + "optional": [ + "last_message", + "chosen_plan" + ], + "types": { + "id": "String", + "last_message": "ChatMessageModel", + "users": "Users", + "unread_messages_count": "int", + "chosen_plan": "ChosenPlanModel" + }, + "kinds": { + "id": "string", + "last_message": "model", + "users": "model", + "unread_messages_count": "number", + "chosen_plan": "unknown" + } + }, + "ChatRoomModel": { + "required": [ + "id", + "chat_room_setting", + "users" + ], + "optional": [ + "chosen_plan_id", + "chosenPlan" + ], + "types": { + "id": "String", + "chat_room_setting": "ChatRoomSettingsModel", + "users": "Users", + "chosen_plan_id": "int", + "chosenPlan": "ChosenPlanModel" + }, + "kinds": { + "id": "string", + "chat_room_setting": "model", + "users": "model", + "chosen_plan_id": "number", + "chosenPlan": "unknown" + } + }, + "ChatRoomPaginationModel": { + "required": [ + "data", + "number_of_unread_chat_rooms", + "meta" + ], + "optional": [], + "types": { + "data": "List", + "number_of_unread_chat_rooms": "int", + "meta": "Meta" + }, + "kinds": { + "data": "list", + "number_of_unread_chat_rooms": "number", + "meta": "model" + } + }, + "ChatRoomSettingsModel": { + "required": [ + "room_name", + "state" + ], + "optional": [], + "types": { + "room_name": "String", + "state": "String" + }, + "kinds": { + "room_name": "string", + "state": "string" + } + }, + "ChatUser": { + "required": [ + "id", + "name", + "profile_photo_url" + ], + "optional": [ + "profile_photo_path", + "is_online", + "last_seen_text" + ], + "types": { + "id": "int", + "name": "String", + "profile_photo_path": "String", + "profile_photo_url": "String", + "is_online": "bool", + "last_seen_text": "String" + }, + "kinds": { + "id": "number", + "name": "string", + "profile_photo_path": "string", + "profile_photo_url": "string", + "is_online": "boolean", + "last_seen_text": "string" + } + }, + "ChatUserModel": { + "required": [ + "id", + "name", + "slug" + ], + "optional": [ + "profile_photo_url", + "is_online", + "last_seen_text" + ], + "types": { + "id": "int", + "name": "String", + "slug": "String", + "profile_photo_url": "String", + "is_online": "bool", + "last_seen_text": "String" + }, + "kinds": { + "id": "number", + "name": "string", + "slug": "string", + "profile_photo_url": "string", + "is_online": "boolean", + "last_seen_text": "string" + } + }, + "CircumferenceModel": { + "required": [ + "id", + "user_id", + "chest_circumference", + "waist_circumference", + "hip_circumference", + "created_at", + "updated_at" + ], + "optional": [], + "types": { + "id": "int", + "user_id": "int", + "chest_circumference": "int", + "waist_circumference": "int", + "hip_circumference": "int", + "created_at": "DateTime", + "updated_at": "DateTime" + }, + "kinds": { + "id": "number", + "user_id": "number", + "chest_circumference": "number", + "waist_circumference": "number", + "hip_circumference": "number", + "created_at": "unknown", + "updated_at": "unknown" + } + }, + "Comment": { + "required": [ + "id", + "author_name", + "author_slug", + "author_profile_icon", + "body", + "children_comments", + "created_at" + ], + "optional": [], + "types": { + "id": "int", + "author_name": "String", + "author_slug": "String", + "author_profile_icon": "String", + "body": "String", + "children_comments": "List", + "created_at": "DateTime" + }, + "kinds": { + "id": "number", + "author_name": "string", + "author_slug": "string", + "author_profile_icon": "string", + "body": "string", + "children_comments": "list", + "created_at": "unknown" + } + }, + "CommentModel": { + "required": [ + "data", + "links", + "meta" + ], + "optional": [], + "types": { + "data": "List", + "links": "Links", + "meta": "Meta" + }, + "kinds": { + "data": "list", + "links": "model", + "meta": "model" + } + }, + "ComplainModel": { + "required": [], + "optional": [ + "description", + "tags" + ], + "types": { + "description": "String", + "tags": "List" + }, + "kinds": { + "description": "string", + "tags": "list" + } + }, + "CompletedCoursesModel": { + "required": [ + "date", + "title" + ], + "optional": [], + "types": { + "date": "String", + "title": "String" + }, + "kinds": { + "date": "string", + "title": "string" + } + }, + "ConsultantCertPhotoModel": { + "required": [ + "id", + "group", + "original_name", + "path", + "size", + "url" + ], + "optional": [ + "user_id", + "specs" + ], + "types": { + "id": "int", + "user_id": "int", + "group": "String", + "original_name": "String", + "path": "String", + "size": "int", + "specs": "Map", + "url": "String" + }, + "kinds": { + "id": "number", + "user_id": "number", + "group": "string", + "original_name": "string", + "path": "string", + "size": "number", + "specs": "map", + "url": "string" + } + }, + "ConsultantProfileModel": { + "required": [], + "optional": [ + "has_medical_education", + "social_links", + "cert_photos", + "completed_courses", + "legal_type", + "last_name", + "first_name", + "middle_name", + "inn", + "snils", + "passport_series", + "passport_number", + "passport_issue_date", + "passport_issuer", + "passport_issuer_code", + "birth_date", + "birth_place", + "registration_address", + "residence_address", + "citizenship", + "tax_resident_us", + "foreign_tax_resident", + "sbp_phone", + "sbp_bank_member_id", + "agreement_accepted", + "frdo_agreement_accepted" + ], + "types": { + "has_medical_education": "bool", + "social_links": "SocialLinksModel", + "cert_photos": "List", + "completed_courses": "List", + "legal_type": "String", + "last_name": "String", + "first_name": "String", + "middle_name": "String", + "inn": "String", + "snils": "String", + "passport_series": "String", + "passport_number": "String", + "passport_issue_date": "String", + "passport_issuer": "String", + "passport_issuer_code": "String", + "birth_date": "String", + "birth_place": "String", + "registration_address": "String", + "residence_address": "String", + "citizenship": "String", + "tax_resident_us": "bool", + "foreign_tax_resident": "bool", + "sbp_phone": "String", + "sbp_bank_member_id": "String", + "agreement_accepted": "bool", + "frdo_agreement_accepted": "bool" + }, + "kinds": { + "has_medical_education": "boolean", + "social_links": "model", + "cert_photos": "list", + "completed_courses": "list", + "legal_type": "string", + "last_name": "string", + "first_name": "string", + "middle_name": "string", + "inn": "string", + "snils": "string", + "passport_series": "string", + "passport_number": "string", + "passport_issue_date": "string", + "passport_issuer": "string", + "passport_issuer_code": "string", + "birth_date": "string", + "birth_place": "string", + "registration_address": "string", + "residence_address": "string", + "citizenship": "string", + "tax_resident_us": "boolean", + "foreign_tax_resident": "boolean", + "sbp_phone": "string", + "sbp_bank_member_id": "string", + "agreement_accepted": "boolean", + "frdo_agreement_accepted": "boolean" + } + }, + "ConsultationRequestModel": { + "required": [ + "education_resource", + "cert_photos" + ], + "optional": [ + "rating_place", + "canceled_moderation", + "legal_entity_resource" + ], + "types": { + "rating_place": "int", + "education_resource": "EducationModel", + "cert_photos": "List", + "canceled_moderation": "CanceledModerationModel", + "legal_entity_resource": "LegalEntityModel" + }, + "kinds": { + "rating_place": "number", + "education_resource": "model", + "cert_photos": "list", + "canceled_moderation": "model", + "legal_entity_resource": "model" + } + }, + "CreateDietModel": { + "required": [ + "type" + ], + "optional": [ + "imageUrl", + "name", + "details", + "state", + "duration", + "image", + "dailySchedule" + ], + "types": { + "imageUrl": "String", + "name": "String", + "details": "String", + "state": "DietState", + "duration": "int", + "type": "Map", + "image": "XFile", + "dailySchedule": "List>>" + }, + "kinds": { + "imageUrl": "string", + "name": "string", + "details": "string", + "state": "enum", + "duration": "number", + "type": "map", + "image": "unknown", + "dailySchedule": "list" + } + }, + "CreateDietState": { + "required": [ + "isLoading", + "isDietPublishing", + "dietData", + "dailyScheduleErrors" + ], + "optional": [ + "errorMessage", + "copiedDay", + "dietNameError", + "dietDescriptionError", + "dietDurationError", + "dietStateError", + "dietImageError", + "durationIndex", + "dietState", + "published", + "drafted", + "isDietBeingEdited" + ], + "types": { + "isLoading": "bool", + "isDietPublishing": "bool", + "errorMessage": "String", + "dietData": "CreateDietModel", + "copiedDay": "List>", + "dietNameError": "String", + "dietDescriptionError": "String", + "dietDurationError": "String", + "dietStateError": "String", + "dietImageError": "String", + "dailyScheduleErrors": "List", + "durationIndex": "int", + "dietState": "DietState", + "published": "bool", + "drafted": "bool", + "isDietBeingEdited": "bool" + }, + "kinds": { + "isLoading": "boolean", + "isDietPublishing": "boolean", + "errorMessage": "string", + "dietData": "model", + "copiedDay": "list", + "dietNameError": "string", + "dietDescriptionError": "string", + "dietDurationError": "string", + "dietStateError": "string", + "dietImageError": "string", + "dailyScheduleErrors": "list", + "durationIndex": "number", + "dietState": "enum", + "published": "boolean", + "drafted": "boolean", + "isDietBeingEdited": "boolean" + } + }, + "CreatePlanModel": { + "required": [ + "type", + "options" + ], + "optional": [ + "name", + "details", + "state", + "duration", + "image" + ], + "types": { + "name": "String", + "details": "String", + "state": "String", + "duration": "int", + "type": "List", + "image": "XFile", + "options": "List" + }, + "kinds": { + "name": "string", + "details": "string", + "state": "string", + "duration": "number", + "type": "list", + "image": "unknown", + "options": "list" + } + }, + "CreatePlanState": { + "required": [ + "isLoading", + "planData", + "validationState", + "planCreated" + ], + "optional": [ + "errorMessage", + "durationIndex", + "selectedOptionIndex" + ], + "types": { + "isLoading": "bool", + "errorMessage": "String", + "planData": "CreatePlanModel", + "validationState": "PlanValidationState", + "planCreated": "bool", + "durationIndex": "int", + "selectedOptionIndex": "int" + }, + "kinds": { + "isLoading": "boolean", + "errorMessage": "string", + "planData": "model", + "validationState": "unknown", + "planCreated": "boolean", + "durationIndex": "number", + "selectedOptionIndex": "number" + } + }, + "CreateRecipeState": { + "required": [ + "entity", + "loading" + ], + "optional": [ + "drafted", + "titleValidationError", + "descriptionValidationError", + "cookingTimeValidationError", + "activeTimeValidationError", + "servingsValidationError", + "energyPerServingValidationError", + "imageValidationError", + "stepsValidationError", + "ingredientsValidationError" + ], + "types": { + "entity": "RecipeCreateEntity", + "loading": "bool", + "drafted": "bool", + "titleValidationError": "String", + "descriptionValidationError": "String", + "cookingTimeValidationError": "String", + "activeTimeValidationError": "String", + "servingsValidationError": "String", + "energyPerServingValidationError": "String", + "imageValidationError": "String", + "stepsValidationError": "String", + "ingredientsValidationError": "String" + }, + "kinds": { + "entity": "model", + "loading": "boolean", + "drafted": "boolean", + "titleValidationError": "string", + "descriptionValidationError": "string", + "cookingTimeValidationError": "string", + "activeTimeValidationError": "string", + "servingsValidationError": "string", + "energyPerServingValidationError": "string", + "imageValidationError": "string", + "stepsValidationError": "string", + "ingredientsValidationError": "string" + } + }, + "CreatedDiet": { + "required": [ + "id" + ], + "optional": [], + "types": { + "id": "int" + }, + "kinds": { + "id": "number" + } + }, + "CredentialsModel": { + "required": [ + "email", + "password" + ], + "optional": [], + "types": { + "email": "String", + "password": "String" + }, + "kinds": { + "email": "string", + "password": "string" + } + }, + "CursorLinks": { + "required": [], + "optional": [ + "first", + "last", + "prev", + "next" + ], + "types": { + "first": "String", + "last": "String", + "prev": "String", + "next": "String" + }, + "kinds": { + "first": "string", + "last": "string", + "prev": "string", + "next": "string" + } + }, + "CursorMeta": { + "required": [ + "path", + "per_page" + ], + "optional": [ + "next_cursor", + "prev_cursor" + ], + "types": { + "path": "String", + "per_page": "int", + "next_cursor": "String", + "prev_cursor": "String" + }, + "kinds": { + "path": "string", + "per_page": "number", + "next_cursor": "string", + "prev_cursor": "string" + } + }, + "DailyNutrition": { + "required": [ + "energy", + "proteins", + "fats", + "carbohydrates" + ], + "optional": [], + "types": { + "energy": "double", + "proteins": "double", + "fats": "double", + "carbohydrates": "double" + }, + "kinds": { + "energy": "number", + "proteins": "number", + "fats": "number", + "carbohydrates": "number" + } + }, + "DailySchedule": { + "required": [ + "name", + "portion", + "quantity" + ], + "optional": [ + "nutrition", + "product_nutritional_value_id" + ], + "types": { + "name": "String", + "portion": "Portion", + "quantity": "int", + "nutrition": "DailyNutrition", + "product_nutritional_value_id": "int" + }, + "kinds": { + "name": "string", + "portion": "enum", + "quantity": "number", + "nutrition": "model", + "product_nutritional_value_id": "number" + } + }, + "DiaryDayModel": { + "required": [ + "date", + "closed", + "comment", + "state", + "meals" + ], + "optional": [ + "id", + "wakeup", + "retire", + "chosen_plan_id", + "day_of_chosen_diet", + "chosen_diet" + ], + "types": { + "id": "int", + "date": "String", + "wakeup": "String", + "retire": "String", + "closed": "int", + "comment": "String", + "state": "TypeOfDiaryModel", + "meals": "List", + "chosen_plan_id": "int", + "day_of_chosen_diet": "int", + "chosen_diet": "DiaryDietModel" + }, + "kinds": { + "id": "number", + "date": "string", + "wakeup": "string", + "retire": "string", + "closed": "number", + "comment": "string", + "state": "model", + "meals": "list", + "chosen_plan_id": "number", + "day_of_chosen_diet": "number", + "chosen_diet": "model" + } + }, + "DiaryDayWrapperModel": { + "required": [ + "diary" + ], + "optional": [], + "types": { + "diary": "DiaryDayModel" + }, + "kinds": { + "diary": "model" + } + }, + "DiaryDietModel": { + "required": [ + "id", + "duration", + "name" + ], + "optional": [], + "types": { + "id": "int", + "duration": "int", + "name": "String" + }, + "kinds": { + "id": "number", + "duration": "number", + "name": "string" + } + }, + "DiaryFutureDietDayEntity": { + "required": [ + "date", + "typeOfDiary", + "meals" + ], + "optional": [ + "diet", + "dayOfChosenDiet" + ], + "types": { + "date": "String", + "typeOfDiary": "TypeOfDiaryModel", + "meals": "List", + "diet": "DiaryDietModel", + "dayOfChosenDiet": "int" + }, + "kinds": { + "date": "string", + "typeOfDiary": "model", + "meals": "list", + "diet": "model", + "dayOfChosenDiet": "number" + } + }, + "DiaryIndexModel": { + "required": [ + "id" + ], + "optional": [], + "types": { + "id": "int" + }, + "kinds": { + "id": "number" + } + }, + "DiaryPhotoModel": { + "required": [ + "id", + "original_name", + "path", + "size" + ], + "optional": [], + "types": { + "id": "int", + "original_name": "String", + "path": "String", + "size": "int" + }, + "kinds": { + "id": "number", + "original_name": "string", + "path": "string", + "size": "number" + } + }, + "DiaryReminderNotifications": { + "required": [ + "first_reminder", + "second_reminder", + "third_reminder" + ], + "optional": [], + "types": { + "first_reminder": "ReminderSettings", + "second_reminder": "ReminderSettings", + "third_reminder": "ReminderSettings" + }, + "kinds": { + "first_reminder": "model", + "second_reminder": "model", + "third_reminder": "model" + } + }, + "Diet": { + "required": [ + "id", + "slug", + "name", + "duration", + "state", + "type", + "description_md", + "daily_schedule", + "author", + "total_likes", + "total_views", + "total_comments", + "was_liked_by_me", + "is_favorite", + "daily_nutrition" + ], + "optional": [ + "image" + ], + "types": { + "id": "int", + "slug": "String", + "name": "String", + "image": "String", + "duration": "int", + "state": "DietState", + "type": "DietType", + "description_md": "String", + "daily_schedule": "Map>>", + "author": "Author", + "total_likes": "int", + "total_views": "int", + "total_comments": "int", + "was_liked_by_me": "bool", + "is_favorite": "bool", + "daily_nutrition": "List" + }, + "kinds": { + "id": "number", + "slug": "string", + "name": "string", + "image": "string", + "duration": "number", + "state": "enum", + "type": "model", + "description_md": "string", + "daily_schedule": "map", + "author": "model", + "total_likes": "number", + "total_views": "number", + "total_comments": "number", + "was_liked_by_me": "boolean", + "is_favorite": "boolean", + "daily_nutrition": "list" + } + }, + "DietLikesModel": { + "required": [ + "total_likers", + "is_liked" + ], + "optional": [], + "types": { + "total_likers": "int", + "is_liked": "bool" + }, + "kinds": { + "total_likers": "number", + "is_liked": "boolean" + } + }, + "DietType": { + "required": [], + "optional": [ + "light", + "veg", + "carb", + "meat" + ], + "types": { + "light": "bool", + "veg": "bool", + "carb": "bool", + "meat": "bool" + }, + "kinds": { + "light": "boolean", + "veg": "boolean", + "carb": "boolean", + "meat": "boolean" + } + }, + "DietsFilterState": { + "required": [ + "sortingIndex", + "durationIndex", + "typeIndex" + ], + "optional": [], + "types": { + "sortingIndex": "int", + "durationIndex": "int", + "typeIndex": "int" + }, + "kinds": { + "sortingIndex": "number", + "durationIndex": "number", + "typeIndex": "number" + } + }, + "DietsPagination": { + "required": [ + "data", + "links", + "meta" + ], + "optional": [], + "types": { + "data": "List", + "links": "Links", + "meta": "Meta" + }, + "kinds": { + "data": "list", + "links": "model", + "meta": "model" + } + }, + "DietsState": { + "required": [ + "filterState", + "dietsList", + "isLoading" + ], + "optional": [ + "errorMessage" + ], + "types": { + "filterState": "DietsFilterState", + "dietsList": "List", + "isLoading": "bool", + "errorMessage": "String" + }, + "kinds": { + "filterState": "model", + "dietsList": "list", + "isLoading": "boolean", + "errorMessage": "string" + } + }, + "DiplomasAndCertificatesModel": { + "required": [ + "id", + "path" + ], + "optional": [], + "types": { + "id": "int", + "path": "String" + }, + "kinds": { + "id": "number", + "path": "string" + } + }, + "EducationModel": { + "required": [ + "request_state" + ], + "optional": [ + "has_medical_education", + "completed_courses", + "vk_link", + "insta_link", + "fb_link", + "tg_link", + "personal_site_link" + ], + "types": { + "has_medical_education": "int", + "completed_courses": "List>", + "vk_link": "String", + "insta_link": "String", + "fb_link": "String", + "tg_link": "String", + "personal_site_link": "String", + "request_state": "ModerationStatus" + }, + "kinds": { + "has_medical_education": "number", + "completed_courses": "list", + "vk_link": "string", + "insta_link": "string", + "fb_link": "string", + "tg_link": "string", + "personal_site_link": "string", + "request_state": "enum" + } + }, + "FeedBlogPostModel": { + "required": [ + "id", + "state", + "image", + "description", + "author", + "video_url", + "created_at", + "updated_at", + "title", + "total_likes", + "total_views", + "total_comments", + "was_liked_by_me" + ], + "optional": [ + "images", + "video_url_embed", + "video_url_thumb" + ], + "types": { + "id": "int", + "state": "String", + "image": "String", + "description": "String", + "author": "Author", + "video_url": "String", + "created_at": "DateTime", + "updated_at": "DateTime", + "title": "String", + "total_likes": "int", + "total_views": "int", + "total_comments": "int", + "was_liked_by_me": "bool", + "images": "List", + "video_url_embed": "String", + "video_url_thumb": "String" + }, + "kinds": { + "id": "number", + "state": "string", + "image": "string", + "description": "string", + "author": "model", + "video_url": "string", + "created_at": "unknown", + "updated_at": "unknown", + "title": "string", + "total_likes": "number", + "total_views": "number", + "total_comments": "number", + "was_liked_by_me": "boolean", + "images": "list", + "video_url_embed": "string", + "video_url_thumb": "string" + } + }, + "FeedBlogPostSettingsModel": { + "required": [ + "data", + "meta" + ], + "optional": [], + "types": { + "data": "List", + "meta": "Meta" + }, + "kinds": { + "data": "list", + "meta": "model" + } + }, + "FeedConsultantModel": { + "required": [ + "slug", + "name", + "additional_info", + "total_scores", + "profile_photo_url", + "badges" + ], + "optional": [ + "rating_place" + ], + "types": { + "slug": "String", + "name": "String", + "additional_info": "String", + "rating_place": "int", + "total_scores": "int", + "profile_photo_url": "String", + "badges": "List" + }, + "kinds": { + "slug": "string", + "name": "string", + "additional_info": "string", + "rating_place": "number", + "total_scores": "number", + "profile_photo_url": "string", + "badges": "list" + } + }, + "FeedConsultantPaginationModel": { + "required": [ + "data", + "links", + "meta" + ], + "optional": [], + "types": { + "data": "List", + "links": "Links", + "meta": "Meta" + }, + "kinds": { + "data": "list", + "links": "model", + "meta": "model" + } + }, + "FeedGalleryImage": { + "required": [ + "url" + ], + "optional": [ + "orientation", + "id", + "position" + ], + "types": { + "url": "String", + "orientation": "String", + "id": "int", + "position": "int" + }, + "kinds": { + "url": "string", + "orientation": "string", + "id": "number", + "position": "number" + } + }, + "FinishDiaryDayModel": { + "required": [ + "wakeUp", + "retire" + ], + "optional": [ + "comment" + ], + "types": { + "wakeUp": "String", + "retire": "String", + "comment": "String" + }, + "kinds": { + "wakeUp": "string", + "retire": "string", + "comment": "string" + } + }, + "GlossaryModel": { + "required": [ + "id", + "name", + "portion_mass_gr", + "energy_on_100gr", + "proteins_on_100gr", + "fats_on_100gr", + "carbohydrates_on_100gr", + "energy_on_portion", + "proteins_on_portion", + "fats_on_portion", + "carbohydrates_on_portion" + ], + "optional": [], + "types": { + "id": "int", + "name": "String", + "portion_mass_gr": "int", + "energy_on_100gr": "double", + "proteins_on_100gr": "double", + "fats_on_100gr": "double", + "carbohydrates_on_100gr": "double", + "energy_on_portion": "double", + "proteins_on_portion": "double", + "fats_on_portion": "double", + "carbohydrates_on_portion": "double" + }, + "kinds": { + "id": "number", + "name": "string", + "portion_mass_gr": "number", + "energy_on_100gr": "number", + "proteins_on_100gr": "number", + "fats_on_100gr": "number", + "carbohydrates_on_100gr": "number", + "energy_on_portion": "number", + "proteins_on_portion": "number", + "fats_on_portion": "number", + "carbohydrates_on_portion": "number" + } + }, + "GlossaryModerationModel": { + "required": [ + "product_nutritional_value" + ], + "optional": [], + "types": { + "product_nutritional_value": "GlossaryModel" + }, + "kinds": { + "product_nutritional_value": "model" + } + }, + "GlossaryPaginationModel": { + "required": [ + "data" + ], + "optional": [], + "types": { + "data": "List" + }, + "kinds": { + "data": "list" + } + }, + "Ingredient": { + "required": [ + "name", + "unit", + "portion", + "product_nutritional_id" + ], + "optional": [], + "types": { + "name": "String", + "unit": "String", + "portion": "String", + "product_nutritional_id": "int" + }, + "kinds": { + "name": "string", + "unit": "string", + "portion": "string", + "product_nutritional_id": "number" + } + }, + "LegalEntityModel": { + "required": [ + "id", + "type" + ], + "optional": [ + "inn", + "snils", + "state", + "alfa_scoring_status", + "alfa_scoring_request_id", + "last_name", + "first_name", + "middle_name", + "passport_series", + "passport_number", + "passport_issue_date", + "passport_issuer", + "passport_issuer_code", + "birth_date", + "birth_place", + "registration_address", + "residence_address", + "citizenship", + "tax_resident_us", + "foreign_tax_resident", + "sbp_phone", + "sbp_bank_member_id" + ], + "types": { + "id": "int", + "type": "String", + "inn": "String", + "snils": "String", + "state": "String", + "alfa_scoring_status": "AlfaScoringStatus", + "alfa_scoring_request_id": "String", + "last_name": "String", + "first_name": "String", + "middle_name": "String", + "passport_series": "String", + "passport_number": "String", + "passport_issue_date": "String", + "passport_issuer": "String", + "passport_issuer_code": "String", + "birth_date": "String", + "birth_place": "String", + "registration_address": "String", + "residence_address": "String", + "citizenship": "String", + "tax_resident_us": "bool", + "foreign_tax_resident": "bool", + "sbp_phone": "String", + "sbp_bank_member_id": "String" + }, + "kinds": { + "id": "number", + "type": "string", + "inn": "string", + "snils": "string", + "state": "string", + "alfa_scoring_status": "enum", + "alfa_scoring_request_id": "string", + "last_name": "string", + "first_name": "string", + "middle_name": "string", + "passport_series": "string", + "passport_number": "string", + "passport_issue_date": "string", + "passport_issuer": "string", + "passport_issuer_code": "string", + "birth_date": "string", + "birth_place": "string", + "registration_address": "string", + "residence_address": "string", + "citizenship": "string", + "tax_resident_us": "boolean", + "foreign_tax_resident": "boolean", + "sbp_phone": "string", + "sbp_bank_member_id": "string" + } + }, + "Link": { + "required": [], + "optional": [ + "url", + "label", + "active" + ], + "types": { + "url": "String", + "label": "String", + "active": "bool" + }, + "kinds": { + "url": "string", + "label": "string", + "active": "boolean" + } + }, + "Links": { + "required": [ + "first", + "last" + ], + "optional": [ + "prev", + "next" + ], + "types": { + "first": "String", + "last": "String", + "prev": "dynamic", + "next": "dynamic" + }, + "kinds": { + "first": "string", + "last": "string", + "prev": "any", + "next": "any" + } + }, + "LocationModel": { + "required": [ + "locations", + "response_body" + ], + "optional": [], + "types": { + "locations": "List", + "response_body": "String" + }, + "kinds": { + "locations": "list", + "response_body": "string" + } + }, + "MealModel": { + "required": [ + "id", + "time", + "type_no", + "satiety", + "mood", + "portions", + "photos" + ], + "optional": [], + "types": { + "id": "int", + "time": "String", + "type_no": "int", + "satiety": "int", + "mood": "int", + "portions": "List", + "photos": "List" + }, + "kinds": { + "id": "number", + "time": "string", + "type_no": "number", + "satiety": "number", + "mood": "number", + "portions": "list", + "photos": "list" + } + }, + "Meta": { + "required": [ + "current_page", + "last_page", + "links", + "per_page", + "total" + ], + "optional": [ + "from", + "path", + "to", + "total_min_price", + "total_max_price" + ], + "types": { + "current_page": "int", + "from": "dynamic", + "last_page": "int", + "links": "List", + "path": "String", + "per_page": "int", + "to": "dynamic", + "total": "int", + "total_min_price": "int", + "total_max_price": "int" + }, + "kinds": { + "current_page": "number", + "from": "any", + "last_page": "number", + "links": "list", + "path": "string", + "per_page": "number", + "to": "any", + "total": "number", + "total_min_price": "number", + "total_max_price": "number" + } + }, + "MyRecipeModel": { + "required": [ + "id", + "image", + "state", + "cooking_time", + "active_time", + "description_md", + "slug", + "energy_per_serving", + "total_likes", + "total_views" + ], + "optional": [ + "steps", + "video_url", + "title" + ], + "types": { + "id": "int", + "image": "String", + "state": "String", + "cooking_time": "int", + "active_time": "int", + "description_md": "String", + "steps": "List", + "video_url": "String", + "title": "String", + "slug": "String", + "energy_per_serving": "double", + "total_likes": "int", + "total_views": "int" + }, + "kinds": { + "id": "number", + "image": "string", + "state": "string", + "cooking_time": "number", + "active_time": "number", + "description_md": "string", + "steps": "list", + "video_url": "string", + "title": "string", + "slug": "string", + "energy_per_serving": "number", + "total_likes": "number", + "total_views": "number" + } + }, + "MyRecipePaginationModel": { + "required": [ + "data" + ], + "optional": [], + "types": { + "data": "List" + }, + "kinds": { + "data": "list" + } + }, + "NotificationSettingsModel": { + "required": [ + "new_updates_notification", + "weight_measurements_notification", + "is_email_notifications_enabled", + "is_push_notifications_enabled", + "diary_reminder_notifications" + ], + "optional": [], + "types": { + "new_updates_notification": "bool", + "weight_measurements_notification": "bool", + "is_email_notifications_enabled": "bool", + "is_push_notifications_enabled": "bool", + "diary_reminder_notifications": "DiaryReminderNotifications" + }, + "kinds": { + "new_updates_notification": "boolean", + "weight_measurements_notification": "boolean", + "is_email_notifications_enabled": "boolean", + "is_push_notifications_enabled": "boolean", + "diary_reminder_notifications": "model" + } + }, + "NutritionDataModel": { + "required": [ + "dailyNutritionIntake", + "dailyNutritionRecommendation" + ], + "optional": [], + "types": { + "dailyNutritionIntake": "NutritionModel", + "dailyNutritionRecommendation": "NutritionModel" + }, + "kinds": { + "dailyNutritionIntake": "model", + "dailyNutritionRecommendation": "model" + } + }, + "NutritionModel": { + "required": [ + "energy", + "proteins", + "fats", + "carbohydrates" + ], + "optional": [], + "types": { + "energy": "double", + "proteins": "double", + "fats": "double", + "carbohydrates": "double" + }, + "kinds": { + "energy": "number", + "proteins": "number", + "fats": "number", + "carbohydrates": "number" + } + }, + "OAuthAccountModel": { + "required": [ + "oauth_id", + "name", + "email", + "phone", + "linked_at" + ], + "optional": [], + "types": { + "oauth_id": "String", + "name": "String", + "email": "String", + "phone": "String", + "linked_at": "DateTime" + }, + "kinds": { + "oauth_id": "string", + "name": "string", + "email": "string", + "phone": "string", + "linked_at": "unknown" + } + }, + "OAuthAccountsDataModel": { + "required": [], + "optional": [ + "vk", + "yandex" + ], + "types": { + "vk": "OAuthAccountModel", + "yandex": "OAuthAccountModel" + }, + "kinds": { + "vk": "model", + "yandex": "model" + } + }, + "OAuthAccountsResponseModel": { + "required": [ + "success", + "data" + ], + "optional": [], + "types": { + "success": "bool", + "data": "OAuthAccountsDataModel" + }, + "kinds": { + "success": "boolean", + "data": "model" + } + }, + "OAuthAttachRequest": { + "required": [ + "provider", + "access_token", + "refresh_token" + ], + "optional": [ + "client_id" + ], + "types": { + "provider": "String", + "client_id": "String", + "access_token": "String", + "refresh_token": "String" + }, + "kinds": { + "provider": "string", + "client_id": "string", + "access_token": "string", + "refresh_token": "string" + } + }, + "OAuthLoginRequest": { + "required": [ + "client_id", + "access_token", + "refresh_token", + "provider" + ], + "optional": [], + "types": { + "client_id": "String", + "access_token": "String", + "refresh_token": "String", + "provider": "String" + }, + "kinds": { + "client_id": "string", + "access_token": "string", + "refresh_token": "string", + "provider": "string" + } + }, + "OAuthModel": { + "required": [ + "access_token", + "refresh_token", + "first_name", + "last_name", + "date_of_birth", + "client_id", + "provider", + "id", + "email", + "phone", + "gender", + "weight", + "height" + ], + "optional": [], + "types": { + "access_token": "String", + "refresh_token": "String", + "first_name": "String", + "last_name": "String", + "date_of_birth": "String", + "client_id": "String", + "provider": "String", + "id": "String", + "email": "String", + "phone": "String", + "gender": "String", + "weight": "int", + "height": "int" + }, + "kinds": { + "access_token": "string", + "refresh_token": "string", + "first_name": "string", + "last_name": "string", + "date_of_birth": "string", + "client_id": "string", + "provider": "string", + "id": "string", + "email": "string", + "phone": "string", + "gender": "string", + "weight": "number", + "height": "number" + } + }, + "OAuthVerificationConfirmRequest": { + "required": [ + "access_token", + "refresh_token", + "client_id", + "provider", + "id", + "code", + "verification_type", + "first_name", + "last_name", + "date_of_birth", + "gender", + "weight", + "height" + ], + "optional": [ + "email", + "phone" + ], + "types": { + "access_token": "String", + "refresh_token": "String", + "client_id": "String", + "provider": "String", + "id": "String", + "email": "String", + "phone": "String", + "code": "String", + "verification_type": "String", + "first_name": "String", + "last_name": "String", + "date_of_birth": "String", + "gender": "String", + "weight": "int", + "height": "int" + }, + "kinds": { + "access_token": "string", + "refresh_token": "string", + "client_id": "string", + "provider": "string", + "id": "string", + "email": "string", + "phone": "string", + "code": "string", + "verification_type": "string", + "first_name": "string", + "last_name": "string", + "date_of_birth": "string", + "gender": "string", + "weight": "number", + "height": "number" + } + }, + "OAuthVerificationConfirmResponse": { + "required": [ + "success", + "message", + "token", + "user", + "is_new_user" + ], + "optional": [], + "types": { + "success": "bool", + "message": "String", + "token": "String", + "user": "Map", + "is_new_user": "bool" + }, + "kinds": { + "success": "boolean", + "message": "string", + "token": "string", + "user": "map", + "is_new_user": "boolean" + } + }, + "OAuthVerificationRequestResponse": { + "required": [ + "email", + "status" + ], + "optional": [], + "types": { + "email": "String", + "status": "String" + }, + "kinds": { + "email": "string", + "status": "string" + } + }, + "PacketOptions": { + "required": [ + "items", + "short", + "has_personal_diet" + ], + "optional": [ + "price", + "description_md" + ], + "types": { + "items": "List", + "price": "dynamic", + "short": "String", + "description_md": "String", + "has_personal_diet": "bool" + }, + "kinds": { + "items": "list", + "price": "any", + "short": "string", + "description_md": "string", + "has_personal_diet": "boolean" + } + }, + "PlanModel": { + "required": [ + "id", + "slug", + "name", + "image", + "duration", + "state", + "type", + "description_md", + "packets_count", + "author", + "total_likes", + "total_views", + "total_comments", + "is_favorite", + "min_price_of_options", + "max_price_of_options", + "was_liked_by_me" + ], + "optional": [ + "packet1_options", + "packet2_options", + "packet3_options" + ], + "types": { + "id": "int", + "slug": "String", + "name": "String", + "image": "String", + "duration": "int", + "state": "String", + "type": "PlanType", + "description_md": "String", + "packets_count": "int", + "packet1_options": "PacketOptions", + "packet2_options": "PacketOptions", + "packet3_options": "PacketOptions", + "author": "Author", + "total_likes": "int", + "total_views": "int", + "total_comments": "int", + "is_favorite": "bool", + "min_price_of_options": "double", + "max_price_of_options": "double", + "was_liked_by_me": "bool" + }, + "kinds": { + "id": "number", + "slug": "string", + "name": "string", + "image": "string", + "duration": "number", + "state": "string", + "type": "model", + "description_md": "string", + "packets_count": "number", + "packet1_options": "model", + "packet2_options": "model", + "packet3_options": "model", + "author": "model", + "total_likes": "number", + "total_views": "number", + "total_comments": "number", + "is_favorite": "boolean", + "min_price_of_options": "number", + "max_price_of_options": "number", + "was_liked_by_me": "boolean" + } + }, + "PlanPaginationModel": { + "required": [ + "data", + "links", + "meta" + ], + "optional": [], + "types": { + "data": "List", + "links": "Links", + "meta": "Meta" + }, + "kinds": { + "data": "list", + "links": "model", + "meta": "model" + } + }, + "PlanType": { + "required": [], + "optional": [ + "fit", + "slim", + "behavior", + "wellness" + ], + "types": { + "fit": "bool", + "slim": "bool", + "behavior": "bool", + "wellness": "bool" + }, + "kinds": { + "fit": "boolean", + "slim": "boolean", + "behavior": "boolean", + "wellness": "boolean" + } + }, + "PlansFilterState": { + "required": [ + "durationIndex", + "typeIndex" + ], + "optional": [], + "types": { + "durationIndex": "int", + "typeIndex": "int" + }, + "kinds": { + "durationIndex": "number", + "typeIndex": "number" + } + }, + "PlansState": { + "required": [ + "isLoading", + "plans", + "filterState" + ], + "optional": [ + "errorMessage", + "minPlanPrice", + "maxPlanPrice" + ], + "types": { + "isLoading": "bool", + "plans": "List", + "errorMessage": "String", + "filterState": "PlansFilterState", + "minPlanPrice": "int", + "maxPlanPrice": "int" + }, + "kinds": { + "isLoading": "boolean", + "plans": "list", + "errorMessage": "string", + "filterState": "model", + "minPlanPrice": "number", + "maxPlanPrice": "number" + } + }, + "PortionModel": { + "required": [ + "id", + "product_name", + "quantity", + "portion", + "from_diet", + "state" + ], + "optional": [ + "product_nutritional_value_id", + "productCaloriesInfo" + ], + "types": { + "id": "int", + "product_nutritional_value_id": "int", + "productCaloriesInfo": "ProductCaloriesInfoModel", + "product_name": "String", + "quantity": "int", + "portion": "String", + "from_diet": "bool", + "state": "PortionState" + }, + "kinds": { + "id": "number", + "product_nutritional_value_id": "number", + "productCaloriesInfo": "model", + "product_name": "string", + "quantity": "number", + "portion": "string", + "from_diet": "boolean", + "state": "enum" + } + }, + "PostAuthorDetails": { + "required": [ + "slug", + "is_consultant" + ], + "optional": [], + "types": { + "slug": "String", + "is_consultant": "bool" + }, + "kinds": { + "slug": "string", + "is_consultant": "boolean" + } + }, + "PostFeedItemModel": { + "required": [ + "id", + "post_id", + "post_author_id", + "post_author_name", + "post_created_at", + "post_title", + "post_views_count", + "post_likes_count", + "post_comments_count", + "post_is_liked_by_user", + "post_state", + "created_at", + "updated_at" + ], + "optional": [ + "post_author_image_url", + "post_author_profile_url", + "post_short_description", + "post_text_md", + "post_image_url", + "images", + "video_embed_url", + "is_entity_subscribed_by_me", + "post_url", + "post_author_detailz", + "video_url_thumb" + ], + "types": { + "id": "int", + "post_id": "int", + "post_author_id": "int", + "post_author_name": "String", + "post_author_image_url": "String", + "post_author_profile_url": "String", + "post_created_at": "DateTime", + "post_title": "String", + "post_short_description": "String", + "post_text_md": "String", + "post_image_url": "String", + "images": "List", + "video_embed_url": "String", + "post_views_count": "int", + "post_likes_count": "int", + "post_comments_count": "int", + "post_is_liked_by_user": "bool", + "is_entity_subscribed_by_me": "bool", + "post_url": "String", + "post_state": "String", + "post_author_detailz": "PostAuthorDetails", + "video_url_thumb": "String", + "created_at": "DateTime", + "updated_at": "DateTime" + }, + "kinds": { + "id": "number", + "post_id": "number", + "post_author_id": "number", + "post_author_name": "string", + "post_author_image_url": "string", + "post_author_profile_url": "string", + "post_created_at": "unknown", + "post_title": "string", + "post_short_description": "string", + "post_text_md": "string", + "post_image_url": "string", + "images": "list", + "video_embed_url": "string", + "post_views_count": "number", + "post_likes_count": "number", + "post_comments_count": "number", + "post_is_liked_by_user": "boolean", + "is_entity_subscribed_by_me": "boolean", + "post_url": "string", + "post_state": "string", + "post_author_detailz": "model", + "video_url_thumb": "string", + "created_at": "unknown", + "updated_at": "unknown" + } + }, + "PostFeedPaginationModel": { + "required": [ + "data", + "links", + "meta" + ], + "optional": [], + "types": { + "data": "List", + "links": "CursorLinks", + "meta": "CursorMeta" + }, + "kinds": { + "data": "list", + "links": "model", + "meta": "model" + } + }, + "Product": { + "required": [ + "id", + "name" + ], + "optional": [], + "types": { + "id": "int", + "name": "String" + }, + "kinds": { + "id": "number", + "name": "string" + } + }, + "ProductCaloriesInfoModel": { + "required": [ + "id", + "product_name", + "energy_on_100gr", + "proteins_on_100gr", + "fats_on_100gr", + "carbohydrates_on_100gr", + "energy_on_portion", + "proteins_on_portion", + "fats_on_portion", + "carbohydrates_on_portion" + ], + "optional": [], + "types": { + "id": "int", + "product_name": "String", + "energy_on_100gr": "double", + "proteins_on_100gr": "double", + "fats_on_100gr": "double", + "carbohydrates_on_100gr": "double", + "energy_on_portion": "double", + "proteins_on_portion": "double", + "fats_on_portion": "double", + "carbohydrates_on_portion": "double" + }, + "kinds": { + "id": "number", + "product_name": "string", + "energy_on_100gr": "number", + "proteins_on_100gr": "number", + "fats_on_100gr": "number", + "carbohydrates_on_100gr": "number", + "energy_on_portion": "number", + "proteins_on_portion": "number", + "fats_on_portion": "number", + "carbohydrates_on_portion": "number" + } + }, + "ProductInMeal": { + "required": [ + "product_name", + "portion", + "quantity" + ], + "optional": [], + "types": { + "product_name": "String", + "portion": "String", + "quantity": "int" + }, + "kinds": { + "product_name": "string", + "portion": "string", + "quantity": "number" + } + }, + "ProductModerationModel": { + "required": [ + "name", + "energy_on_100gr", + "proteins_on_100gr", + "fats_on_100gr", + "carbohydrates_on_100gr" + ], + "optional": [], + "types": { + "name": "String", + "energy_on_100gr": "double", + "proteins_on_100gr": "double", + "fats_on_100gr": "double", + "carbohydrates_on_100gr": "double" + }, + "kinds": { + "name": "string", + "energy_on_100gr": "number", + "proteins_on_100gr": "number", + "fats_on_100gr": "number", + "carbohydrates_on_100gr": "number" + } + }, + "ProfileDetailsModel": { + "required": [ + "first_name", + "last_name", + "birthdate", + "gender", + "country", + "city", + "lang", + "weight", + "height", + "chest_circumference", + "waist_circumference", + "hip_circumference", + "blood_pressure_up", + "blood_pressure_down", + "has_hypo", + "has_hyper", + "about_alergy", + "about_taking_pils", + "about_sport_hobbies", + "favorite_foods", + "unfavorite_foods", + "additional_info", + "is_mood_fillable", + "is_bedtime_fillable", + "activity_coefficient" + ], + "optional": [ + "created_at", + "updated_at" + ], + "types": { + "first_name": "String", + "last_name": "String", + "birthdate": "String", + "gender": "String", + "country": "String", + "city": "String", + "lang": "String", + "weight": "double", + "height": "int", + "chest_circumference": "int", + "waist_circumference": "int", + "hip_circumference": "int", + "blood_pressure_up": "int", + "blood_pressure_down": "int", + "has_hypo": "bool", + "has_hyper": "bool", + "about_alergy": "String", + "about_taking_pils": "String", + "about_sport_hobbies": "String", + "favorite_foods": "String", + "unfavorite_foods": "String", + "additional_info": "String", + "created_at": "DateTime", + "updated_at": "DateTime", + "is_mood_fillable": "bool", + "is_bedtime_fillable": "bool", + "activity_coefficient": "double" + }, + "kinds": { + "first_name": "string", + "last_name": "string", + "birthdate": "string", + "gender": "string", + "country": "string", + "city": "string", + "lang": "string", + "weight": "number", + "height": "number", + "chest_circumference": "number", + "waist_circumference": "number", + "hip_circumference": "number", + "blood_pressure_up": "number", + "blood_pressure_down": "number", + "has_hypo": "boolean", + "has_hyper": "boolean", + "about_alergy": "string", + "about_taking_pils": "string", + "about_sport_hobbies": "string", + "favorite_foods": "string", + "unfavorite_foods": "string", + "additional_info": "string", + "created_at": "unknown", + "updated_at": "unknown", + "is_mood_fillable": "boolean", + "is_bedtime_fillable": "boolean", + "activity_coefficient": "number" + } + }, + "ProfileModel": { + "required": [ + "id", + "name", + "email", + "phone_confirmation_state", + "is_public_profile", + "is_blocked_by_me", + "is_blocked_by_author", + "is_blocked_by_user", + "is_entity_subscribed_by_me", + "slug", + "profile_photo_url", + "roles", + "badges", + "subscriptions", + "subscribers" + ], + "optional": [ + "rating_place", + "email_verified_at", + "created_at", + "updated_at", + "education_resource", + "cert_photos" + ], + "types": { + "id": "int", + "name": "String", + "email": "String", + "rating_place": "int", + "email_verified_at": "DateTime", + "phone_confirmation_state": "String", + "created_at": "DateTime", + "updated_at": "DateTime", + "is_public_profile": "bool", + "is_blocked_by_me": "bool", + "is_blocked_by_author": "bool", + "is_blocked_by_user": "bool", + "is_entity_subscribed_by_me": "bool", + "slug": "String", + "profile_photo_url": "String", + "roles": "List", + "badges": "List", + "subscriptions": "List", + "subscribers": "List", + "education_resource": "EducationModel", + "cert_photos": "List" + }, + "kinds": { + "id": "number", + "name": "string", + "email": "string", + "rating_place": "number", + "email_verified_at": "unknown", + "phone_confirmation_state": "string", + "created_at": "unknown", + "updated_at": "unknown", + "is_public_profile": "boolean", + "is_blocked_by_me": "boolean", + "is_blocked_by_author": "boolean", + "is_blocked_by_user": "boolean", + "is_entity_subscribed_by_me": "boolean", + "slug": "string", + "profile_photo_url": "string", + "roles": "list", + "badges": "list", + "subscriptions": "list", + "subscribers": "list", + "education_resource": "model", + "cert_photos": "list" + } + }, + "ProfilePreviewModel": { + "required": [ + "profile_photo_url" + ], + "optional": [], + "types": { + "profile_photo_url": "String" + }, + "kinds": { + "profile_photo_url": "string" + } + }, + "ProfileSettingsModel": { + "required": [ + "is_mood_fillable", + "is_bedtime_fillable", + "is_public_profile" + ], + "optional": [ + "new_updates_notifications", + "diary_reminder_notifications" + ], + "types": { + "new_updates_notifications": "dynamic", + "diary_reminder_notifications": "dynamic", + "is_mood_fillable": "bool", + "is_bedtime_fillable": "bool", + "is_public_profile": "bool" + }, + "kinds": { + "new_updates_notifications": "any", + "diary_reminder_notifications": "any", + "is_mood_fillable": "boolean", + "is_bedtime_fillable": "boolean", + "is_public_profile": "boolean" + } + }, + "PromoAppliedListResponse": { + "required": [ + "promo_codes" + ], + "optional": [], + "types": { + "promo_codes": "List" + }, + "kinds": { + "promo_codes": "list" + } + }, + "PromoApplyResponse": { + "required": [ + "success" + ], + "optional": [ + "error", + "promo_code", + "discount_percent", + "discount_amount_rub", + "original_amount_rub", + "final_amount_rub" + ], + "types": { + "success": "bool", + "error": "String", + "promo_code": "PromoCodeModel", + "discount_percent": "int", + "discount_amount_rub": "double", + "original_amount_rub": "double", + "final_amount_rub": "double" + }, + "kinds": { + "success": "boolean", + "error": "string", + "promo_code": "model", + "discount_percent": "number", + "discount_amount_rub": "number", + "original_amount_rub": "number", + "final_amount_rub": "number" + } + }, + "PromoCodeModel": { + "required": [ + "id", + "chosen_plan_id", + "promo_code", + "discount_percent", + "discount_amount_rub", + "state" + ], + "optional": [ + "applied_at", + "confirmed_at", + "cancelled_at" + ], + "types": { + "id": "int", + "chosen_plan_id": "int", + "promo_code": "String", + "discount_percent": "int", + "discount_amount_rub": "double", + "state": "String", + "applied_at": "String", + "confirmed_at": "String", + "cancelled_at": "String" + }, + "kinds": { + "id": "number", + "chosen_plan_id": "number", + "promo_code": "string", + "discount_percent": "number", + "discount_amount_rub": "number", + "state": "string", + "applied_at": "string", + "confirmed_at": "string", + "cancelled_at": "string" + } + }, + "PromoRemoveResponse": { + "required": [ + "success" + ], + "optional": [ + "error" + ], + "types": { + "success": "bool", + "error": "String" + }, + "kinds": { + "success": "boolean", + "error": "string" + } + }, + "PromoValidateResponse": { + "required": [ + "valid" + ], + "optional": [ + "error", + "promo_code", + "discount_percent", + "discount_amount_rub", + "original_amount_rub", + "final_amount_rub" + ], + "types": { + "valid": "bool", + "error": "String", + "promo_code": "String", + "discount_percent": "int", + "discount_amount_rub": "double", + "original_amount_rub": "double", + "final_amount_rub": "double" + }, + "kinds": { + "valid": "boolean", + "error": "string", + "promo_code": "string", + "discount_percent": "number", + "discount_amount_rub": "number", + "original_amount_rub": "number", + "final_amount_rub": "number" + } + }, + "Recipe": { + "required": [ + "id", + "image", + "state", + "cooking_time", + "active_time", + "description_md", + "slug", + "total_comments", + "was_liked_by_me", + "is_favorite", + "energy_per_serving", + "total_likes", + "total_views", + "tags", + "author" + ], + "optional": [ + "ingredients", + "steps", + "video_url", + "created_at", + "updated_at", + "title", + "type", + "servings" + ], + "types": { + "id": "int", + "image": "String", + "state": "String", + "cooking_time": "int", + "active_time": "int", + "ingredients": "List", + "description_md": "String", + "steps": "List", + "video_url": "String", + "created_at": "DateTime", + "updated_at": "DateTime", + "title": "String", + "type": "String", + "slug": "String", + "total_comments": "int", + "was_liked_by_me": "bool", + "is_favorite": "bool", + "energy_per_serving": "double", + "servings": "int", + "total_likes": "int", + "total_views": "int", + "tags": "List", + "author": "Author" + }, + "kinds": { + "id": "number", + "image": "string", + "state": "string", + "cooking_time": "number", + "active_time": "number", + "ingredients": "list", + "description_md": "string", + "steps": "list", + "video_url": "string", + "created_at": "unknown", + "updated_at": "unknown", + "title": "string", + "type": "string", + "slug": "string", + "total_comments": "number", + "was_liked_by_me": "boolean", + "is_favorite": "boolean", + "energy_per_serving": "number", + "servings": "number", + "total_likes": "number", + "total_views": "number", + "tags": "list", + "author": "model" + } + }, + "RecipeCreateEntity": { + "required": [ + "ingredients", + "steps", + "tags" + ], + "optional": [ + "title", + "typeOfRecipe", + "countOfPortions", + "energyInOnePortion", + "totalCookHours", + "totalCookMinutes", + "activeCookHours", + "activeCookMinutes", + "description", + "image", + "videoUrl" + ], + "types": { + "title": "String", + "typeOfRecipe": "TypeOfRecipe", + "countOfPortions": "int", + "energyInOnePortion": "double", + "totalCookHours": "int", + "totalCookMinutes": "int", + "activeCookHours": "int", + "activeCookMinutes": "int", + "description": "String", + "image": "File", + "videoUrl": "String", + "ingredients": "List", + "steps": "List", + "tags": "List" + }, + "kinds": { + "title": "string", + "typeOfRecipe": "enum", + "countOfPortions": "number", + "energyInOnePortion": "number", + "totalCookHours": "number", + "totalCookMinutes": "number", + "activeCookHours": "number", + "activeCookMinutes": "number", + "description": "string", + "image": "unknown", + "videoUrl": "string", + "ingredients": "list", + "steps": "list", + "tags": "list" + } + }, + "RecipeCreateModel": { + "required": [ + "title", + "state", + "description_md", + "type", + "servings", + "energy_per_serving", + "tags", + "ingredients", + "steps" + ], + "optional": [ + "video_url", + "cooking_time", + "active_time" + ], + "types": { + "title": "String", + "state": "String", + "description_md": "String", + "type": "RecipeType", + "video_url": "String", + "cooking_time": "String", + "active_time": "String", + "servings": "String", + "energy_per_serving": "String", + "tags": "List", + "ingredients": "List", + "steps": "List" + }, + "kinds": { + "title": "string", + "state": "string", + "description_md": "string", + "type": "enum", + "video_url": "string", + "cooking_time": "string", + "active_time": "string", + "servings": "string", + "energy_per_serving": "string", + "tags": "list", + "ingredients": "list", + "steps": "list" + } + }, + "RecipeModel": { + "required": [], + "optional": [ + "ingredients", + "steps", + "tags", + "type", + "state", + "title", + "description", + "video_url", + "image", + "totalCookingTime", + "activeCookTime", + "servings", + "energyPerServing" + ], + "types": { + "ingredients": "List", + "steps": "List", + "tags": "List", + "type": "String", + "state": "String", + "title": "String", + "description": "String", + "video_url": "String", + "image": "String", + "totalCookingTime": "int", + "activeCookTime": "int", + "servings": "int", + "energyPerServing": "int" + }, + "kinds": { + "ingredients": "list", + "steps": "list", + "tags": "list", + "type": "string", + "state": "string", + "title": "string", + "description": "string", + "video_url": "string", + "image": "string", + "totalCookingTime": "number", + "activeCookTime": "number", + "servings": "number", + "energyPerServing": "number" + } + }, + "RecipeTag": { + "required": [ + "id", + "name", + "recipes_count", + "created_at", + "updated_at" + ], + "optional": [], + "types": { + "id": "int", + "name": "String", + "recipes_count": "int", + "created_at": "DateTime", + "updated_at": "DateTime" + }, + "kinds": { + "id": "number", + "name": "string", + "recipes_count": "number", + "created_at": "unknown", + "updated_at": "unknown" + } + }, + "RecipeTagModel": { + "required": [ + "data", + "links", + "meta" + ], + "optional": [], + "types": { + "data": "List", + "links": "Links", + "meta": "Meta" + }, + "kinds": { + "data": "list", + "links": "model", + "meta": "model" + } + }, + "RecipesPagination": { + "required": [ + "data" + ], + "optional": [], + "types": { + "data": "List" + }, + "kinds": { + "data": "list" + } + }, + "RecipesState": { + "required": [ + "recipesList", + "recipesTags", + "selectedTags", + "selectedTagsCounter", + "isLoading" + ], + "optional": [ + "errorMessage" + ], + "types": { + "recipesList": "List", + "recipesTags": "List", + "selectedTags": "Set", + "selectedTagsCounter": "int", + "isLoading": "bool", + "errorMessage": "String" + }, + "kinds": { + "recipesList": "list", + "recipesTags": "list", + "selectedTags": "unknown", + "selectedTagsCounter": "number", + "isLoading": "boolean", + "errorMessage": "string" + } + }, + "RegisterConsultantState": { + "required": [ + "consultant", + "acceptOffer", + "loading", + "isUpdating" + ], + "optional": [ + "acceptFrdoAgreement", + "requestModel", + "certPhotos", + "sbpBanks", + "newAddedPhotos", + "deletedPhotoIds", + "alfaScoringStatus", + "isScoringPolling", + "scoringRetryInProgress", + "fieldErrors", + "error" + ], + "types": { + "consultant": "ConsultantProfileModel", + "acceptOffer": "bool", + "acceptFrdoAgreement": "bool", + "loading": "bool", + "isUpdating": "bool", + "requestModel": "ConsultationRequestModel", + "certPhotos": "List", + "sbpBanks": "List", + "newAddedPhotos": "List", + "deletedPhotoIds": "List", + "alfaScoringStatus": "AlfaScoringStatus", + "isScoringPolling": "bool", + "scoringRetryInProgress": "bool", + "fieldErrors": "Map", + "error": "String" + }, + "kinds": { + "consultant": "model", + "acceptOffer": "boolean", + "acceptFrdoAgreement": "boolean", + "loading": "boolean", + "isUpdating": "boolean", + "requestModel": "model", + "certPhotos": "list", + "sbpBanks": "list", + "newAddedPhotos": "list", + "deletedPhotoIds": "list", + "alfaScoringStatus": "enum", + "isScoringPolling": "boolean", + "scoringRetryInProgress": "boolean", + "fieldErrors": "map", + "error": "string" + } + }, + "RegistrationState": { + "required": [ + "sheet" + ], + "optional": [ + "error" + ], + "types": { + "sheet": "UserRegistrationSheet", + "error": "String" + }, + "kinds": { + "sheet": "model", + "error": "string" + } + }, + "RegistrationStateV2": { + "required": [], + "optional": [ + "firstName", + "lastName", + "email", + "phone", + "gender", + "password", + "weight", + "height", + "activityCoefficient", + "birthday", + "isFirstNameValid", + "isLastNameValid", + "isEmailValid", + "isPhoneValid", + "isGenderValid", + "isWeightValid", + "isHeightValid", + "isActivityCoefficientValid", + "isBirthdayValid", + "step", + "emailDisplayError", + "phoneDisplayError", + "dateOfBirthDisplayError", + "registrationError" + ], + "types": { + "firstName": "String", + "lastName": "String", + "email": "String", + "phone": "String", + "gender": "String", + "password": "String", + "weight": "int", + "height": "int", + "activityCoefficient": "double", + "birthday": "DateTime", + "isFirstNameValid": "bool", + "isLastNameValid": "bool", + "isEmailValid": "bool", + "isPhoneValid": "bool", + "isGenderValid": "bool", + "isWeightValid": "bool", + "isHeightValid": "bool", + "isActivityCoefficientValid": "bool", + "isBirthdayValid": "bool", + "step": "int", + "emailDisplayError": "String", + "phoneDisplayError": "String", + "dateOfBirthDisplayError": "String", + "registrationError": "String" + }, + "kinds": { + "firstName": "string", + "lastName": "string", + "email": "string", + "phone": "string", + "gender": "string", + "password": "string", + "weight": "number", + "height": "number", + "activityCoefficient": "number", + "birthday": "unknown", + "isFirstNameValid": "boolean", + "isLastNameValid": "boolean", + "isEmailValid": "boolean", + "isPhoneValid": "boolean", + "isGenderValid": "boolean", + "isWeightValid": "boolean", + "isHeightValid": "boolean", + "isActivityCoefficientValid": "boolean", + "isBirthdayValid": "boolean", + "step": "number", + "emailDisplayError": "string", + "phoneDisplayError": "string", + "dateOfBirthDisplayError": "string", + "registrationError": "string" + } + }, + "ReminderSettings": { + "required": [ + "enabled", + "local_time_hours", + "local_time_mins" + ], + "optional": [], + "types": { + "enabled": "bool", + "local_time_hours": "int", + "local_time_mins": "int" + }, + "kinds": { + "enabled": "boolean", + "local_time_hours": "number", + "local_time_mins": "number" + } + }, + "SbpBankModel": { + "required": [ + "bank_member_id", + "bank_name" + ], + "optional": [ + "short_name", + "logo_url", + "bic" + ], + "types": { + "bank_member_id": "String", + "bank_name": "String", + "short_name": "String", + "logo_url": "String", + "bic": "String" + }, + "kinds": { + "bank_member_id": "string", + "bank_name": "string", + "short_name": "string", + "logo_url": "string", + "bic": "string" + } + }, + "SelectionPopupModel": { + "required": [ + "title" + ], + "optional": [ + "id", + "value", + "isSelected" + ], + "types": { + "id": "int", + "title": "String", + "value": "dynamic", + "isSelected": "bool" + }, + "kinds": { + "id": "number", + "title": "string", + "value": "any", + "isSelected": "boolean" + } + }, + "SocialLinksModel": { + "required": [], + "optional": [ + "vk_link", + "tg_link", + "fb_link", + "insta_link", + "personal_site_link" + ], + "types": { + "vk_link": "String", + "tg_link": "String", + "fb_link": "String", + "insta_link": "String", + "personal_site_link": "String" + }, + "kinds": { + "vk_link": "string", + "tg_link": "string", + "fb_link": "string", + "insta_link": "string", + "personal_site_link": "string" + } + }, + "Step": { + "required": [], + "optional": [ + "image", + "description" + ], + "types": { + "image": "String", + "description": "String" + }, + "kinds": { + "image": "string", + "description": "string" + } + }, + "StepEntity": { + "required": [], + "optional": [ + "description", + "image" + ], + "types": { + "description": "String", + "image": "File" + }, + "kinds": { + "description": "string", + "image": "unknown" + } + }, + "SubModel": { + "required": [ + "slug", + "name", + "city", + "birthdate", + "is_entity_subscribed_by_me", + "profile_photo_url", + "roles" + ], + "optional": [], + "types": { + "slug": "String", + "name": "String", + "city": "String", + "birthdate": "DateTime", + "is_entity_subscribed_by_me": "bool", + "profile_photo_url": "String", + "roles": "List" + }, + "kinds": { + "slug": "string", + "name": "string", + "city": "string", + "birthdate": "unknown", + "is_entity_subscribed_by_me": "boolean", + "profile_photo_url": "string", + "roles": "list" + } + }, + "SubPaginationModel": { + "required": [ + "data", + "links", + "meta" + ], + "optional": [], + "types": { + "data": "List", + "links": "Links", + "meta": "Meta" + }, + "kinds": { + "data": "list", + "links": "model", + "meta": "model" + } + }, + "Tag": { + "required": [ + "name" + ], + "optional": [], + "types": { + "name": "String" + }, + "kinds": { + "name": "string" + } + }, + "TokenModel": { + "required": [ + "token" + ], + "optional": [], + "types": { + "token": "String" + }, + "kinds": { + "token": "string" + } + }, + "TypeOfDiaryModel": { + "required": [ + "has_chosen_diet", + "has_chosen_plan" + ], + "optional": [ + "last_state" + ], + "types": { + "has_chosen_diet": "bool", + "has_chosen_plan": "bool", + "last_state": "DiaryStateEnum" + }, + "kinds": { + "has_chosen_diet": "boolean", + "has_chosen_plan": "boolean", + "last_state": "enum" + } + }, + "UserRegistrationSheet": { + "required": [ + "first_name", + "last_name", + "email", + "password", + "password_confirmation", + "birthdate", + "gender", + "location", + "response_body", + "lang", + "weight", + "height", + "phone", + "chest_circumference", + "waist_circumference", + "hip_circumference", + "blood_pressure_up", + "blood_pressure_down", + "has_hypo", + "has_hyper", + "about_alergy", + "about_taking_pils", + "about_sport_hobbies", + "favorite_foods", + "unfavorite_foods", + "additional_info" + ], + "optional": [ + "terms", + "activity_coefficient" + ], + "types": { + "first_name": "String", + "last_name": "String", + "email": "String", + "password": "String", + "password_confirmation": "String", + "terms": "String", + "birthdate": "String", + "gender": "String", + "location": "String", + "response_body": "String", + "lang": "String", + "weight": "int", + "height": "int", + "phone": "String", + "chest_circumference": "int", + "waist_circumference": "int", + "hip_circumference": "int", + "blood_pressure_up": "int", + "blood_pressure_down": "int", + "has_hypo": "int", + "has_hyper": "int", + "about_alergy": "String", + "about_taking_pils": "String", + "about_sport_hobbies": "String", + "favorite_foods": "String", + "unfavorite_foods": "String", + "additional_info": "String", + "activity_coefficient": "double" + }, + "kinds": { + "first_name": "string", + "last_name": "string", + "email": "string", + "password": "string", + "password_confirmation": "string", + "terms": "string", + "birthdate": "string", + "gender": "string", + "location": "string", + "response_body": "string", + "lang": "string", + "weight": "number", + "height": "number", + "phone": "string", + "chest_circumference": "number", + "waist_circumference": "number", + "hip_circumference": "number", + "blood_pressure_up": "number", + "blood_pressure_down": "number", + "has_hypo": "number", + "has_hyper": "number", + "about_alergy": "string", + "about_taking_pils": "string", + "about_sport_hobbies": "string", + "favorite_foods": "string", + "unfavorite_foods": "string", + "additional_info": "string", + "activity_coefficient": "number" + } + }, + "Users": { + "required": [ + "from_chat_user", + "to_chat_user" + ], + "optional": [], + "types": { + "from_chat_user": "ChatUser", + "to_chat_user": "ChatUser" + }, + "kinds": { + "from_chat_user": "model", + "to_chat_user": "model" + } + }, + "VideoCallAcceptResponse": { + "required": [ + "sent", + "devices_notified" + ], + "optional": [], + "types": { + "sent": "bool", + "devices_notified": "int" + }, + "kinds": { + "sent": "boolean", + "devices_notified": "number" + } + }, + "VideoCallAdminTokenResponse": { + "required": [ + "adminToken", + "roomName", + "apiUrl" + ], + "optional": [], + "types": { + "adminToken": "String", + "roomName": "String", + "apiUrl": "String" + }, + "kinds": { + "adminToken": "string", + "roomName": "string", + "apiUrl": "string" + } + }, + "VideoCallEndedResponse": { + "required": [ + "sent" + ], + "optional": [], + "types": { + "sent": "bool" + }, + "kinds": { + "sent": "boolean" + } + }, + "VideoCallSessionState": { + "required": [], + "optional": [ + "conversationUuid", + "counterpartName", + "counterpartIsConsultant", + "localParticipantName", + "micEnabled", + "videoEnabled", + "splitParticipantView", + "mainStageShowsLocalVideo", + "useFrontCamera", + "connectError", + "uiMode", + "connectPhase", + "linkIssue", + "callStageEntered", + "entryMode" + ], + "types": { + "conversationUuid": "String", + "counterpartName": "String", + "counterpartIsConsultant": "bool", + "localParticipantName": "String", + "micEnabled": "bool", + "videoEnabled": "bool", + "splitParticipantView": "bool", + "mainStageShowsLocalVideo": "bool", + "useFrontCamera": "bool", + "connectError": "String", + "uiMode": "VideoCallUiMode", + "connectPhase": "VideoCallConnectPhase", + "linkIssue": "VideoCallLinkIssue", + "callStageEntered": "bool", + "entryMode": "VideoCallEntryMode" + }, + "kinds": { + "conversationUuid": "string", + "counterpartName": "string", + "counterpartIsConsultant": "boolean", + "localParticipantName": "string", + "micEnabled": "boolean", + "videoEnabled": "boolean", + "splitParticipantView": "boolean", + "mainStageShowsLocalVideo": "boolean", + "useFrontCamera": "boolean", + "connectError": "string", + "uiMode": "enum", + "connectPhase": "enum", + "linkIssue": "enum", + "callStageEntered": "boolean", + "entryMode": "enum" + } + }, + "VideoCallTokenResponse": { + "required": [ + "token", + "roomName", + "wsUrl" + ], + "optional": [], + "types": { + "token": "String", + "roomName": "String", + "wsUrl": "String" + }, + "kinds": { + "token": "string", + "roomName": "string", + "wsUrl": "string" + } + }, + "WebsocketInfoModel": { + "required": [ + "key", + "host", + "port", + "cluster", + "user_id" + ], + "optional": [], + "types": { + "key": "String", + "host": "String", + "port": "String", + "cluster": "String", + "user_id": "int" + }, + "kinds": { + "key": "string", + "host": "string", + "port": "string", + "cluster": "string", + "user_id": "number" + } + }, + "WeightModel": { + "required": [ + "id", + "user_id", + "weight", + "created_at", + "updated_at", + "bmi" + ], + "optional": [], + "types": { + "id": "int", + "user_id": "int", + "weight": "int", + "created_at": "DateTime", + "updated_at": "DateTime", + "bmi": "double" + }, + "kinds": { + "id": "number", + "user_id": "number", + "weight": "number", + "created_at": "unknown", + "updated_at": "unknown", + "bmi": "number" + } + } +} diff --git a/tool/export_api_contract.dart b/tool/export_api_contract.dart new file mode 100644 index 00000000..72321e8c --- /dev/null +++ b/tool/export_api_contract.dart @@ -0,0 +1,223 @@ +// FILE: tool/export_api_contract.dart +// VERSION: 1.0.0 +// PURPOSE: Экспорт контракта мобильных моделей для бэкенд-репозитория (eatvio-web). +// +// Обходит lib/**/*.dart, находит @freezed фабрики (const factory XModel({...})) +// и извлекает JSON-контракт каждой модели: какие ключи обязательны (required), +// а какие допускают null/отсутствие. +// +// Источник истины — исходные декларации, НЕ сгенерированные *.g.dart: +// nullability = "?" в типе Dart; отсутствие ключа безопасно при @Default(...); +// JSON-имя = @JsonKey(name: "...") либо имя поля (дефолт json_serializable). +// +// USAGE: +// dart run tool/export_api_contract.dart # манифест в stdout +// dart run tool/export_api_contract.dart out.json # манифест в файл +// +// CI (flutter-репо): регенерить и сравнивать с закоммиченной копией; +// diff → fail с требованием обновить манифест и синхронизировать его +// в eatvio-web: tests/Fixtures/Contract/flutter_model_contract.json + +import 'dart:convert'; +import 'dart:io'; + +void main(List args) { + final root = Directory('lib'); + if (!root.existsSync()) { + stderr.writeln('Run from the flutter app root (directory "lib" not found).'); + exit(2); + } + + final sources = {}; + final enumNames = {}; + + for (final file in root.listSync(recursive: true).whereType()) { + if (!file.path.endsWith('.dart') || file.path.endsWith('.g.dart') || file.path.endsWith('.freezed.dart')) { + continue; + } + final source = file.readAsStringSync(); + sources[file] = source; + enumNames.addAll( + RegExp(r'^\s*enum\s+(\w+)', multiLine: true) + .allMatches(source) + .map((m) => m.group(1)!), + ); + } + + final contract = >{}; + for (final source in sources.values) { + contract.addAll(extractModels(source)); + } + + // kinds считаются второй проходкой: нужны все имена моделей (contract.keys) и enum'ов. + final modelNames = contract.keys.toSet(); + for (final entry in contract.entries) { + final types = (entry.value['types'] ?? {}) as Map; + entry.value['kinds'] = { + for (final e in types.entries) e.key: jsonKindFor(e.value, modelNames, enumNames), + }; + } + + final sorted = >{ + for (final key in contract.keys.toList()..sort()) key: contract[key]!, + }; + final encoder = JsonEncoder.withIndent(' '); + final output = encoder.convert(sorted); + + if (args.isNotEmpty) { + File(args.first).writeAsStringSync('$output\n'); + stdout.writeln('Contract written to ${args.first} (${sorted.length} models)'); + } else { + stdout.writeln(output); + } +} + +/// JSON-вид значения для Dart-типа — по нему бэкенд-чекер валидирует payload: +/// number (int/double), string, boolean, list, map, model (JSON-объект), +/// enum (строка-имя или int), any (не проверяем). +String jsonKindFor(String dartType, Set modelNames, Set enumNames) { + final type = dartType.trim(); + if (type == 'dynamic' || type == 'Object') { + return 'any'; + } + if (type.startsWith('List<')) { + return 'list'; + } + if (type.startsWith('Map<')) { + return 'map'; + } + if (const {'int', 'double', 'num'}.contains(type)) { + return 'number'; + } + if (type == 'String') { + return 'string'; + } + if (type == 'bool') { + return 'boolean'; + } + if (enumNames.contains(type)) { + return 'enum'; + } + if (modelNames.contains(type)) { + return 'model'; + } + return 'unknown'; +} + +/// Извлекает все @freezed-модели из исходника. +/// Возвращает {ModelName: {required: [...], optional: [...]}}. +Map> extractModels(String source) { + final result = >{}; + + // Блоки "const factory ModelName({ ... }) = _ModelName[...];" (одно- и многострочные). + final factoryRe = RegExp(r'const\s+factory\s+(\w+)\(\s*\{(.*?)\}\s*\)\s*=', dotAll: true); + for (final match in factoryRe.allMatches(source)) { + final modelName = match.group(1)!; + final body = match.group(2)!; + + final required = []; + final optional = []; + final types = {}; + + for (final rawLine in _splitTopLevel(body)) { + final line = rawLine.trim(); + if (line.isEmpty || line.startsWith('//')) { + continue; + } + + final field = _parseField(line); + if (field == null) { + continue; + } + + (field.required ? required : optional).add(field.jsonName); + types[field.jsonName] = field.dartType; + } + + if (required.isNotEmpty || optional.isNotEmpty) { + result[modelName] = { + 'required': required, + 'optional': optional, + 'types': types, + }; + } + } + + return result; +} + +/// Разбивает тело фабрики по запятым верхнего уровня, +/// не разрывая дженерики (List) и вложенные скобки. +List _splitTopLevel(String body) { + final parts = []; + final current = StringBuffer(); + var angleDepth = 0; + var parenDepth = 0; + var braceDepth = 0; + + for (final char in body.runes) { + final ch = String.fromCharCode(char); + if (ch == '<') { + angleDepth++; + } else if (ch == '>') { + angleDepth = (angleDepth - 1).clamp(0, 1 << 30); + } else if (ch == '(') { + parenDepth++; + } else if (ch == ')') { + parenDepth = (parenDepth - 1).clamp(0, 1 << 30); + } else if (ch == '{') { + braceDepth++; + } else if (ch == '}') { + braceDepth = (braceDepth - 1).clamp(0, 1 << 30); + } else if (ch == ',' && angleDepth == 0 && parenDepth == 0 && braceDepth == 0) { + parts.add(current.toString()); + current.clear(); + + continue; + } + current.write(ch); + } + parts.add(current.toString()); + + return parts; +} + +class _Field { + _Field(this.jsonName, this.required, this.dartType); + + final String jsonName; + final bool required; + final String dartType; +} + +_Field? _parseField(String line) { + // Убираем комментарии в конце строки. + final code = line.split('//').first.trim(); + if (code.isEmpty) { + return null; + } + + // JSON-имя: @JsonKey(name: "type_no") или имя поля (одинарные и двойные кавычки). + final jsonKeyMatch = + RegExp(r'''@JsonKey\([^)]*?\bname:\s*["']([^"']+)["']''').firstMatch(code); + // Убираем аннотации, оставляем "модификаторы тип поле". + final declaration = code.replaceAll(RegExp(r'@\w+\([^)]*\)'), '').trim(); + + final fieldMatch = RegExp(r'^(?:(?:required|final|const)\s+)*([\w<>?,\s]+?)\s+(\w+)$').firstMatch(declaration); + if (fieldMatch == null) { + return null; + } + + final type = fieldMatch.group(1)!.trim(); + final dartName = fieldMatch.group(2)!; + + // Метаданные исходной строки. + final hasDefault = RegExp(r'@Default\(').hasMatch(line); + final isNullable = type.endsWith('?') || type == 'dynamic'; + + return _Field( + jsonKeyMatch?.group(1) ?? dartName, + !isNullable && !hasDefault, + type.endsWith('?') ? type.substring(0, type.length - 1) : type, + ); +}