Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 58 additions & 41 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,54 +12,66 @@ HTTP API, протокол HTTP, Postman, js-playwright. Уроки и само
## Как собран сервер

Четыре спецификации на TypeSpec (`typespec/<app>/`) компилируются в OpenAPI
(`make compile` → `tsp-output/`). Три из них отдаёт мок prism, а спецификацию
`http-api` целиком обслуживает приложение на fastify (`custom-server/`): мок
возвращает пример дословно и запрос не разбирает, а уроки курса HTTP API учат
как раз на `skip`, `limit`, `select` и идентификаторе в пути.
(`make compile` → `tsp-output/`), по одной на курс: `http-api`, `http-protocol`,
`js-playwright`, `postman`. Все четыре целиком обслуживает приложение на fastify
(`custom-server/`). Мока prism больше нет ни у одной: он возвращал пример
дословно и запрос не разбирал, а уроки учат как раз на `skip`, `limit`, `select`
и идентификаторе в пути.

Маршруты, валидация запроса и проверка авторизации при этом **не написаны
руками**. Их строит из той же спецификации `fastify-openapi-glue`
(`custom-server/src/http-api.ts`), а типы обработчиков и моделей генерирует
(`custom-server/src/openapi-spec.ts`, плагин регистрируется по разу на каждую из
четырёх), а типы обработчиков и моделей генерирует
`@hey-api/openapi-ts` (`make generate` → `custom-server/src/generated/`).
Написанного руками остаётся ровно столько, сколько в OpenAPI не выражается: сами
данные, выборка страницы и полей, `/http-api/rpc`, `/http-api/echo`, стрим и
куки для курса про протокол, статика.
данные, выборка страницы и полей, `/http-api/rpc`, `/http-api/echo`,
`/http-protocol/login`, стрим и куки для курса про протокол, статика.

Обработчики одни на все четыре спецификации: имена интерфейсов в них совпадают,
значит совпадают и `operationId`, а `js-playwright` просто подмножество, у него
нет постов и комментариев. Лишние ключи glue не смотрит, а отсутствующий
обработчик находит на старте и падает громко. Типы генерируются по `http-api`,
как по самой полной.

Снаружи всё сшивает Caddy на `$PORT`.

```text
Caddy :$PORT
├── :4010 fastify весь http-api, rpc, echo, стрим, куки, статика, swagger-ui
├── :4012 prism http-protocol
├── :4013 prism js-playwright
└── :4014 prism postman
└── :4010 fastify все четыре спецификации, rpc, echo, стрим, куки,
статика, swagger-ui
```

Список приложений и путь к документации задаёт `app.config.json`: имя приложения
плюс `docRoute` дают маршрут вида `/http-api-openapi`.

В `Caddyfile` два разных механизма, и разница между ними решает, какой путь
увидит бэкенд. Директива `handle_path /http-protocol/*` **срезает** префикс,
поэтому prism получает `/tasks`. Директива `handle /http-api/*` префикс
оставляет, поэтому приложение регистрирует полные пути. Префикс приложению
подставляет не Caddy, а опция `prefix` у glue: в спецификации он вынесен в
`servers` (`@server("/http-api")`), а сам glue `servers` не применяет.
В `Caddyfile` везде `handle`, а не `handle_path`: префикс не срезается, и
приложение регистрирует полные пути. Подставляет его не Caddy, а опция `prefix`
у glue: в спецификации префикс вынесен в `servers` (`@server("/http-api")`), а
сам glue `servers` не применяет.

Отдельно в `Caddyfile` стоят четыре `redir` на адреса документации со слешем на
конце. Причина не в Caddy: `swagger-ui` регистрирует на этот путь и `GET`, и
и `HEAD`, который добавляет fastify, а `fastify-allow` запоминает последний
метод, поэтому в `Allow` остаётся только `HEAD`, а браузерный `GET` получает 405.
Ссылка со слешем живая: урок `crud` курса http-api ведёт на
`/http-api-openapi/#/Tasks`, а фрагмент после решётки до сервера не доходит.

