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
5 changes: 5 additions & 0 deletions .changeset/asyncapi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@dskripchenko/openapi': minor
---

AsyncAPI: `loadAsyncApi` reads AsyncAPI 2.x and 3.0 into one shape — servers, channels with parameters, operations as send/receive from the application's side (2.x `publish`/`subscribe` turned around), messages with headers, payloads, examples and correlation ids, replies, bindings, traits applied — and `detectSpecKind` tells AsyncAPI and OpenAPI documents apart. `AsyncApiReference` in `@dskripchenko/openapi/vue` shows such a document as a reference page, with the schema view of the OpenAPI reference; payloads in other schema languages are shown as written.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

Swagger 2.0, OpenAPI 3.0 and OpenAPI 3.1 specs in YAML or JSON: read them,
validate them, show them as an API reference with "Try it" and code samples,
and edit them in a visual editor. The core has **no runtime dependencies**;
and edit them in a visual editor. AsyncAPI 2.x and 3.0 documents of
event-driven APIs are read and shown too. The core has **no runtime dependencies**;
the Vue components are built on
[`@dskripchenko/ui`](https://github.com/dskripchenko/ui).

Expand Down Expand Up @@ -132,6 +133,7 @@ English and Russian.
| Embedding an operation | [en](docs/en/embed.md) | [ru](docs/ru/embed.md) | [de](docs/de/embed.md) | [zh](docs/zh/embed.md) |
| API blocks | [en](docs/en/blocks.md) | [ru](docs/ru/blocks.md) | [de](docs/de/blocks.md) | [zh](docs/zh/blocks.md) |
| TypeScript types | [en](docs/en/types.md) | [ru](docs/ru/types.md) | [de](docs/de/types.md) | [zh](docs/zh/types.md) |
| AsyncAPI | [en](docs/en/asyncapi.md) | [ru](docs/ru/asyncapi.md) | [de](docs/de/asyncapi.md) | [zh](docs/zh/asyncapi.md) |
| OpenApiEditor | [en](docs/en/editor.md) | [ru](docs/ru/editor.md) | [de](docs/de/editor.md) | [zh](docs/zh/editor.md) |
| Core API | [en](docs/en/core.md) | [ru](docs/ru/core.md) | [de](docs/de/core.md) | [zh](docs/zh/core.md) |
| Code samples | [en](docs/en/snippets.md) | [ru](docs/ru/snippets.md) | [de](docs/de/snippets.md) | [zh](docs/zh/snippets.md) |
Expand Down
60 changes: 60 additions & 0 deletions docs/de/asyncapi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# AsyncAPI

Ereignisgesteuerte APIs, beschrieben mit AsyncAPI 2.x (2.0–2.6) oder 3.0,
werden wie OpenAPI-Spezifikationen gelesen und gezeigt.

## Auf einer Seite

```vue
<script setup lang="ts">
import { AsyncApiReference } from '@dskripchenko/openapi/vue'
import '@dskripchenko/openapi/style.css'
</script>

<template>
<AsyncApiReference :spec="yamlOrJsonText" locale="en" />
</template>
```

Die Seite zeigt die Beschreibung des Dokuments und die Server (Protokoll,
Adresse, Variablen), dann die Operationen, gruppiert nach Tag — oder nach
Kanal, wenn keine Operation Tags hat. Jede Operation zeigt, ob die Anwendung
**sendet** oder **empfängt**, die Adresse des Kanals mit hervorgehobenen
`{Parametern}`, die Parameter, jede Nachricht (Schemata von Kopfzeilen und
Nutzlast, Inhaltstyp, Korrelations-ID, ein Beispiel — aus dem Dokument oder
aus den Schemata erzeugt — und Bindings), die Antwort einer
Request/Reply-Operation (3.0) und die Bindings der Operation. Danach folgen
die Nachrichten und Schemata der Komponenten. Schemata werden wie in der
OpenAPI-Referenz dargestellt.

Eine Nutzlast in einer anderen Schemasprache (Avro, Protobuf, RAML…) wird so
gezeigt, wie sie geschrieben ist, mit ihrem Format.

| Prop | Typ | Bedeutung |
| ---------- | ------------------------ | -------------------------------------------------- |
| `spec` | `string \| object` | Das Dokument: YAML- oder JSON-Text oder ein geparstes Objekt |
| `locale` | `string` | `en` oder `ru`; standardmäßig die Browsersprache |
| `labels` | `Partial<AsyncApiLabels>`| Ersetzte Texte |
| `external` | `(url) => unknown` | Lädt Dokumente, auf die `$ref`s per URL verweisen |

Slots: `actions` (im Kopf) und `footer`.

## Im Code

```ts
import { loadAsyncApi, detectSpecKind } from '@dskripchenko/openapi'

detectSpecKind(parsed) // 'asyncapi' | 'openapi' | null
const spec = loadAsyncApi(text)
spec.operations // [{ id, action: 'send' | 'receive', channel, messages, reply, bindings, … }]
spec.channels // [{ id, address, parameters, messages, servers, … }]
spec.servers // [{ id, url, protocol, variables, … }]
```

Beide Versionen werden in eine Form gebracht. AsyncAPI 2.x beschreibt
Operationen aus Sicht des Clients; sie werden wie in 3.0 auf die Sicht der
Anwendung gedreht: Ein Kanal, den die Anwendung „abonniert“ (`subscribe`), ist
einer, auf dem sie **sendet**, `publish` ist **Empfang**. Traits von
Nachrichten und Operationen werden angewendet, `$ref`s aufgelöst, und eine
Nachricht, auf die mehrere Stellen verweisen, ist ein einziges Objekt.
`warnings` nennt, was nicht aufgelöst werden konnte.
58 changes: 58 additions & 0 deletions docs/en/asyncapi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# AsyncAPI

Event-driven APIs described with AsyncAPI 2.x (2.0–2.6) or 3.0 are read and
shown the same way as OpenAPI specs.

## On a page

```vue
<script setup lang="ts">
import { AsyncApiReference } from '@dskripchenko/openapi/vue'
import '@dskripchenko/openapi/style.css'
</script>

<template>
<AsyncApiReference :spec="yamlOrJsonText" locale="en" />
</template>
```

The page shows the document's description and servers (protocol, address,
variables), then the operations, grouped by tag — or by channel when no
operation has tags. Each operation shows whether the application **sends**
or **receives**, the channel's address with its `{parameters}` marked, the
parameters, every message (headers and payload schemas, content type,
correlation id, an example — the document's own or one made from the
schemas — and bindings), the reply of a request/reply operation (3.0) and
the operation's bindings. Then come the messages and schemas of the
components. Schemas use the same view as the OpenAPI reference.

