@etc-utils/typespec-amqp-ws 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +287 -0
- package/dist/src/amqp/builder.d.ts +4 -0
- package/dist/src/amqp/builder.d.ts.map +1 -0
- package/dist/src/amqp/builder.js +121 -0
- package/dist/src/amqp/builder.js.map +1 -0
- package/dist/src/amqp/decorators.d.ts +32 -0
- package/dist/src/amqp/decorators.d.ts.map +1 -0
- package/dist/src/amqp/decorators.js +38 -0
- package/dist/src/amqp/decorators.js.map +1 -0
- package/dist/src/amqp/index.d.ts +5 -0
- package/dist/src/amqp/index.d.ts.map +1 -0
- package/dist/src/amqp/index.js +7 -0
- package/dist/src/amqp/index.js.map +1 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/shared/asyncapi-emitter.d.ts +5 -0
- package/dist/src/shared/asyncapi-emitter.d.ts.map +1 -0
- package/dist/src/shared/asyncapi-emitter.js +70 -0
- package/dist/src/shared/asyncapi-emitter.js.map +1 -0
- package/dist/src/shared/decorators-service.d.ts +33 -0
- package/dist/src/shared/decorators-service.d.ts.map +1 -0
- package/dist/src/shared/decorators-service.js +13 -0
- package/dist/src/shared/decorators-service.js.map +1 -0
- package/dist/src/shared/document.d.ts +134 -0
- package/dist/src/shared/document.d.ts.map +1 -0
- package/dist/src/shared/document.js +4 -0
- package/dist/src/shared/document.js.map +1 -0
- package/dist/src/shared/lib.d.ts +250 -0
- package/dist/src/shared/lib.d.ts.map +1 -0
- package/dist/src/shared/lib.js +108 -0
- package/dist/src/shared/lib.js.map +1 -0
- package/dist/src/shared/options.d.ts +8 -0
- package/dist/src/shared/options.d.ts.map +1 -0
- package/dist/src/shared/options.js +25 -0
- package/dist/src/shared/options.js.map +1 -0
- package/dist/src/shared/schema-emitter.d.ts +34 -0
- package/dist/src/shared/schema-emitter.d.ts.map +1 -0
- package/dist/src/shared/schema-emitter.js +351 -0
- package/dist/src/shared/schema-emitter.js.map +1 -0
- package/dist/src/shared/state.d.ts +13 -0
- package/dist/src/shared/state.d.ts.map +1 -0
- package/dist/src/shared/state.js +14 -0
- package/dist/src/shared/state.js.map +1 -0
- package/dist/src/shared/yaml-writer.d.ts +7 -0
- package/dist/src/shared/yaml-writer.d.ts.map +1 -0
- package/dist/src/shared/yaml-writer.js +8 -0
- package/dist/src/shared/yaml-writer.js.map +1 -0
- package/dist/src/testing.d.ts +2 -0
- package/dist/src/testing.d.ts.map +1 -0
- package/dist/src/testing.js +6 -0
- package/dist/src/testing.js.map +1 -0
- package/dist/src/ws/builder.d.ts +4 -0
- package/dist/src/ws/builder.d.ts.map +1 -0
- package/dist/src/ws/builder.js +94 -0
- package/dist/src/ws/builder.js.map +1 -0
- package/dist/src/ws/decorators.d.ts +12 -0
- package/dist/src/ws/decorators.d.ts.map +1 -0
- package/dist/src/ws/decorators.js +18 -0
- package/dist/src/ws/decorators.js.map +1 -0
- package/dist/src/ws/index.d.ts +5 -0
- package/dist/src/ws/index.d.ts.map +1 -0
- package/dist/src/ws/index.js +7 -0
- package/dist/src/ws/index.js.map +1 -0
- package/docs/README.md +48 -0
- package/docs/architecture.md +319 -0
- package/docs/usage.md +281 -0
- package/examples/amqp-consume.tsp +26 -0
- package/examples/amqp-publish.tsp +25 -0
- package/examples/ws-discriminator.tsp +26 -0
- package/examples/ws-reply.tsp +29 -0
- package/lib/amqp.tsp +83 -0
- package/lib/main.tsp +65 -0
- package/lib/ws.tsp +42 -0
- package/package.json +75 -0
package/docs/usage.md
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# Использование `@etc-utils/typespec-amqp-ws`
|
|
2
|
+
|
|
3
|
+
## Установка
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install -D @etc-utils/typespec-amqp-ws @typespec/compiler @typespec/asset-emitter
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Требования:
|
|
10
|
+
- Node.js 22+ (рекомендуется 24+ для совместимости с `@asyncapi/cli`)
|
|
11
|
+
- TypeSpec 1.12+
|
|
12
|
+
|
|
13
|
+
## Конфигурация
|
|
14
|
+
|
|
15
|
+
В корне папки с TypeSpec-описанием сервиса (например, `<service>/asyncapi/`) создаётся файл `tspconfig.yaml`. Эмиттер `@etc-utils/typespec-amqp-ws` имеет два emit-таргета: `/amqp` и `/ws`. Один проект использует **один** из них.
|
|
16
|
+
|
|
17
|
+
### Конфиг для AMQP-сервиса
|
|
18
|
+
|
|
19
|
+
```yaml
|
|
20
|
+
emit:
|
|
21
|
+
- "@etc-utils/typespec-amqp-ws/amqp"
|
|
22
|
+
options:
|
|
23
|
+
"@etc-utils/typespec-amqp-ws/amqp":
|
|
24
|
+
file-type: yaml # yaml (default) или json
|
|
25
|
+
output-file: "asyncapi.yaml"
|
|
26
|
+
new-line: "lf" # lf (default) или crlf
|
|
27
|
+
output-dir: "{project-root}/tsp-output"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Конфиг для WebSocket-сервиса
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
emit:
|
|
34
|
+
- "@etc-utils/typespec-amqp-ws/ws"
|
|
35
|
+
options:
|
|
36
|
+
"@etc-utils/typespec-amqp-ws/ws":
|
|
37
|
+
file-type: yaml
|
|
38
|
+
output-file: "asyncapi.yaml"
|
|
39
|
+
new-line: "lf"
|
|
40
|
+
output-dir: "{project-root}/tsp-output"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
После компиляции (`tsp compile .`) сгенерированный YAML лежит в `tsp-output/@etc-utils/typespec-amqp-ws/asyncapi.yaml`. Дальнейший пайплайн (redocly lint, modelina codegen, документация) идёт от этого файла.
|
|
44
|
+
|
|
45
|
+
## Структура проекта
|
|
46
|
+
|
|
47
|
+
Рекомендованная (соответствует тому, как у нас в команде):
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
<service>/asyncapi/
|
|
51
|
+
├── main.tsp # @service / @info / @server + операции
|
|
52
|
+
├── tsp-components/
|
|
53
|
+
│ └── models.tsp # scalars + enums + модели
|
|
54
|
+
├── tspconfig.yaml # конфиг эмиттера
|
|
55
|
+
├── package.json # зависимости (typespec, asset-emitter)
|
|
56
|
+
├── redocly.yaml # конфиг линтера
|
|
57
|
+
└── Makefile # include шаблонного build pipeline
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## API эмиттера
|
|
61
|
+
|
|
62
|
+
### Декораторы общего назначения (namespace `TspAsyncApi`)
|
|
63
|
+
|
|
64
|
+
| Декоратор | Применяется к | Что делает |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `@service(#{title})` | namespace | Стандартный из `@typespec/compiler`. Маркирует namespace как корневой сервис. |
|
|
67
|
+
| `@info(#{...})` | namespace | Заполняет блок `info:` AsyncAPI. Поля: `version`, `description?`, `contact?{name?, url?, email?}`, `license?{name, url?}`, `externalDocs?{url, description?}` |
|
|
68
|
+
| `@server(name, #{...})` | namespace | Описывает один сервер (брокер). Поля: `host`, `protocol`, `pathname?`, `description?`, `variables?: Record<#{default?, description?, enum?}>` |
|
|
69
|
+
|
|
70
|
+
### Декораторы AMQP (namespace `TspAsyncApi.Amqp`)
|
|
71
|
+
|
|
72
|
+
| Декоратор | Применяется к | Описание |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `@publish(#{...})` | op | Операция-publisher → `action: send`. Поля: `channelName?`, `routingKey?`, `exchange: #{name, type: "direct"\|"fanout", durable?, autoDelete?}` |
|
|
75
|
+
| `@consume(#{...})` | op | Операция-consumer → `action: receive`. Поля: `channelName?`, `routingKey?`, `queue: #{name, durable?, autoDelete?, exclusive?}` |
|
|
76
|
+
| `@message(#{...})` | op | Override параметров сообщения: `name?`, `summary?` |
|
|
77
|
+
|
|
78
|
+
### Декораторы WebSocket (namespace `TspAsyncApi.WebSocket`)
|
|
79
|
+
|
|
80
|
+
| Декоратор | Применяется к | Описание |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `@publish` | op | без аргументов → `action: send` |
|
|
83
|
+
| `@consume` | op | без аргументов → `action: receive` |
|
|
84
|
+
| `@reply(MessageModel)` | op | Модель ответного сообщения (request/reply pattern). Reply-сообщение автоматически добавится в канал и `components.messages`. |
|
|
85
|
+
| `@binary` | op | Помечает сообщение бинарным → `contentType: application/octet-stream` |
|
|
86
|
+
| `@message(#{...})` | op | Override параметров сообщения |
|
|
87
|
+
|
|
88
|
+
### Стандартные TypeSpec-декораторы
|
|
89
|
+
|
|
90
|
+
Из `@typespec/compiler`:
|
|
91
|
+
- `@doc("...")` — длинное описание (попадает в `description` YAML)
|
|
92
|
+
- `@summary("...")` — короткое summary (попадает в `summary` YAML)
|
|
93
|
+
|
|
94
|
+
## Поддерживаемые TypeSpec-типы
|
|
95
|
+
|
|
96
|
+
| TypeSpec | YAML | Go / TS / C++ |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `string` | `{type: string}` | string / string / std::string |
|
|
99
|
+
| `boolean` | `{type: boolean}` | bool / boolean / bool |
|
|
100
|
+
| `integer` | `{type: integer}` | int32 / number / int |
|
|
101
|
+
| `bytes` | `{type: string, format: binary}` | []byte / Uint8Array / std::vector<uint8_t> |
|
|
102
|
+
| `enum X { A, B }` | `{type: string, enum: [A, B]}` | string-typedef с константами |
|
|
103
|
+
| `scalar X extends string` | `{type: string}` в `components.schemas.X` | именованный string-typedef |
|
|
104
|
+
| `model X { ... }` | `{type: object, properties, required, additionalProperties: false}` | сгенерированная структура |
|
|
105
|
+
| `T[]` | `{type: array, items: <T>}` | slice / array |
|
|
106
|
+
| `Record<T>` | `{type: object, additionalProperties: <T>}` | `map[string]T` / `Record<string, T>` |
|
|
107
|
+
| literal `"foo"` (на поле модели) | `{type: string, const: "foo"}` | const-значение |
|
|
108
|
+
| `T \| null` | `{type: [<base>, null]}` | pointer / nullable |
|
|
109
|
+
| `field?: T` | поле не в `required` | optional |
|
|
110
|
+
|
|
111
|
+
## Запрещённые типы (ошибка компиляции)
|
|
112
|
+
|
|
113
|
+
Эмиттер намеренно **запрещает**:
|
|
114
|
+
|
|
115
|
+
| Тип | Почему |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `int8`/`int16`/`int32`, `uint8`/`uint16`/`uint32` | Кодгенераторы (`modelina`, `openapi-generator-cli`) **игнорируют** ширину и signed/unsigned, генерируют `int32`/`number`. Размер-специфичные типы создают ложное ожидание сохранения семантики на границе между языками. |
|
|
118
|
+
| `int64`/`uint64` | 64-битные числа должны передаваться как `string` — JavaScript не умеет точно представлять 64-битные числа в JSON. Поясните формат в `@doc`. |
|
|
119
|
+
| `float32`/`float64`/`decimal`/`decimal128` | Числа с плавающей точкой передавайте через `string` во избежание потерь точности на границе между языками. |
|
|
120
|
+
| `safeint`/`numeric` | Неоднозначно для кодогенерации. Используйте `integer`. |
|
|
121
|
+
| `utcDateTime`/`plainDate`/`plainTime`/`duration` | Дата/время — это `string` в RFC-3339 с пояснением в `@doc`. TypeScript-кодген иначе подставляет `Date`, что ломает разбор в разных локалях. |
|
|
122
|
+
| `url` | URL — это `string`. |
|
|
123
|
+
|
|
124
|
+
Эмиттер также **запрещает анонимные inline-модели в полях**:
|
|
125
|
+
|
|
126
|
+
```typespec
|
|
127
|
+
// ❌ Ошибка эмиттера
|
|
128
|
+
model M {
|
|
129
|
+
payload: { foo: string };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ✅ Только через явно объявленную модель
|
|
133
|
+
model MPayload { foo: string; }
|
|
134
|
+
model M { payload: MPayload; }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Обоснование: детерминированные имена в выводе важнее лаконичности на стороне источника. modelina требует осмысленных имён для генерации Go/TS-типов; авто-генерация ненадёжна.
|
|
138
|
+
|
|
139
|
+
## Полный пример: AMQP-сервис
|
|
140
|
+
|
|
141
|
+
```typespec
|
|
142
|
+
import "@etc-utils/typespec-amqp-ws";
|
|
143
|
+
|
|
144
|
+
using TspAsyncApi;
|
|
145
|
+
using TspAsyncApi.Amqp;
|
|
146
|
+
|
|
147
|
+
@service(#{ title: "Notifications" })
|
|
148
|
+
@info(#{
|
|
149
|
+
version: "1.0.0",
|
|
150
|
+
description: "Сервис рассылки уведомлений через RabbitMQ",
|
|
151
|
+
})
|
|
152
|
+
@server("rabbit", #{
|
|
153
|
+
host: "rabbit.example.com:5672",
|
|
154
|
+
pathname: "/notifications",
|
|
155
|
+
protocol: "amqp",
|
|
156
|
+
description: "RabbitMQ-сервер",
|
|
157
|
+
})
|
|
158
|
+
namespace Notifications;
|
|
159
|
+
|
|
160
|
+
@doc("Уведомление пользователю")
|
|
161
|
+
model Notification {
|
|
162
|
+
@doc("Идентификатор уведомления")
|
|
163
|
+
notificationId: string;
|
|
164
|
+
|
|
165
|
+
@doc("Текст уведомления")
|
|
166
|
+
text: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@publish(#{
|
|
170
|
+
routingKey: "notifications.created",
|
|
171
|
+
exchange: #{
|
|
172
|
+
name: "notifications-exchange",
|
|
173
|
+
type: "direct",
|
|
174
|
+
durable: true,
|
|
175
|
+
},
|
|
176
|
+
})
|
|
177
|
+
@summary("Опубликовать новое уведомление")
|
|
178
|
+
op sendNotification(): Notification;
|
|
179
|
+
|
|
180
|
+
@consume(#{
|
|
181
|
+
routingKey: "notifications.acknowledge",
|
|
182
|
+
queue: #{
|
|
183
|
+
name: "notifications-ack-queue",
|
|
184
|
+
durable: true,
|
|
185
|
+
autoDelete: false,
|
|
186
|
+
},
|
|
187
|
+
})
|
|
188
|
+
@summary("Обработать подтверждение доставки")
|
|
189
|
+
op handleAck(): Notification;
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Полный пример: WebSocket с дискриминатором и reply
|
|
193
|
+
|
|
194
|
+
```typespec
|
|
195
|
+
import "@etc-utils/typespec-amqp-ws";
|
|
196
|
+
|
|
197
|
+
using TspAsyncApi;
|
|
198
|
+
using TspAsyncApi.WebSocket;
|
|
199
|
+
|
|
200
|
+
@service(#{ title: "Chat WS" })
|
|
201
|
+
@info(#{ version: "1.0.0" })
|
|
202
|
+
@server("public", #{
|
|
203
|
+
host: "localhost:{port}",
|
|
204
|
+
protocol: "ws",
|
|
205
|
+
pathname: "/chat",
|
|
206
|
+
variables: #{
|
|
207
|
+
port: #{ `default`: "8080", description: "Порт WS-сервера" },
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
namespace Chat;
|
|
211
|
+
|
|
212
|
+
// Дискриминатор сообщения — обычное поле literal-типа.
|
|
213
|
+
// Эмиттер выведет {type: "string", const: "userJoined"} в JSON Schema.
|
|
214
|
+
model UserJoined {
|
|
215
|
+
eventType: "userJoined";
|
|
216
|
+
msgUid: string;
|
|
217
|
+
userId: string;
|
|
218
|
+
nickname: string;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
model SendMessage {
|
|
222
|
+
eventType: "sendMessage";
|
|
223
|
+
msgUid: string;
|
|
224
|
+
text: string;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
model SendMessageResponse {
|
|
228
|
+
eventType: "sendMessageResponse";
|
|
229
|
+
msgUid: string;
|
|
230
|
+
ok: boolean;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Receive
|
|
234
|
+
@consume
|
|
235
|
+
@summary("Пользователь подключился к чату")
|
|
236
|
+
op userJoined(): UserJoined;
|
|
237
|
+
|
|
238
|
+
// Send + reply (request/reply pattern)
|
|
239
|
+
@publish
|
|
240
|
+
@reply(SendMessageResponse)
|
|
241
|
+
@summary("Отправить сообщение в чат")
|
|
242
|
+
op sendMessage(): SendMessage;
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Все WS-операции автоматически складываются на единый канал `/`. Это типичный паттерн WebSocket-API, где дискриминация сообщений происходит через поле `eventType`.
|
|
246
|
+
|
|
247
|
+
## Версионирование AsyncAPI
|
|
248
|
+
|
|
249
|
+
Эмиттер генерирует AsyncAPI 3.0.0. Версия 3.1 backward-совместима, но мы остаёмся на 3.0 ради консервативности и максимальной совместимости с `modelina`/`redocly`.
|
|
250
|
+
|
|
251
|
+
## Дискриминация сообщений в WebSocket
|
|
252
|
+
|
|
253
|
+
В TypeSpec литерал-типы (`"localActions"`) превращаются в JSON Schema `const` — нативный механизм без специальных декораторов. **Любое** поле модели типа `: "значение"` становится `const` в схеме. Поле может называться как угодно — `eventType`, `kind`, `type`, и т.д. Модели без таких полей тоже валидны (например, AMQP-сообщения обычно без дискриминатора).
|
|
254
|
+
|
|
255
|
+
## Диагностики
|
|
256
|
+
|
|
257
|
+
Эмиттер выдаёт следующие коды (все с префиксом `@etc-utils/typespec-amqp-ws/`):
|
|
258
|
+
|
|
259
|
+
- `unsupported-sized-int`, `unsupported-int64`, `unsupported-float`, `unsupported-fuzzy-numeric`, `unsupported-temporal`, `unsupported-url` — попытка использовать запрещённый тип.
|
|
260
|
+
- `anonymous-model`, `anonymous-return` — анонимная inline-модель в поле или return type.
|
|
261
|
+
- `non-string-enum`, `invalid-enum-value` — некорректный enum (numeric или не-идентификаторное значение).
|
|
262
|
+
- `unknown-exchange-type` — `topic` или `headers` exchange (вне scope).
|
|
263
|
+
- `unsupported-union` — union, не являющийся `T | null`.
|
|
264
|
+
- `missing-doc` (warning) — модель/enum без `@doc`.
|
|
265
|
+
|
|
266
|
+
## Out of scope (v1)
|
|
267
|
+
|
|
268
|
+
Намеренно **не реализовано** в v1 — добавляется по запросу при реальной потребности:
|
|
269
|
+
|
|
270
|
+
- Транспорты Kafka, MQTT, HTTP/SSE, SNS/SQS.
|
|
271
|
+
- Exchange types `topic`, `headers`.
|
|
272
|
+
- AsyncAPI security schemes.
|
|
273
|
+
- `correlationId`.
|
|
274
|
+
- Traits (`channelTraits`, `operationTraits`, `messageTraits`).
|
|
275
|
+
- AsyncAPI extensions (`x-` properties).
|
|
276
|
+
- Polymorphism: пользовательские `oneOf`/`anyOf`/`allOf` (внутренний `allOf` для $ref+description — используется автоматически).
|
|
277
|
+
- TypeSpec `@versioned` интеграция.
|
|
278
|
+
- Numeric-валуированные enum.
|
|
279
|
+
- `@tag` на operations.
|
|
280
|
+
- AsyncAPI 3.1.
|
|
281
|
+
- JSON output (только YAML).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import "typespec-amqp-ws";
|
|
2
|
+
|
|
3
|
+
using TspAsyncApi;
|
|
4
|
+
using TspAsyncApi.Amqp;
|
|
5
|
+
|
|
6
|
+
@service(#{ title: "Notifications Consumer" })
|
|
7
|
+
@info(#{ version: "1.0.0" })
|
|
8
|
+
@server("rabbit", #{ host: "localhost:5672", protocol: "amqp" })
|
|
9
|
+
namespace NotificationsConsumer;
|
|
10
|
+
|
|
11
|
+
@doc("Уведомление пользователю")
|
|
12
|
+
model Notification {
|
|
13
|
+
notificationId: string;
|
|
14
|
+
text: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@consume(#{
|
|
18
|
+
routingKey: "notifications.created",
|
|
19
|
+
queue: #{
|
|
20
|
+
name: "notifications-consumer-queue",
|
|
21
|
+
durable: true,
|
|
22
|
+
autoDelete: false,
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
@summary("Обработать уведомление из очереди")
|
|
26
|
+
op handleNotification(): Notification;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import "typespec-amqp-ws";
|
|
2
|
+
|
|
3
|
+
using TspAsyncApi;
|
|
4
|
+
using TspAsyncApi.Amqp;
|
|
5
|
+
|
|
6
|
+
@service(#{ title: "Notifications Producer" })
|
|
7
|
+
@info(#{ version: "1.0.0" })
|
|
8
|
+
@server("rabbit", #{ host: "localhost:5672", protocol: "amqp" })
|
|
9
|
+
namespace NotificationsProducer;
|
|
10
|
+
|
|
11
|
+
@doc("Уведомление пользователю")
|
|
12
|
+
model Notification {
|
|
13
|
+
@doc("Уникальный идентификатор уведомления")
|
|
14
|
+
notificationId: string;
|
|
15
|
+
|
|
16
|
+
@doc("Текст уведомления")
|
|
17
|
+
text: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@publish(#{
|
|
21
|
+
routingKey: "notifications.created",
|
|
22
|
+
exchange: #{ name: "notifications-exchange", type: "direct", durable: true },
|
|
23
|
+
})
|
|
24
|
+
@summary("Опубликовать новое уведомление")
|
|
25
|
+
op sendNotification(): Notification;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import "typespec-amqp-ws";
|
|
2
|
+
|
|
3
|
+
using TspAsyncApi;
|
|
4
|
+
using TspAsyncApi.WebSocket;
|
|
5
|
+
|
|
6
|
+
@service(#{ title: "Chat WS" })
|
|
7
|
+
@info(#{ version: "1.0.0" })
|
|
8
|
+
@server("public", #{ host: "localhost:8080", protocol: "ws", pathname: "/ws" })
|
|
9
|
+
namespace Chat;
|
|
10
|
+
|
|
11
|
+
model UserJoinedPayload { userId: string; nickname: string; }
|
|
12
|
+
model MessagePayload { from: string; text: string; }
|
|
13
|
+
|
|
14
|
+
// Дискриминатор через literal-тип — никаких специальных декораторов.
|
|
15
|
+
model UserJoined {
|
|
16
|
+
eventType: "userJoined";
|
|
17
|
+
payload: UserJoinedPayload;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
model NewMessage {
|
|
21
|
+
eventType: "newMessage";
|
|
22
|
+
payload: MessagePayload;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@consume op userJoined(): UserJoined;
|
|
26
|
+
@consume op newMessage(): NewMessage;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import "typespec-amqp-ws";
|
|
2
|
+
|
|
3
|
+
using TspAsyncApi;
|
|
4
|
+
using TspAsyncApi.WebSocket;
|
|
5
|
+
|
|
6
|
+
@service(#{ title: "Auth WS" })
|
|
7
|
+
@info(#{ version: "1.0.0" })
|
|
8
|
+
@server("public", #{ host: "localhost:8080", protocol: "ws", pathname: "/ws" })
|
|
9
|
+
namespace Auth;
|
|
10
|
+
|
|
11
|
+
model LoginRequestPayload { username: string; password: string; }
|
|
12
|
+
model LoginResponsePayload { ok: boolean; token?: string; }
|
|
13
|
+
|
|
14
|
+
model LoginRequest {
|
|
15
|
+
eventType: "loginRequest";
|
|
16
|
+
msgUid: string;
|
|
17
|
+
payload: LoginRequestPayload;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
model LoginResponse {
|
|
21
|
+
eventType: "loginResponse";
|
|
22
|
+
msgUid: string;
|
|
23
|
+
payload: LoginResponsePayload;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@publish
|
|
27
|
+
@reply(LoginResponse)
|
|
28
|
+
@summary("Запрос на аутентификацию")
|
|
29
|
+
op login(): LoginRequest;
|
package/lib/amqp.tsp
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import "../dist/src/amqp/decorators.js";
|
|
2
|
+
|
|
3
|
+
using TypeSpec.Reflection;
|
|
4
|
+
|
|
5
|
+
namespace TspAsyncApi.Amqp;
|
|
6
|
+
|
|
7
|
+
@doc("Конфиг AMQP-обменника (exchange)")
|
|
8
|
+
model ExchangeConfig {
|
|
9
|
+
@doc("Имя exchange в RabbitMQ")
|
|
10
|
+
name: string;
|
|
11
|
+
|
|
12
|
+
@doc("Тип exchange: direct либо fanout")
|
|
13
|
+
type: string;
|
|
14
|
+
|
|
15
|
+
@doc("Переживает рестарт брокера")
|
|
16
|
+
durable?: boolean;
|
|
17
|
+
|
|
18
|
+
@doc("Удаляется когда последний биндинг отвалился")
|
|
19
|
+
autoDelete?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@doc("Конфиг AMQP-очереди (queue)")
|
|
23
|
+
model QueueConfig {
|
|
24
|
+
@doc("Имя очереди в RabbitMQ")
|
|
25
|
+
name: string;
|
|
26
|
+
|
|
27
|
+
@doc("Переживает рестарт брокера")
|
|
28
|
+
durable?: boolean;
|
|
29
|
+
|
|
30
|
+
@doc("Удаляется когда последний consumer отвалился")
|
|
31
|
+
autoDelete?: boolean;
|
|
32
|
+
|
|
33
|
+
@doc("Эксклюзивная — только один consumer")
|
|
34
|
+
exclusive?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@doc("Конфиг операции-publisher (action: send)")
|
|
38
|
+
model PublishConfig {
|
|
39
|
+
@doc("Имя канала в YAML (override). Default — имя операции верзатим")
|
|
40
|
+
channelName?: string;
|
|
41
|
+
|
|
42
|
+
@doc("Routing key. Может быть опущен для fanout exchange")
|
|
43
|
+
routingKey?: string;
|
|
44
|
+
|
|
45
|
+
@doc("Exchange, в который публикуется сообщение")
|
|
46
|
+
exchange: ExchangeConfig;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@doc("Конфиг операции-consumer (action: receive)")
|
|
50
|
+
model ConsumeConfig {
|
|
51
|
+
@doc("Имя канала в YAML (override)")
|
|
52
|
+
channelName?: string;
|
|
53
|
+
|
|
54
|
+
@doc("Routing key (биндинг очереди к exchange)")
|
|
55
|
+
routingKey?: string;
|
|
56
|
+
|
|
57
|
+
@doc("Очередь, из которой читаются сообщения")
|
|
58
|
+
queue: QueueConfig;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@doc("Override параметров сообщения")
|
|
62
|
+
model MessageOverride {
|
|
63
|
+
@doc("Имя ключа сообщения в YAML")
|
|
64
|
+
name?: string;
|
|
65
|
+
|
|
66
|
+
@doc("Краткое описание сообщения")
|
|
67
|
+
summary?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Операция-publisher: сервис отправляет сообщение в exchange.
|
|
72
|
+
* → AsyncAPI action: send.
|
|
73
|
+
*/
|
|
74
|
+
extern dec publish(target: Operation, config: valueof PublishConfig);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Операция-consumer: сервис читает сообщения из очереди.
|
|
78
|
+
* → AsyncAPI action: receive.
|
|
79
|
+
*/
|
|
80
|
+
extern dec consume(target: Operation, config: valueof ConsumeConfig);
|
|
81
|
+
|
|
82
|
+
/** Override параметров сообщения, выводимого эмиттером */
|
|
83
|
+
extern dec message(target: Operation, config: valueof MessageOverride);
|
package/lib/main.tsp
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import "../dist/src/index.js";
|
|
2
|
+
import "../dist/src/shared/decorators-service.js";
|
|
3
|
+
|
|
4
|
+
import "./amqp.tsp";
|
|
5
|
+
import "./ws.tsp";
|
|
6
|
+
|
|
7
|
+
using TypeSpec.Reflection;
|
|
8
|
+
|
|
9
|
+
namespace TspAsyncApi;
|
|
10
|
+
|
|
11
|
+
@doc("Контактные данные владельца API")
|
|
12
|
+
model ContactInfo {
|
|
13
|
+
name?: string;
|
|
14
|
+
url?: string;
|
|
15
|
+
email?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@doc("Лицензия API")
|
|
19
|
+
model LicenseInfo {
|
|
20
|
+
name: string;
|
|
21
|
+
url?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@doc("Ссылка на внешнюю документацию")
|
|
25
|
+
model ExternalDocs {
|
|
26
|
+
url: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@doc("Блок info AsyncAPI документа")
|
|
31
|
+
model InfoConfig {
|
|
32
|
+
version: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
contact?: ContactInfo;
|
|
35
|
+
license?: LicenseInfo;
|
|
36
|
+
externalDocs?: ExternalDocs;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@doc("Переменная в шаблоне URL сервера (например, {port})")
|
|
40
|
+
model ServerVariable {
|
|
41
|
+
`default`?: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
`enum`?: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@doc("Описание сервера (брокера). Соответствует одному ключу в servers AsyncAPI")
|
|
47
|
+
model ServerConfig {
|
|
48
|
+
host: string;
|
|
49
|
+
protocol: string;
|
|
50
|
+
pathname?: string;
|
|
51
|
+
description?: string;
|
|
52
|
+
variables?: Record<ServerVariable>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Заполняет блок info AsyncAPI документа.
|
|
57
|
+
* Применяется к корневому namespace сервиса (тому же, к которому @service).
|
|
58
|
+
*/
|
|
59
|
+
extern dec info(target: Namespace, options: valueof InfoConfig);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Добавляет одну запись в блок servers AsyncAPI документа.
|
|
63
|
+
* Можно вызывать многократно для нескольких серверов.
|
|
64
|
+
*/
|
|
65
|
+
extern dec server(target: Namespace, name: valueof string, options: valueof ServerConfig);
|
package/lib/ws.tsp
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import "../dist/src/ws/decorators.js";
|
|
2
|
+
|
|
3
|
+
using TypeSpec.Reflection;
|
|
4
|
+
|
|
5
|
+
namespace TspAsyncApi.WebSocket;
|
|
6
|
+
|
|
7
|
+
@doc("Override параметров сообщения")
|
|
8
|
+
model MessageOverride {
|
|
9
|
+
@doc("Имя ключа сообщения в YAML")
|
|
10
|
+
name?: string;
|
|
11
|
+
|
|
12
|
+
@doc("Краткое описание сообщения")
|
|
13
|
+
summary?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Операция-publisher: сервис отправляет сообщение.
|
|
18
|
+
* → AsyncAPI action: send.
|
|
19
|
+
* Все WS-операции по умолчанию на канале "/".
|
|
20
|
+
*/
|
|
21
|
+
extern dec publish(target: Operation);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Операция-consumer: сервис принимает сообщение.
|
|
25
|
+
* → AsyncAPI action: receive.
|
|
26
|
+
*/
|
|
27
|
+
extern dec consume(target: Operation);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Указывает модель ответного сообщения (request/reply pattern).
|
|
31
|
+
* Реплай-сообщение автоматически добавится в канал и components.messages.
|
|
32
|
+
*/
|
|
33
|
+
extern dec reply(target: Operation, replyType: Model);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Помечает сообщение операции как бинарное.
|
|
37
|
+
* → components.messages.X.contentType: application/octet-stream
|
|
38
|
+
*/
|
|
39
|
+
extern dec binary(target: Operation);
|
|
40
|
+
|
|
41
|
+
/** Override параметров сообщения */
|
|
42
|
+
extern dec message(target: Operation, config: valueof MessageOverride);
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@etc-utils/typespec-amqp-ws",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "TypeSpec emitter for AsyncAPI 3.0 covering AMQP (RabbitMQ) and WebSocket transports",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/TertiumOrganum1/typespec-amqp-ws.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/TertiumOrganum1/typespec-amqp-ws#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/TertiumOrganum1/typespec-amqp-ws/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"typespec",
|
|
17
|
+
"asyncapi",
|
|
18
|
+
"amqp",
|
|
19
|
+
"rabbitmq",
|
|
20
|
+
"websocket",
|
|
21
|
+
"emitter"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22.0.0"
|
|
25
|
+
},
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"import": "./dist/src/index.js",
|
|
29
|
+
"types": "./dist/src/index.d.ts"
|
|
30
|
+
},
|
|
31
|
+
"./amqp": {
|
|
32
|
+
"import": "./dist/src/amqp/index.js",
|
|
33
|
+
"types": "./dist/src/amqp/index.d.ts"
|
|
34
|
+
},
|
|
35
|
+
"./ws": {
|
|
36
|
+
"import": "./dist/src/ws/index.js",
|
|
37
|
+
"types": "./dist/src/ws/index.d.ts"
|
|
38
|
+
},
|
|
39
|
+
"./testing": {
|
|
40
|
+
"import": "./dist/src/testing.js",
|
|
41
|
+
"types": "./dist/src/testing.d.ts"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"tspMain": "./lib/main.tsp",
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc -b",
|
|
47
|
+
"watch": "tsc -b --watch",
|
|
48
|
+
"test": "vitest run",
|
|
49
|
+
"test:watch": "vitest",
|
|
50
|
+
"clean": "rm -rf dist coverage *.tsbuildinfo"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@typespec/compiler": "^1.12.0",
|
|
54
|
+
"@typespec/asset-emitter": "^0.79.0"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"js-yaml": "^4.1.0"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@typespec/compiler": "^1.12.0",
|
|
61
|
+
"@typespec/asset-emitter": "^0.79.0",
|
|
62
|
+
"@types/js-yaml": "^4.0.9",
|
|
63
|
+
"@types/node": "^20.0.0",
|
|
64
|
+
"typescript": "^5.4.0",
|
|
65
|
+
"vitest": "^1.6.0"
|
|
66
|
+
},
|
|
67
|
+
"files": [
|
|
68
|
+
"dist/src/**",
|
|
69
|
+
"lib/**",
|
|
70
|
+
"docs/**",
|
|
71
|
+
"examples/**",
|
|
72
|
+
"README.md",
|
|
73
|
+
"LICENSE"
|
|
74
|
+
]
|
|
75
|
+
}
|