@etc-utils/typespec-amqp-ws
v1.0.8
Published
TypeSpec emitter for AsyncAPI 3.0 covering AMQP (RabbitMQ) and WebSocket transports
Maintainers
Readme
@etc-utils/typespec-amqp-ws
TypeSpec-эмиттер для генерации AsyncAPI 3.0 спецификаций из лаконичных TypeSpec-описаний. Поддерживает два транспорта:
- AMQP (RabbitMQ): publish в exchange, consume из queue, типы exchange
directиfanout. - WebSocket: единый канал
/, дискриминатор сообщений через literal-типы, request/reply, бинарные сообщения.
Эмиттер сознательно ограничен — поддерживает только то подмножество AsyncAPI, которое реально применяется на практике в типичных микросервисах с RabbitMQ и WebSocket. Это упрощает реализацию, валидацию и сопровождение.
Установка
npm install -D @etc-utils/typespec-amqp-ws @typespec/compiler @typespec/asset-emitterТребования: Node.js 22+.
Конфигурация
В tspconfig.yaml сервиса выбирается один из двух emit-таргетов:
# Для сервиса с AMQP/RabbitMQ:
emit:
- "@etc-utils/typespec-amqp-ws/amqp"
options:
"@etc-utils/typespec-amqp-ws/amqp":
file-type: yaml # yaml | json. Default: yaml
output-file: "asyncapi.yaml"
new-line: "lf" # lf | crlf. Default: lf
output-dir: "{project-root}/tsp-output"# Для сервиса с WebSocket-API:
emit:
- "@etc-utils/typespec-amqp-ws/ws"
output-dir: "{project-root}/tsp-output"Запуск:
tsp compile .Полученный asyncapi.yaml можно скармливать @asyncapi/cli для генерации моделей на Go/TypeScript и @redocly/cli для документации.
Полный AMQP-пример
import "@etc-utils/typespec-amqp-ws";
using TspAsyncApi;
using TspAsyncApi.Amqp;
@service(#{ title: "Notifications Service" })
@info(#{
version: "1.0.0",
description: "Сервис рассылки уведомлений через RabbitMQ",
})
@server("rabbit", #{
host: "rabbit.example.com:5672",
pathname: "/notifications",
protocol: "amqp",
description: "RabbitMQ-сервер",
})
namespace Notifications;
@doc("Уведомление пользователю")
model Notification {
@doc("Идентификатор уведомления")
notificationId: string;
@doc("Текст уведомления")
text: string;
}
// PUBLISH: payload в параметре операции, returnType — `void` (без ответа).
@publish(#{
routingKey: "notifications.created",
exchange: #{
name: "notifications-exchange",
type: "direct",
durable: true,
},
})
@summary("Опубликовать новое уведомление")
op sendNotification(notification: Notification): void;
// CONSUME: чтение из именованной очереди — сообщение в returnType.
@consume(#{
routingKey: "notifications.acknowledge",
queue: #{
name: "notifications-ack-queue",
durable: true,
autoDelete: false,
},
})
@summary("Обработать подтверждение доставки")
op handleAck(): Notification;Результат компиляции — валидный AsyncAPI 3.0 YAML с двумя каналами, операциями и AMQP-биндингами.
Полный WebSocket-пример
import "@etc-utils/typespec-amqp-ws";
using TspAsyncApi;
using TspAsyncApi.WebSocket;
@service(#{ title: "Chat WS" })
@info(#{ version: "1.0.0" })
@server("public", #{
host: "localhost:{port}",
protocol: "ws",
pathname: "/chat",
variables: #{
port: #{ `default`: "8080", description: "Порт WS-сервера" },
},
})
namespace Chat;
// Дискриминатор сообщения — обычное поле литерального типа.
// Эмиттер выведет { type: "string", const: "userJoined" } в JSON Schema.
model UserJoined {
eventType: "userJoined";
msgUid: string;
userId: string;
nickname: string;
}
model SendMessage {
eventType: "sendMessage";
msgUid: string;
text: string;
}
model SendMessageResponse {
eventType: "sendMessageResponse";
msgUid: string;
ok: boolean;
}
// Receive: returnType — принимаемое сообщение
@consume
@summary("Пользователь подключился к чату")
op userJoined(): UserJoined;
// Request/reply: параметр — отправляемое сообщение, returnType — ответ
@publish
@summary("Отправить сообщение в чат")
op sendMessage(request: SendMessage): SendMessageResponse;Все WebSocket-операции автоматически складываются на единый канал /. Это типичный паттерн WebSocket-API, где дискриминация сообщений происходит через literal-поле модели (например, eventType).
Поддерживаемые TypeSpec-типы
| TypeSpec | JSON Schema | Кодген (Go / TS / C++) |
|---|---|---|
| string | { type: string } | string / string / std::string |
| boolean | { type: boolean } | bool / boolean / bool |
| integer | { type: integer } | int32 / number / int |
| bytes | { type: string, format: binary } | []byte / Uint8Array / std::vector<uint8_t> |
| enum X { A, B } | { type: string, enum: [A, B] } | string-typedef |
| scalar X extends string | именованная схема { type: string } | string-typedef |
| model X { ... } | { type: object, properties, required, additionalProperties: false } | struct/interface |
| T[] | { type: array, items: <T> } | slice/array |
| Record<T> | { type: object, additionalProperties: <T> } | map[string]T / Record<string, T> |
| literal "foo" | { type: string, const: "foo" } | константа |
| literal 1 (integer) | { type: integer, const: 1 } | константа (например, маркер версии) |
| literal 1.5 (не integer) | { type: string, const: "1.5" } | деградация — float'ы через API мы не передаём |
| literal true / false | { type: boolean, const: <value> } | константа |
| T \| null | { type: [<base>, null] } | pointer / nullable |
| foo?: T | поле не входит в required | опциональное поле |
Запрещённые типы
Эти типы вызывают ошибку компиляции — намеренно, ради переносимости между языками:
| Тип | Почему запрещён |
|---|---|
| int8/int16/int32, uint8/uint16/uint32 | Codegen всё равно генерирует signed int32 / number для всех ширин. Размер-специфичные типы создают ложное ожидание сохранения семантики. Используйте integer. |
| int64/uint64 | 64-битные числа должны передаваться как string (JS не умеет точно представлять 64-битные числа в JSON). Поясните формат в @doc. |
| float32/float64/decimal/decimal128 | Числа с плавающей точкой — через string во избежание потерь точности на границе языков. |
| safeint/numeric | Неоднозначно для кодогенерации. Используйте integer. |
| utcDateTime/plainDate/plainTime/duration | Дата/время — это string в RFC-3339. TypeScript-codegen иначе подставляет Date, что ломает разбор в разных локалях. |
| url | URL — это string. |
Запрет анонимных моделей
// ❌ Нельзя — ошибка эмиттера
model M {
payload: { foo: string };
}
// ✅ Только через явно объявленную модель
model MPayload { foo: string; }
model M { payload: MPayload; }Обоснование: детерминированные имена в выводе важнее лаконичности. modelina (генератор моделей AsyncAPI) требует осмысленных имён, и авто-генерация ненадёжна.
Декораторы
Сервис
| Декоратор | Назначение |
|---|---|
| @service(#{ title }) | Стандартный из @typespec/compiler. Маркирует namespace как сервис. |
| @info(#{ version, description?, contact?, license?, externalDocs? }) | Метаданные info-блока AsyncAPI. |
| @server(name, #{ host, protocol, pathname?, description?, variables? }) | Описание одного брокера/сервера. Можно вызывать несколько раз. |
AMQP
| Декоратор | Назначение |
|---|---|
| @publish(#{ channelName?, description?, routingKey?, exchange }) | Операция-publisher → action: send. Exchange-типы: direct, fanout. Payload — параметр операции (op X(msg: M): void). description — описание канала; fallback на @doc(...) операции. |
| @consume(#{ channelName?, description?, routingKey?, queue }) | Операция-consumer → action: receive. Принимаемое сообщение — returnType (op X(): M). description — описание канала; fallback на @doc(...) операции. |
| @summary(text) | Стандартный из @typespec/compiler. Short summary операции. |
| @message(#{ name?, summary? }) | Override параметров сгенерированного message. |
| @doc(text) | Стандартный. Длинное описание (description). |
WebSocket
| Декоратор | Назначение |
|---|---|
| @publish | Без аргументов. Операция → action: send. Payload — параметр операции (op X(msg: M): void). returnType ≠ void означает reply (op X(req: Req): Resp). |
| @consume | Без аргументов. Операция → action: receive. Принимаемое сообщение — returnType (op X(): M). |
| @binary | Помечает операцию как бинарную: op X(): void, без параметра и без returnType. Эмиттит message только с contentType: application/octet-stream, формат описывается в @doc. |
| @message, @summary, @doc | Как в AMQP. |
Принципы преобразования
additionalProperties: false— по умолчанию на всех моделях. Намеренное отличие от@typespec/openapi3: мы не разрешаем произвольные поля без явного указания.- Поле с
@doc, ссылающееся на скаляр — оборачивается вallOfс описанием. Копия поведения@typespec/openapi3: JSON Schema не допускает соседства$refсdescription. - Namespace prefix: вложенные namespace дают префикс через точку (
outer.Inner). Top-level service-namespace в префикс не входит. Соответствует@typespec/openapi3. - AsyncAPI 3.0.0 — выбранная версия. 3.1 backward-совместима, но 3.0 проверена на широкой инструментальной поддержке.
- Имя сообщения по умолчанию = имя модели payload верзатим. Для
@consume op X(): MyMessageи для@publish op X(msg: MyMessage)ключ вcomponents.messages—MyMessage(без понижения регистра первой буквы). Если нужно отдельное имя — используйте@message(#{ name: "..." }). - Описание канала AMQP наследуется от
@doc(...)операции, если не задано явно в@publish/@consume. В подавляющем большинстве случаев канал и операция 1:1, и описание одно и то же — это убирает дублирование. - Симметричная семантика операций.
@consume— принимаемое сообщение вreturnType.@publish— отправляемое сообщение в параметре;returnTypeотличный отvoid— это reply (request/reply pattern).@binary(WS) — без параметра и без returnType, payload в YAML не пишется, бинарный формат описывается в@doc.
Диагностика
Все диагностики с префиксом @etc-utils/typespec-amqp-ws/:
unsupported-sized-int,unsupported-int64,unsupported-float,unsupported-fuzzy-numeric,unsupported-temporal,unsupported-url— попытка использовать запрещённый тип.anonymous-model,anonymous-return— анонимная inline-модель.non-string-enum,invalid-enum-value— некорректный enum.unknown-exchange-type— неподдерживаемый тип exchange (толькоdirect,fanout).unsupported-union— union, не являющийсяT | null.missing-doc(warning) — модель/enum без@doc.
Out of scope (v1)
Намеренно не реализовано в v1 — добавляется при реальной потребности:
- Транспорты Kafka, MQTT, HTTP/SSE, SNS/SQS.
- Exchange types
topic,headers. - AsyncAPI security schemes.
correlationId.- Traits (
channelTraits,operationTraits,messageTraits). - AsyncAPI extensions (
x-properties). - Polymorphism: пользовательские
oneOf/anyOf/allOf. - TypeSpec
@versionedинтеграция. - Numeric-валуированные enum.
@tagна operations.- AsyncAPI 3.1.
- JSON output (только YAML).
Документация
Подробная документация — в docs/:
- docs/usage.md — установка, конфигурация, синтаксис, поддерживаемые типы, типичные сценарии.
- docs/architecture.md — внутреннее устройство эмиттера, ключевые решения, точки расширения.
Примеры
В examples/:
- amqp-publish.tsp — простой AMQP publisher.
- amqp-consume.tsp — простой AMQP consumer.
- ws-discriminator.tsp — WebSocket с literal-дискриминатором.
- ws-reply.tsp — WebSocket с request/reply.
Лицензия
MIT.