A payload in another schema language (Avro, Protobuf, RAML…) is shown as
written, with its format.

| Prop | Type | Meaning |
| ---------- | ------------------------ | ----------------------------------------------- |
| `spec` | `string \| object` | The document: YAML or JSON text, or a parsed object |
| `locale` | `string` | `en` or `ru`; the browser's language by default |
| `labels` | `Partial<AsyncApiLabels>`| Overrides of the texts |
| `external` | `(url) => unknown` | Loads documents that `$ref`s point to by URL |

Slots: `actions` (the header) and `footer`.

## In code

```ts
import { loadAsyncApi, detectSpecKind } from '@dskripchenko/openapi'

detectSpecKind(parsed) // 'asyncapi' | 'openapi' | null
const spec = loadAsyncApi(text)
spec.operations // [{ id, action: 'send' | 'receive', channel, messages, reply, bindings, … }]
spec.channels // [{ id, address, parameters, messages, servers, … }]
spec.servers // [{ id, url, protocol, variables, … }]
```

Both versions are read into one shape. AsyncAPI 2.x describes operations
from the client's side; they are turned around to the application's, as in
3.0: a channel the application `subscribe`s to is one it **sends** on,
`publish` is **receive**. Message and operation traits are applied, `$ref`s
are followed, and a message referred to from several places is one object.
`warnings` lists what could not be resolved.
59 changes: 59 additions & 0 deletions docs/ru/asyncapi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# AsyncAPI

Событийные API, описанные в AsyncAPI 2.x (2.0–2.6) или 3.0, читаются и
показываются так же, как спецификации OpenAPI.

## На странице

```vue
<script setup lang="ts">
import { AsyncApiReference } from '@dskripchenko/openapi/vue'
import '@dskripchenko/openapi/style.css'
</script>

<template>
<AsyncApiReference :spec="yamlOrJsonText" locale="ru" />
</template>
```

Страница показывает описание документа и серверы (протокол, адрес,
переменные), затем операции, сгруппированные по тегам — или по каналам, если
тегов нет ни у одной операции. У каждой операции видно, **отправляет**
приложение или **принимает**, адрес канала с выделенными `{параметрами}`,
параметры, каждое сообщение (схемы заголовков и содержимого, тип содержимого,
идентификатор корреляции, пример — из документа или построенный по схемам — и
привязки), ответ операции «запрос–ответ» (3.0) и привязки операции. Дальше
идут сообщения и схемы из компонентов. Схемы показываются тем же видом, что в
справочнике OpenAPI.

Содержимое на другом языке схем (Avro, Protobuf, RAML…) показывается как
есть, с указанием формата.

