@scalar/mock-server 0.11.1 → 0.12.1
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/CHANGELOG.md +9 -0
- package/dist/create-asyncapi-mock-server.d.ts +51 -0
- package/dist/create-asyncapi-mock-server.d.ts.map +1 -0
- package/dist/create-asyncapi-mock-server.js +51 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/transports/index.d.ts +9 -0
- package/dist/transports/index.d.ts.map +1 -0
- package/dist/transports/index.js +9 -0
- package/dist/transports/sse.d.ts +11 -0
- package/dist/transports/sse.d.ts.map +1 -0
- package/dist/transports/sse.js +37 -0
- package/dist/transports/types.d.ts +94 -0
- package/dist/transports/types.d.ts.map +1 -0
- package/dist/transports/types.js +1 -0
- package/dist/transports/websocket.d.ts +10 -0
- package/dist/transports/websocket.d.ts.map +1 -0
- package/dist/transports/websocket.js +45 -0
- package/dist/utils/deserialize-parameter.d.ts +76 -0
- package/dist/utils/deserialize-parameter.d.ts.map +1 -0
- package/dist/utils/deserialize-parameter.js +297 -0
- package/dist/utils/generate-message.d.ts +12 -0
- package/dist/utils/generate-message.d.ts.map +1 -0
- package/dist/utils/generate-message.js +41 -0
- package/dist/utils/process-asyncapi-document.d.ts +23 -0
- package/dist/utils/process-asyncapi-document.d.ts.map +1 -0
- package/dist/utils/process-asyncapi-document.js +56 -0
- package/dist/utils/resolve-channels.d.ts +11 -0
- package/dist/utils/resolve-channels.d.ts.map +1 -0
- package/dist/utils/resolve-channels.js +100 -0
- package/dist/utils/validate-request.d.ts +2 -2
- package/dist/utils/validate-request.d.ts.map +1 -1
- package/dist/utils/validate-request.js +173 -22
- package/package.json +7 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @scalar/mock-server
|
|
2
2
|
|
|
3
|
+
## 0.12.1
|
|
4
|
+
|
|
5
|
+
## 0.12.0
|
|
6
|
+
|
|
7
|
+
### Minor Changes
|
|
8
|
+
|
|
9
|
+
- [#9572](https://github.com/scalar/scalar/pull/9572): Add `createAsyncApiMockServer` to mock event-driven APIs from an AsyncAPI 3.1 document. Channels are served over WebSocket and SSE, with messages generated from each message's payload schema (the same generator the REST mocker uses). Additional protocols (e.g. SignalR) can be added through the `transports` extension point. The Docker mock server now auto-detects AsyncAPI documents.
|
|
10
|
+
- [#9486](https://github.com/scalar/scalar/pull/9486): Extend request validation to cover all parameter locations and serialization styles. Header (`in: header`) and cookie (`in: cookie`) parameters are now validated against their schema, with case-insensitive header matching and the `Accept`/`Content-Type`/`Authorization` headers ignored per the OpenAPI specification. Array and object parameters are deserialized by their `style`/`explode` before validation — `form`, `simple`, `spaceDelimited`, `pipeDelimited`, `deepObject`, `label`, and `matrix` — so values like `?ids=1&ids=2`, `?filter[min]=1`, or `/;point=x,1,y,2` validate against their schema instead of being rejected as a raw string. Violations are reported with a `header`, `cookie`, `path`, or `query` location.
|
|
11
|
+
|
|
3
12
|
## 0.11.1
|
|
4
13
|
|
|
5
14
|
### Patch Changes
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createNodeWebSocket } from '@hono/node-ws';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import type { MessageDirection, MockTransport } from './transports/types.js';
|
|
4
|
+
/** Options for {@link createAsyncApiMockServer}. */
|
|
5
|
+
export type AsyncApiMockServerOptions = {
|
|
6
|
+
/**
|
|
7
|
+
* The AsyncAPI 3.1 document to mock. Can be a string (URL or file path), a raw JSON/YAML
|
|
8
|
+
* string, or an already-parsed object.
|
|
9
|
+
*/
|
|
10
|
+
document?: string | Record<string, any>;
|
|
11
|
+
/**
|
|
12
|
+
* Additional transports appended after the built-in WebSocket and SSE transports. Use this to
|
|
13
|
+
* support extra protocols (for example SignalR) without changing the core. The first transport
|
|
14
|
+
* whose `supports()` returns `true` for a channel owns it.
|
|
15
|
+
*/
|
|
16
|
+
transports?: MockTransport[];
|
|
17
|
+
/** Called for every message flowing in or out of the mock, for logging or inspection. */
|
|
18
|
+
onMessage?: (event: {
|
|
19
|
+
channel: string;
|
|
20
|
+
direction: MessageDirection;
|
|
21
|
+
payload: unknown;
|
|
22
|
+
}) => void;
|
|
23
|
+
/** Optional sink for transport lifecycle log lines. Defaults to no-op. */
|
|
24
|
+
logger?: (line: string) => void;
|
|
25
|
+
};
|
|
26
|
+
/** The result of {@link createAsyncApiMockServer}. */
|
|
27
|
+
export type AsyncApiMockServer = {
|
|
28
|
+
/** The Hono app serving SSE channels and WebSocket upgrade routes. */
|
|
29
|
+
app: Hono;
|
|
30
|
+
/**
|
|
31
|
+
* Attaches WebSocket handling to the running Node HTTP server returned by `@hono/node-server`'s
|
|
32
|
+
* `serve()`. Must be called for WebSocket channels to accept connections.
|
|
33
|
+
*/
|
|
34
|
+
injectWebSocket: ReturnType<typeof createNodeWebSocket>['injectWebSocket'];
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Create a mock server for an AsyncAPI 3.1 document — the event-driven counterpart of
|
|
38
|
+
* {@link createMockServer}. Each channel is registered on a transport (WebSocket or SSE by
|
|
39
|
+
* default) that emits realistic mock messages generated from the channel's message payload
|
|
40
|
+
* schemas, the same way the REST mocker generates HTTP response bodies.
|
|
41
|
+
*
|
|
42
|
+
* WebSocket support requires attaching to the HTTP server after `serve()`:
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* const { app, injectWebSocket } = await createAsyncApiMockServer({ document })
|
|
46
|
+
* const server = serve({ fetch: app.fetch, port: 3000 })
|
|
47
|
+
* injectWebSocket(server)
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare function createAsyncApiMockServer(options: AsyncApiMockServerOptions): Promise<AsyncApiMockServer>;
|
|
51
|
+
//# sourceMappingURL=create-asyncapi-mock-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-asyncapi-mock-server.d.ts","sourceRoot":"","sources":["../src/create-asyncapi-mock-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAI3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAoB,MAAM,oBAAoB,CAAA;AAK3F,oDAAoD;AACpD,MAAM,MAAM,yBAAyB,GAAG;IACtC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAEvC;;;;OAIG;IACH,UAAU,CAAC,EAAE,aAAa,EAAE,CAAA;IAE5B,yFAAyF;IACzF,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,gBAAgB,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAA;IAE/F,0EAA0E;IAC1E,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC,CAAA;AAED,sDAAsD;AACtD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,sEAAsE;IACtE,GAAG,EAAE,IAAI,CAAA;IACT;;;OAGG;IACH,eAAe,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,iBAAiB,CAAC,CAAA;CAC3E,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAsC9G"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createNodeWebSocket } from '@hono/node-ws';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { cors } from 'hono/cors';
|
|
4
|
+
import { defaultTransports } from './transports/index.js';
|
|
5
|
+
import { generateMessage } from './utils/generate-message.js';
|
|
6
|
+
import { processAsyncApiDocument } from './utils/process-asyncapi-document.js';
|
|
7
|
+
import { resolveChannels } from './utils/resolve-channels.js';
|
|
8
|
+
/**
|
|
9
|
+
* Create a mock server for an AsyncAPI 3.1 document — the event-driven counterpart of
|
|
10
|
+
* {@link createMockServer}. Each channel is registered on a transport (WebSocket or SSE by
|
|
11
|
+
* default) that emits realistic mock messages generated from the channel's message payload
|
|
12
|
+
* schemas, the same way the REST mocker generates HTTP response bodies.
|
|
13
|
+
*
|
|
14
|
+
* WebSocket support requires attaching to the HTTP server after `serve()`:
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* const { app, injectWebSocket } = await createAsyncApiMockServer({ document })
|
|
18
|
+
* const server = serve({ fetch: app.fetch, port: 3000 })
|
|
19
|
+
* injectWebSocket(server)
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export async function createAsyncApiMockServer(options) {
|
|
23
|
+
const app = new Hono();
|
|
24
|
+
// The Node WebSocket adapter must be created against the app before routes are registered so the
|
|
25
|
+
// `upgradeWebSocket` helper shares this app's lifecycle. `injectWebSocket` is wired to the
|
|
26
|
+
// HTTP server by the caller after `serve()`.
|
|
27
|
+
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
|
|
28
|
+
const document = await processAsyncApiDocument(options.document);
|
|
29
|
+
const channels = resolveChannels(document);
|
|
30
|
+
const transports = [...defaultTransports, ...(options.transports ?? [])];
|
|
31
|
+
const log = options.logger ?? (() => undefined);
|
|
32
|
+
// CORS for the SSE/HTTP routes (WebSocket upgrades are not subject to CORS).
|
|
33
|
+
app.use(cors());
|
|
34
|
+
const context = {
|
|
35
|
+
app,
|
|
36
|
+
upgradeWebSocket,
|
|
37
|
+
generateMessage,
|
|
38
|
+
onMessage: options.onMessage,
|
|
39
|
+
log,
|
|
40
|
+
};
|
|
41
|
+
for (const channel of channels) {
|
|
42
|
+
const transport = transports.find((candidate) => candidate.supports(channel));
|
|
43
|
+
if (!transport) {
|
|
44
|
+
log(`[asyncapi] no transport for channel "${channel.id}" (protocols: ${channel.protocols.join(', ') || 'none'})`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
transport.register(channel, context);
|
|
48
|
+
log(`[asyncapi] ${transport.name} -> ${channel.route} (channel "${channel.id}")`);
|
|
49
|
+
}
|
|
50
|
+
return { app, injectWebSocket };
|
|
51
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
|
+
export { type AsyncApiMockServer, type AsyncApiMockServerOptions, createAsyncApiMockServer, } from './create-asyncapi-mock-server.js';
|
|
1
2
|
export { createMockServer } from './create-mock-server.js';
|
|
3
|
+
export { defaultTransports, sseTransport, websocketTransport } from './transports/index.js';
|
|
4
|
+
export type { MessageDirection, MockMessage, MockTransport, ResolvedChannel, ResolvedMessage, ResolvedOperation, TransportContext, } from './transports/types.js';
|
|
5
|
+
export { isAsyncApiDocument } from './utils/process-asyncapi-document.js';
|
|
2
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,yBAAyB,EAC9B,wBAAwB,GACzB,MAAM,+BAA+B,CAAA;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAClF,YAAY,EACV,gBAAgB,EAChB,WAAW,EACX,aAAa,EACb,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1 +1,4 @@
|
|
|
1
|
+
export { createAsyncApiMockServer, } from './create-asyncapi-mock-server.js';
|
|
1
2
|
export { createMockServer } from './create-mock-server.js';
|
|
3
|
+
export { defaultTransports, sseTransport, websocketTransport } from './transports/index.js';
|
|
4
|
+
export { isAsyncApiDocument } from './utils/process-asyncapi-document.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MockTransport } from '../transports/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Built-in transports, in match priority order. WebSocket is checked before SSE so a `ws`
|
|
4
|
+
* channel is never claimed by the SSE fallback. Consumer transports are appended after these.
|
|
5
|
+
*/
|
|
6
|
+
export declare const defaultTransports: MockTransport[];
|
|
7
|
+
export { sseTransport } from './sse.js';
|
|
8
|
+
export { websocketTransport } from './websocket.js';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/transports/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAKvD;;;GAGG;AACH,eAAO,MAAM,iBAAiB,EAAE,aAAa,EAAuC,CAAA;AAEpF,OAAO,EAAE,YAAY,EAAE,MAAM,OAAO,CAAA;AACpC,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { sseTransport } from './sse.js';
|
|
2
|
+
import { websocketTransport } from './websocket.js';
|
|
3
|
+
/**
|
|
4
|
+
* Built-in transports, in match priority order. WebSocket is checked before SSE so a `ws`
|
|
5
|
+
* channel is never claimed by the SSE fallback. Consumer transports are appended after these.
|
|
6
|
+
*/
|
|
7
|
+
export const defaultTransports = [websocketTransport, sseTransport];
|
|
8
|
+
export { sseTransport } from './sse.js';
|
|
9
|
+
export { websocketTransport } from './websocket.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { MockTransport } from '../transports/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Built-in Server-Sent Events transport. Serves one-way, server-push channels over HTTP: a `GET`
|
|
4
|
+
* on the channel route opens an SSE stream and emits a generated message per `receive` operation.
|
|
5
|
+
*
|
|
6
|
+
* Claims channels whose servers speak `sse`, or plain `http`/`https` channels that have at least
|
|
7
|
+
* one `receive` operation (a server-push channel). WebSocket takes precedence for `ws` channels
|
|
8
|
+
* because it is registered first in the default transport list.
|
|
9
|
+
*/
|
|
10
|
+
export declare const sseTransport: MockTransport;
|
|
11
|
+
//# sourceMappingURL=sse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../../src/transports/sse.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAqB,MAAM,oBAAoB,CAAA;AAE1E;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,EAAE,aAmC1B,CAAA"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { streamSSE } from 'hono/streaming';
|
|
2
|
+
/**
|
|
3
|
+
* Built-in Server-Sent Events transport. Serves one-way, server-push channels over HTTP: a `GET`
|
|
4
|
+
* on the channel route opens an SSE stream and emits a generated message per `receive` operation.
|
|
5
|
+
*
|
|
6
|
+
* Claims channels whose servers speak `sse`, or plain `http`/`https` channels that have at least
|
|
7
|
+
* one `receive` operation (a server-push channel). WebSocket takes precedence for `ws` channels
|
|
8
|
+
* because it is registered first in the default transport list.
|
|
9
|
+
*/
|
|
10
|
+
export const sseTransport = {
|
|
11
|
+
name: 'sse',
|
|
12
|
+
supports: (channel) => {
|
|
13
|
+
if (channel.protocols.includes('sse')) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
const isHttp = channel.protocols.includes('http') || channel.protocols.includes('https');
|
|
17
|
+
const hasServerPush = channel.operations.some((operation) => operation.action === 'receive');
|
|
18
|
+
return isHttp && hasServerPush;
|
|
19
|
+
},
|
|
20
|
+
register: (channel, context) => {
|
|
21
|
+
const { app, generateMessage, onMessage, log } = context;
|
|
22
|
+
const receiveOperations = channel.operations.filter((operation) => operation.action === 'receive');
|
|
23
|
+
// Fall back to all channel messages when the channel has no explicit receive operation.
|
|
24
|
+
const operations = receiveOperations.length > 0 ? receiveOperations : [{ messages: channel.messages }];
|
|
25
|
+
app.get(channel.route, (c) => streamSSE(c, async (stream) => {
|
|
26
|
+
log(`[sse] open ${channel.route}`);
|
|
27
|
+
for (const operation of operations) {
|
|
28
|
+
const message = generateMessage(channel, operation.messages[0]?.id);
|
|
29
|
+
if (message) {
|
|
30
|
+
await stream.writeSSE({ data: message.data, event: message.event });
|
|
31
|
+
onMessage?.({ channel: channel.id, direction: 'out', payload: message.data });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
log(`[sse] close ${channel.route}`);
|
|
35
|
+
}));
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { OpenAPIV3_1 } from '@scalar/openapi-types';
|
|
2
|
+
import type { Hono } from 'hono';
|
|
3
|
+
import type { UpgradeWebSocket } from 'hono/ws';
|
|
4
|
+
/**
|
|
5
|
+
* A message resolved from an AsyncAPI channel/operation, with `$ref`s already resolved.
|
|
6
|
+
* The `payload` is a plain JSON Schema ready to feed to `getExampleFromSchema`.
|
|
7
|
+
*/
|
|
8
|
+
export type ResolvedMessage = {
|
|
9
|
+
/** The message id (its key in `channel.messages`), used as the default event name. */
|
|
10
|
+
id: string;
|
|
11
|
+
/** The JSON Schema for the message payload, or `undefined` when the message has no payload. */
|
|
12
|
+
payload?: OpenAPIV3_1.SchemaObject;
|
|
13
|
+
/** Named/inline examples defined on the message, preferred over generated payloads. */
|
|
14
|
+
examples: unknown[];
|
|
15
|
+
/** Content type used to encode the payload (defaults to the document's `defaultContentType`). */
|
|
16
|
+
contentType?: string;
|
|
17
|
+
};
|
|
18
|
+
/** A single AsyncAPI operation (`send`/`receive`) bound to its channel. */
|
|
19
|
+
export type ResolvedOperation = {
|
|
20
|
+
/** The operation id (its key in the document's `operations` map). */
|
|
21
|
+
id: string;
|
|
22
|
+
/**
|
|
23
|
+
* `send`: the application sends to the channel, so the mock *receives* a client message
|
|
24
|
+
* and echoes a reply. `receive`: the application receives from the channel, so the mock
|
|
25
|
+
* *pushes* a message to the client.
|
|
26
|
+
*/
|
|
27
|
+
action: 'send' | 'receive';
|
|
28
|
+
/** Messages this operation may carry (a subset of the channel's messages, or all of them). */
|
|
29
|
+
messages: ResolvedMessage[];
|
|
30
|
+
};
|
|
31
|
+
/** A normalized, dereferenced view of an AsyncAPI channel that a transport can serve. */
|
|
32
|
+
export type ResolvedChannel = {
|
|
33
|
+
/** The channel id (its key in the document's `channels` map). */
|
|
34
|
+
id: string;
|
|
35
|
+
/** The raw channel address, e.g. `user/signedup` or `rooms/{roomId}`. */
|
|
36
|
+
address: string;
|
|
37
|
+
/** Hono-style route derived from the address (`rooms/{roomId}` -> `/rooms/:roomId`). */
|
|
38
|
+
route: string;
|
|
39
|
+
/** Connection protocols advertised by the channel's servers, e.g. `['ws']`, `['sse']`. */
|
|
40
|
+
protocols: string[];
|
|
41
|
+
/** Operations targeting this channel. */
|
|
42
|
+
operations: ResolvedOperation[];
|
|
43
|
+
/** All messages declared on the channel. */
|
|
44
|
+
messages: ResolvedMessage[];
|
|
45
|
+
};
|
|
46
|
+
/** An encoded frame ready to write to a transport, plus the optional event name. */
|
|
47
|
+
export type MockMessage = {
|
|
48
|
+
/** The wire payload (already encoded per the message `contentType`). */
|
|
49
|
+
data: string;
|
|
50
|
+
/** Event name (the message id), surfaced by transports that support named events (SSE). */
|
|
51
|
+
event?: string;
|
|
52
|
+
};
|
|
53
|
+
/** Direction of a message relative to the mock server. */
|
|
54
|
+
export type MessageDirection = 'in' | 'out';
|
|
55
|
+
/**
|
|
56
|
+
* Everything a transport needs to register a channel on the running server. The core builds
|
|
57
|
+
* this once and passes it to every transport's {@link MockTransport.register}.
|
|
58
|
+
*/
|
|
59
|
+
export type TransportContext = {
|
|
60
|
+
/** The Hono app. HTTP-based transports (SSE, SignalR negotiate) register routes here. */
|
|
61
|
+
app: Hono;
|
|
62
|
+
/**
|
|
63
|
+
* Hono's WebSocket upgrade helper, bound to the Node adapter. Upgrade-based transports
|
|
64
|
+
* (WebSocket, SignalR) register sockets via `app.get(route, upgradeWebSocket(...))`.
|
|
65
|
+
*/
|
|
66
|
+
upgradeWebSocket: UpgradeWebSocket;
|
|
67
|
+
/**
|
|
68
|
+
* Generate an encoded mock message for a channel. When `messageId` is omitted the first
|
|
69
|
+
* available message is used. Returns `null` when the channel declares no messages.
|
|
70
|
+
*/
|
|
71
|
+
generateMessage: (channel: ResolvedChannel, messageId?: string) => MockMessage | null;
|
|
72
|
+
/** Notifies the caller about every message flowing in or out, for logging/inspection. */
|
|
73
|
+
onMessage?: (event: {
|
|
74
|
+
channel: string;
|
|
75
|
+
direction: MessageDirection;
|
|
76
|
+
payload: unknown;
|
|
77
|
+
}) => void;
|
|
78
|
+
/** Structured logger for transport lifecycle lines. */
|
|
79
|
+
log: (line: string) => void;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* A pluggable transport that knows how to serve a class of AsyncAPI channels (by protocol).
|
|
83
|
+
* Built-in transports cover WebSocket and SSE; consumers can append their own (e.g. SignalR)
|
|
84
|
+
* via {@link AsyncApiMockServerOptions.transports} without changing the core.
|
|
85
|
+
*/
|
|
86
|
+
export type MockTransport = {
|
|
87
|
+
/** Unique transport id, e.g. `websocket`, `sse`. */
|
|
88
|
+
name: string;
|
|
89
|
+
/** Whether this transport should own the given channel. The first match wins. */
|
|
90
|
+
supports: (channel: ResolvedChannel) => boolean;
|
|
91
|
+
/** Register the channel on the server. Called once per matching channel at startup. */
|
|
92
|
+
register: (channel: ResolvedChannel, context: TransportContext) => void;
|
|
93
|
+
};
|
|
94
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/transports/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAChC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE/C;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,sFAAsF;IACtF,EAAE,EAAE,MAAM,CAAA;IACV,+FAA+F;IAC/F,OAAO,CAAC,EAAE,WAAW,CAAC,YAAY,CAAA;IAClC,uFAAuF;IACvF,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,iGAAiG;IACjG,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAED,2EAA2E;AAC3E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,qEAAqE;IACrE,EAAE,EAAE,MAAM,CAAA;IACV;;;;OAIG;IACH,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B,8FAA8F;IAC9F,QAAQ,EAAE,eAAe,EAAE,CAAA;CAC5B,CAAA;AAED,yFAAyF;AACzF,MAAM,MAAM,eAAe,GAAG;IAC5B,iEAAiE;IACjE,EAAE,EAAE,MAAM,CAAA;IACV,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAA;IACf,wFAAwF;IACxF,KAAK,EAAE,MAAM,CAAA;IACb,0FAA0F;IAC1F,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,yCAAyC;IACzC,UAAU,EAAE,iBAAiB,EAAE,CAAA;IAC/B,4CAA4C;IAC5C,QAAQ,EAAE,eAAe,EAAE,CAAA;CAC5B,CAAA;AAED,oFAAoF;AACpF,MAAM,MAAM,WAAW,GAAG;IACxB,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAA;IACZ,2FAA2F;IAC3F,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED,0DAA0D;AAC1D,MAAM,MAAM,gBAAgB,GAAG,IAAI,GAAG,KAAK,CAAA;AAE3C;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,yFAAyF;IACzF,GAAG,EAAE,IAAI,CAAA;IACT;;;OAGG;IACH,gBAAgB,EAAE,gBAAgB,CAAA;IAClC;;;OAGG;IACH,eAAe,EAAE,CAAC,OAAO,EAAE,eAAe,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,WAAW,GAAG,IAAI,CAAA;IACrF,yFAAyF;IACzF,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,gBAAgB,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAA;IAC/F,uDAAuD;IACvD,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAC5B,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAA;IACZ,iFAAiF;IACjF,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAA;IAC/C,uFAAuF;IACvF,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAA;CACxE,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { MockTransport } from '../transports/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Built-in WebSocket transport. Serves channels whose servers speak `ws`/`wss` by upgrading the
|
|
4
|
+
* channel route to a WebSocket connection.
|
|
5
|
+
*
|
|
6
|
+
* - `receive` operations (server push): one message is emitted per operation when a client connects.
|
|
7
|
+
* - `send` operations (client -> server): each inbound frame is logged and echoed with a generated reply.
|
|
8
|
+
*/
|
|
9
|
+
export declare const websocketTransport: MockTransport;
|
|
10
|
+
//# sourceMappingURL=websocket.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../../src/transports/websocket.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAEvD;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,EAAE,aA6ChC,CAAA"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in WebSocket transport. Serves channels whose servers speak `ws`/`wss` by upgrading the
|
|
3
|
+
* channel route to a WebSocket connection.
|
|
4
|
+
*
|
|
5
|
+
* - `receive` operations (server push): one message is emitted per operation when a client connects.
|
|
6
|
+
* - `send` operations (client -> server): each inbound frame is logged and echoed with a generated reply.
|
|
7
|
+
*/
|
|
8
|
+
export const websocketTransport = {
|
|
9
|
+
name: 'websocket',
|
|
10
|
+
supports: (channel) => channel.protocols.includes('ws') || channel.protocols.includes('wss'),
|
|
11
|
+
register: (channel, context) => {
|
|
12
|
+
const { app, upgradeWebSocket, generateMessage, onMessage, log } = context;
|
|
13
|
+
const receiveOperations = channel.operations.filter((operation) => operation.action === 'receive');
|
|
14
|
+
const sendOperation = channel.operations.find((operation) => operation.action === 'send');
|
|
15
|
+
app.get(channel.route, upgradeWebSocket(() => ({
|
|
16
|
+
onOpen: (_event, ws) => {
|
|
17
|
+
log(`[ws] open ${channel.route}`);
|
|
18
|
+
// Push a single message per receive operation on connect (quiet, deterministic default).
|
|
19
|
+
for (const operation of receiveOperations) {
|
|
20
|
+
const message = generateMessage(channel, operation.messages[0]?.id);
|
|
21
|
+
if (message) {
|
|
22
|
+
ws.send(message.data);
|
|
23
|
+
onMessage?.({ channel: channel.id, direction: 'out', payload: message.data });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
onMessage: (event, ws) => {
|
|
28
|
+
const incoming = typeof event.data === 'string' ? event.data : '[binary]';
|
|
29
|
+
log(`[ws] recv ${channel.route}: ${incoming}`);
|
|
30
|
+
onMessage?.({ channel: channel.id, direction: 'in', payload: incoming });
|
|
31
|
+
// Only echo a reply when the channel declares a `send` operation. Receive-only channels
|
|
32
|
+
// (server push) stay quiet on inbound frames instead of fabricating an unsolicited reply.
|
|
33
|
+
if (!sendOperation) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const reply = generateMessage(channel, sendOperation.messages[0]?.id);
|
|
37
|
+
if (reply) {
|
|
38
|
+
ws.send(reply.data);
|
|
39
|
+
onMessage?.({ channel: channel.id, direction: 'out', payload: reply.data });
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
onClose: () => log(`[ws] close ${channel.route}`),
|
|
43
|
+
})));
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for turning string-encoded request parameters back into the structured values that JSON
|
|
3
|
+
* Schema validation expects, following the OpenAPI `style`/`explode` serialization rules.
|
|
4
|
+
*
|
|
5
|
+
* @see https://spec.openapis.org/oas/v3.1.1.html#style-values
|
|
6
|
+
*/
|
|
7
|
+
/** OpenAPI parameter location, which determines the default serialization style. */
|
|
8
|
+
export type ParameterLocation = 'path' | 'query' | 'header' | 'cookie';
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the effective `style` and `explode` for a parameter, applying the OpenAPI defaults.
|
|
11
|
+
*
|
|
12
|
+
* `explode` defaults to `true` only for the `form` style and `false` for every other style, so the
|
|
13
|
+
* default depends on the resolved style rather than the location alone.
|
|
14
|
+
*/
|
|
15
|
+
export declare const resolveSerialization: (location: ParameterLocation, style?: string, explode?: boolean) => {
|
|
16
|
+
style: string;
|
|
17
|
+
explode: boolean;
|
|
18
|
+
};
|
|
19
|
+
/** Whether a resolved schema describes an array, including the OpenAPI 3.1 `type: ['array', 'null']` form. */
|
|
20
|
+
export declare const isArraySchema: (schema: Record<string, unknown> | undefined) => boolean;
|
|
21
|
+
/** Whether a resolved schema describes an object, including the OpenAPI 3.1 `type: ['object', 'null']` form. */
|
|
22
|
+
export declare const isObjectSchema: (schema: Record<string, unknown> | undefined) => boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Collect the declared property names of an object schema, looking through `anyOf`/`oneOf`/`allOf`.
|
|
25
|
+
*
|
|
26
|
+
* `isObjectSchema` unwraps composed schemas (for example an optional object written as
|
|
27
|
+
* `anyOf: [{ type: 'object', properties: {…} }, { type: 'null' }]`), so property extraction has to do the
|
|
28
|
+
* same. Otherwise the names live on a subschema, the top level looks empty, and exploded `form` objects
|
|
29
|
+
* fall back to free-form gathering — claiming unrelated keys and failing `additionalProperties: false`.
|
|
30
|
+
*/
|
|
31
|
+
export declare const getObjectPropertyNames: (schema: Record<string, unknown> | undefined) => string[];
|
|
32
|
+
/**
|
|
33
|
+
* Deserialize a string-encoded array parameter into its elements.
|
|
34
|
+
*
|
|
35
|
+
* Returns `undefined` when the parameter is absent so the caller can enforce `required` separately.
|
|
36
|
+
* Object parameters (`deepObject`, simple/form objects) are not handled yet and are validated as-is.
|
|
37
|
+
*/
|
|
38
|
+
export declare const deserializeArrayParameter: ({ style, explode, single, multi, }: {
|
|
39
|
+
style: string;
|
|
40
|
+
explode: boolean;
|
|
41
|
+
/** The single, joined value (for example `1,2,3`) as read from path, query, header, or cookie */
|
|
42
|
+
single: string | undefined;
|
|
43
|
+
/** Every repeated value (query only, for example `?id=1&id=2` becomes `['1', '2']`) */
|
|
44
|
+
multi?: string[] | undefined;
|
|
45
|
+
}) => string[] | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Deserialize a string-encoded object parameter into its properties, following the OpenAPI
|
|
48
|
+
* `style`/`explode` rules. Property values stay as strings so the caller can coerce them against the
|
|
49
|
+
* object's property schemas.
|
|
50
|
+
*
|
|
51
|
+
* Returns `undefined` when the parameter is absent so the caller can enforce `required` separately.
|
|
52
|
+
* Property values are strings, except for repeated query keys (an array-valued property such as
|
|
53
|
+
* `filter[tags]=a&filter[tags]=b`), which stay as a string array.
|
|
54
|
+
*/
|
|
55
|
+
export declare const deserializeObjectParameter: ({ style, explode, single, map, name, propertyNames, reservedKeys, }: {
|
|
56
|
+
style: string;
|
|
57
|
+
explode: boolean;
|
|
58
|
+
/** The single value (for example `R,100,G,200`) as read from path, query, header, or cookie */
|
|
59
|
+
single: string | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* The full key/value map for the location, needed for `deepObject` (query only) and exploded `form`
|
|
62
|
+
* objects (query top-level keys, or one cookie per property). A key with repeated query values carries
|
|
63
|
+
* a string array so array-valued object properties survive deserialization.
|
|
64
|
+
*/
|
|
65
|
+
map?: Record<string, string | string[]> | undefined;
|
|
66
|
+
name: string;
|
|
67
|
+
/** Declared object property names, used to gather exploded `form` objects from the location map */
|
|
68
|
+
propertyNames?: string[] | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* Names of the other parameters declared in the same location. A free-form exploded object (no declared
|
|
71
|
+
* properties) claims the remaining keys, so these are excluded to avoid swallowing a sibling parameter's
|
|
72
|
+
* value (for example a required free-form `meta` must not be satisfied by a `limit` query key).
|
|
73
|
+
*/
|
|
74
|
+
reservedKeys?: Set<string> | undefined;
|
|
75
|
+
}) => Record<string, string | string[]> | undefined;
|
|
76
|
+
//# sourceMappingURL=deserialize-parameter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deserialize-parameter.d.ts","sourceRoot":"","sources":["../../src/utils/deserialize-parameter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,oFAAoF;AACpF,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAUtE;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,GAC/B,UAAU,iBAAiB,EAC3B,QAAQ,MAAM,EACd,UAAU,OAAO,KAChB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAGnC,CAAA;AA2BD,8GAA8G;AAC9G,eAAO,MAAM,aAAa,GAAI,QAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,KAAG,OAU3E,CAAA;AAED,gHAAgH;AAChH,eAAO,MAAM,cAAc,GAAI,QAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,KAAG,OAU5E,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,GAAI,QAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,KAAG,MAAM,EA4B1F,CAAA;AAgGD;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,GAAI,oCAKvC;IACD,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,OAAO,CAAA;IAChB,iGAAiG;IACjG,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B,uFAAuF;IACvF,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;CAC7B,KAAG,MAAM,EAAE,GAAG,SAoBd,CAAA;AAgCD;;;;;;;;GAQG;AACH,eAAO,MAAM,0BAA0B,GAAI,qEAQxC;IACD,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,OAAO,CAAA;IAChB,+FAA+F;IAC/F,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,SAAS,CAAA;IACnD,IAAI,EAAE,MAAM,CAAA;IACZ,mGAAmG;IACnG,aAAa,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;IACpC;;;;OAIG;IACH,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,SAAS,CAAA;CACvC,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,SAoEvC,CAAA"}
|