## У `http-api` мока нет, у остальных трёх он динамический
## Мока prism больше нет

В `bin/start.sh` подняты три мока, и все с флагом `-d`. Спецификации `http-api`
среди них нет.
`bin/start.sh` поднимает приложение и Caddy, и больше ничего. Зависимость на
`@stoplight/prism-cli` снята.

Динамический режим генерирует данные faker'ом: тот не смотрит на объявленный
тип, поэтому при `uint16` (0..65535) выдавал `"id":-39177245`, а ответы менялись
на каждый запрос. Статичный режим отдаёт примеры из спецификации дословно, то
есть на любой `/tasks/{id}` приходит одна и та же запись, а `skip` и `limit`
возвращаются такими, как записаны в примере. Уроки курса http-api не работают ни
на том, ни на другом, поэтому эта спецификация ушла в приложение целиком.
Ответ приходил без разбора запроса, и режимов было два, оба негодные.
Динамический генерировал данные faker'ом: тот не смотрит на объявленный тип,
поэтому при `uint16` (0..65535) выдавал `"id":-39177245`, а ответы менялись на
каждый запрос. Статичный отдавал примеры дословно, то есть на любой `/tasks/{id}`
приходила одна и та же запись, а `skip` и `limit` возвращались такими, как
записаны в примере.

Три остальные остаются динамическими, потому что примеров в них нет: статичный
режим отдал бы вместо данных заглушки вида `"string"`. Чтобы перевести любую из
них на статику, сначала заводят `@example` во всех её моделях.
Уроки всех четырёх курсов учат ровно на том, чего мок не умеет. В курсе Postman
это было особенно заметно: урок `200-complex-requests` просит `?limit=2` и
получал четыре записи с `limit` равным 10928880, а `/users/3` отдавал
пользователя 52956988 с латинским именем.

## Спецификация задаёт поведение, а не описывает его

Expand Down Expand Up @@ -120,7 +132,8 @@ Caddy :$PORT

## Коды ответов несут уроки

На этих кодах построены самостоятельные работы:
На этих кодах построены самостоятельные работы. Таблица действует на все четыре
спецификации, различия между ними только в том, какие операции чем закрыты:

| Запрос | Код | Кто отдаёт |
| --- | --- | --- |
Expand All @@ -130,6 +143,7 @@ Caddy :$PORT
| `?skip=-1` | 422 | формат `uint16`, объявленный в `index.js` |
| `POST /posts` без токена | 401 | `securityHandlers` по `@useAuth` |
| `GET /courses` без ключа | 401 | `securityHandlers` по `@useAuth` |
| `GET /postman/tasks/1` без Basic | 401 | `securityHandlers` по `@useAuth` |
| `GET /tasks/999` | 404 | обработчик в `handlers.ts` |
| `POST /posts` с токеном | 201 | обработчик в `handlers.ts` |
| `DELETE /tasks/1` | 204 | обработчик в `handlers.ts` |
Expand All @@ -143,14 +157,11 @@ Caddy :$PORT

## Версии прибиты не для порядка

`@stoplight/prism-cli` стоит **точной** версией `5.14.2`: начиная с 5.15 в
ответы дописываются случайные поля, которых нет в схеме, и флаг
`--json-schema-faker-fillProperties=false` этого не снимает. Для учебного сервера
это мусор в примерах уроков.

`--multiprocess=false` тоже обязателен: многопроцессный режим prism обращается к
`cluster.default.isPrimary`, которого на текущих node нет, и контейнер падает на
старте.
Зависимости на prism больше нет. Если мок когда-нибудь вернут, с ним вернутся и
две прибитые мины: версия нужна точная, `5.14.2`, потому что с 5.15 он дописывает
в ответы случайные поля вне схемы, и обязателен `--multiprocess=false`, потому что
многопроцессный режим обращается к `cluster.default.isPrimary`, которого на
текущих node нет, и контейнер падает на старте.