| Свойство | Тип | Значение |
| ---------- | ------------------------ | ------------------------------------------------- |
| `spec` | `string \| object` | Документ: текст YAML или JSON либо разобранный объект |
| `locale` | `string` | `en` или `ru`; по умолчанию — язык браузера |
| `labels` | `Partial<AsyncApiLabels>`| Замена текстов |
| `external` | `(url) => unknown` | Загружает документы, на которые `$ref` ссылается по адресу |

Слоты: `actions` (в шапке) и `footer`.

## В коде

```ts
import { loadAsyncApi, detectSpecKind } from '@dskripchenko/openapi'

detectSpecKind(parsed) // 'asyncapi' | 'openapi' | null
const spec = loadAsyncApi(text)
spec.operations // [{ id, action: 'send' | 'receive', channel, messages, reply, bindings, … }]
spec.channels // [{ id, address, parameters, messages, servers, … }]
spec.servers // [{ id, url, protocol, variables, … }]
```

Обе версии приводятся к одному виду. AsyncAPI 2.x описывает операции со
стороны клиента; они разворачиваются к стороне приложения, как в 3.0: канал,
на который приложение «подписано» (`subscribe`), — тот, куда оно
**отправляет**, а `publish` — это **приём**. Применяются трейты сообщений и
операций, разворачиваются `$ref`, а сообщение, на которое ссылаются из
нескольких мест, — один и тот же объект. В `warnings` — то, что не удалось
разрешить.
43 changes: 43 additions & 0 deletions docs/zh/asyncapi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# AsyncAPI

使用 AsyncAPI 2.x(2.0–2.6)或 3.0 描述的事件驱动 API,可以像 OpenAPI 规范一样被读取和展示。

## 在页面上

```vue
<script setup lang="ts">
import { AsyncApiReference } from '@dskripchenko/openapi/vue'
import '@dskripchenko/openapi/style.css'
</script>

<template>
<AsyncApiReference :spec="yamlOrJsonText" locale="en" />
</template>
```

页面会显示文档的描述和服务器(协议、地址、变量),然后是按标签分组的操作——若所有操作都没有标签,则按通道分组。每个操作显示应用是**发送**还是**接收**、标出 `{参数}` 的通道地址、参数、每条消息(头部与负载的 schema、内容类型、关联 ID、示例——来自文档或根据 schema 生成——以及绑定)、请求/响应操作的回复(3.0)以及操作的绑定。随后是组件中的消息和 schema。schema 使用与 OpenAPI 参考相同的视图展示。

使用其他 schema 语言(Avro、Protobuf、RAML 等)的负载按原样显示,并注明其格式。

| 属性 | 类型 | 含义 |
| ---------- | ------------------------ | ---------------------------------------- |
| `spec` | `string \| object` | 文档:YAML 或 JSON 文本,或已解析的对象 |
| `locale` | `string` | `en` 或 `ru`;默认使用浏览器语言 |
| `labels` | `Partial<AsyncApiLabels>`| 覆盖界面文本 |
| `external` | `(url) => unknown` | 加载 `$ref` 通过 URL 引用的文档 |

插槽:`actions`(页眉)和 `footer`。

## 在代码中

```ts
import { loadAsyncApi, detectSpecKind } from '@dskripchenko/openapi'

detectSpecKind(parsed) // 'asyncapi' | 'openapi' | null
const spec = loadAsyncApi(text)
spec.operations // [{ id, action: 'send' | 'receive', channel, messages, reply, bindings, … }]
spec.channels // [{ id, address, parameters, messages, servers, … }]
spec.servers // [{ id, url, protocol, variables, … }]
```

两个版本会被统一为同一种结构。AsyncAPI 2.x 从客户端的角度描述操作;这里会像 3.0 那样转换为应用的视角:应用 `subscribe` 的通道是它**发送**消息的通道,`publish` 则表示**接收**。会应用消息和操作的 traits,解析 `$ref`,被多处引用的消息是同一个对象。`warnings` 列出无法解析的内容。
9 changes: 7 additions & 2 deletions playground/main.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { createApp, h, ref } from 'vue'
import '@dskripchenko/ui/styles/all.css'
import { OpenApiBlock, OpenApiEditor, OpenApiOperation, OpenApiReference } from '../src/vue'
import { AsyncApiReference, OpenApiBlock, OpenApiEditor, OpenApiOperation, OpenApiReference } from '../src/vue'
import '../src/vue/openapi.css'
import petstore from '../tests/fixtures/specs/petstore-30.yaml?raw'
import kitchen from '../tests/fixtures/specs/kitchen-sink-31.json?raw'
import swagger from '../tests/fixtures/specs/petstore-swagger2.json?raw'
import oauth from '../tests/fixtures/specs/oauth-31.json?raw'
import asyncOrders3 from '../tests/fixtures/asyncapi/orders-3.yaml?raw'
import asyncOrders2 from '../tests/fixtures/asyncapi/orders-2.yaml?raw'

const specs: Record<string, string> = { petstore, kitchen, swagger, oauth, empty: '' }

Expand Down Expand Up @@ -78,7 +80,10 @@ createApp({
spacer ? h('div', { class: 'playground-spacer', style: `height: ${spacer}px` }) : null,
h('div', { class: 'playground-box', style: boxStyle }, [
// ?mode=embed&op=createOrder: one operation in an article, as OpenApiOperation.
mode === 'block'
// ?mode=asyncapi&spec=orders2: an AsyncAPI document (3.0 by default).
mode === 'asyncapi'
? h(AsyncApiReference, { spec: params.get('spec') === 'orders2' ? asyncOrders2 : asyncOrders3, locale })
: mode === 'block'
? h('article', { class: 'playground-article', style: 'max-width: 980px; margin: 0 auto' }, [
h('h1', { style: 'font: 600 22px/1.3 var(--uid-font-family-sans); color: var(--uid-text-primary)' }, 'A page with an API block'),
h(OpenApiBlock, {
Expand Down
25 changes: 25 additions & 0 deletions scripts/browser-check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,31 @@ try {
check('embed (Vue) and print: no page errors', evErrors.length === 0, evErrors.join('; '))
await ev.close()

// AsyncAPI: operations with their channels, messages, reply and schemas; both versions.
const aa = await browser.newPage({ viewport: { width: 1440, height: 1200 } })
const aaErrors = []
aa.on('pageerror', (e) => aaErrors.push(String(e)))
await aa.goto(`${base}?mode=asyncapi`)
await aa.locator('.oa-async-op').first().waitFor()
check('asyncapi 3.0: every operation, send and receive', (await aa.locator('.oa-async-op').count()) === 3 && (await aa.locator('.oa-async-op .oa-action--receive').count()) === 1)
check('asyncapi 3.0: channel parameters marked in the address', (await aa.locator('[data-operation="publishOrderPlaced"] .oa-async__param').innerText()) === '{region}')
check('asyncapi 3.0: payload schema resolved through $ref', (await aa.locator('[data-operation="publishOrderPlaced"] .oa-async-message').first().innerText()).includes('items'))
check('asyncapi 3.0: reply with its messages', (await aa.locator('[data-operation="requestPayment"] .oa-async-reply [data-message="PaymentResult"]').count()) === 1)
check('asyncapi 3.0: Avro payload shown as written', (await aa.locator('[id="message/PaymentResult"]').innerText()).toLowerCase().includes('vnd.apache.avro'))
await aa.locator('.oa-nav__op', { hasText: 'Item' }).click()
// Smooth scrolling takes a moment.
await aa.waitForTimeout(1200)
check('asyncapi: navigation scrolls to a schema', await aa.locator('[id="schema/Item"]').evaluate((el) => el.getBoundingClientRect().top < window.innerHeight))
await aa.screenshot({ path: resolve(out, 'asyncapi-3--light.png') })
await aa.goto(`${base}?mode=asyncapi&spec=orders2&theme=dark&locale=ru`)
await aa.locator('.oa-async-op').first().waitFor()
check('asyncapi 2.6: publish and subscribe as receive and send', (await aa.locator('.oa-async-op .oa-action--send').count()) === 1 && (await aa.locator('.oa-async-op .oa-action--receive').count()) === 1)
check('asyncapi 2.6: oneOf messages', (await aa.locator('[data-operation="payments/commands.publish"] .oa-async-message').count()) === 2)
await aa.screenshot({ path: resolve(out, 'asyncapi-2--dark.png') })
check('asyncapi: labels in the page language', (await aa.locator('.oa-async-op').first().innerText()).toLowerCase().includes('отправка'))
check('asyncapi: no page errors', aaErrors.length === 0, aaErrors.join('; '))
await aa.close()

// The editor: visual edits land in the text, the source view reports problems.
const ed = await browser.newPage({ viewport: { width: 1440, height: 1200 } })
const edErrors = []
Expand Down
15 changes: 15 additions & 0 deletions src/asyncapi/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export {
loadAsyncApi,
detectSpecKind,
groupAsyncOperations,
type AsyncApiSpec,
type AsyncServer,
type AsyncChannel,
type AsyncOperation,
type AsyncMessage,
type AsyncMessageExample,
type AsyncParameter,
type AsyncTag,
type AsyncAction,
type SpecKind,
} from './load'
Loading
Loading