`@typespec/*` держатся диапазоном, а не `latest`. На `latest` очередной релиз
сломал сборку молча: в 1.x пакеты `versioning` и `openapi` выделены в отдельные,
Expand All @@ -160,9 +171,15 @@ Caddy :$PORT

`make test` сначала перегенерирует типы и падает, если сгенерированное в
репозитории отстало от спецификации, затем поднимает приложение и проверяет
ответы запросами:
совпадение данных REST и RPC, коды из таблицы выше, границы `uint16`. Нужна
скомпилированная спецификация, то есть сначала `make setup` либо `make compile`.
ответы запросами по всем четырём префиксам: пагинацию, `select`, отбор по пути,
вложенные ресурсы, состав коллекций, схемы авторизации, коды из таблицы выше,
границы `uint16`, совпадение данных REST и RPC и совпадение примеров
спецификации с наборами. Нужна скомпилированная спецификация, то есть сначала
`make setup` либо `make compile`.

Устаревший `tsp-output/` виден именно здесь: `check-generated` перегенерирует
типы из него и падает на `git diff`, хотя спецификации в репозитории не менялись.
Лечится `make compile`.

Caddy в прогоне не участвует, в раннере его нет: порты опрашиваются напрямую, как
это делает Caddy со срезанным префиксом. Сборка гоняется и на пуллреквестах,
Expand Down
49 changes: 19 additions & 30 deletions Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
# Апстримы указаны адресом, а не именем localhost: и prism, и приложение слушают
# только IPv4 (0.0.0.0), а localhost резолвится ещё и в ::1, где не слушает никто.

# Документация со слешем на конце отвечает 405, а не страницей: swagger-ui
# регистрирует и GET, и добавленный фастифаем HEAD на один путь, а fastify-allow
# запоминает по маршруту последний метод, и в Allow остаётся только HEAD. Ссылка
# со слешем живая, урок crud курса http-api ведёт на /http-api-openapi/#/Tasks,
# поэтому адрес нормализуется здесь.
redir /http-api-openapi/ /http-api-openapi 301
redir /http-protocol-openapi/ /http-protocol-openapi 301
redir /js-playwright-openapi/ /js-playwright-openapi 301
redir /postman-openapi/ /postman-openapi 301

# http-api course
# Весь курс обслуживает приложение, мока у этой спецификации больше нет. Мок
# отдавал пример дословно, поэтому не применял skip, limit и select и не отбирал
Expand All @@ -19,58 +29,37 @@ handle /http-api/* {
}

# http-protocol course
handle /http-protocol/example {
reverse_proxy 127.0.0.1:4010
}

handle /http-protocol/login {
reverse_proxy 127.0.0.1:4010
}

handle /http-protocol/stream {
reverse_proxy 127.0.0.1:4010
}

handle /http-protocol/removed {
# Мока у этой спецификации больше нет, весь префикс обслуживает приложение.
# Отдельный блок на /http-protocol нужен потому, что путь без слеша под
# /http-protocol/* не попадает: по нему отдаётся страница урока.
handle /http-protocol-openapi* {
reverse_proxy 127.0.0.1:4010
}

handle /http-protocol {
reverse_proxy 127.0.0.1:4010
}

handle /http-protocol-openapi* {
handle /http-protocol/* {
reverse_proxy 127.0.0.1:4010
}

handle_path /http-protocol/* {
reverse_proxy 127.0.0.1:4012
}

# js-playwright course
handle /js-playwright/users-list {
reverse_proxy 127.0.0.1:4010
}

handle /js-playwright-openapi* {
reverse_proxy 127.0.0.1:4010
}

handle_path /js-playwright/* {
reverse_proxy 127.0.0.1:4013
}

# postman course
handle /postman/cookie {
handle /js-playwright/* {
reverse_proxy 127.0.0.1:4010
}

# postman course
handle /postman-openapi* {
reverse_proxy 127.0.0.1:4010
}

handle_path /postman/* {
reverse_proxy 127.0.0.1:4014
handle /postman/* {
reverse_proxy 127.0.0.1:4010
}

# js-dom-testing-library course
Expand Down
Loading