@scalar/mock-server 0.11.0 → 0.12.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/create-asyncapi-mock-server.d.ts +51 -0
  3. package/dist/create-asyncapi-mock-server.d.ts.map +1 -0
  4. package/dist/create-asyncapi-mock-server.js +51 -0
  5. package/dist/index.d.ts +4 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +3 -0
  8. package/dist/routes/mock-any-response.d.ts.map +1 -1
  9. package/dist/routes/mock-any-response.js +9 -7
  10. package/dist/routes/mock-handler-response.d.ts.map +1 -1
  11. package/dist/routes/mock-handler-response.js +6 -5
  12. package/dist/transports/index.d.ts +9 -0
  13. package/dist/transports/index.d.ts.map +1 -0
  14. package/dist/transports/index.js +9 -0
  15. package/dist/transports/sse.d.ts +11 -0
  16. package/dist/transports/sse.d.ts.map +1 -0
  17. package/dist/transports/sse.js +37 -0
  18. package/dist/transports/types.d.ts +94 -0
  19. package/dist/transports/types.d.ts.map +1 -0
  20. package/dist/transports/types.js +1 -0
  21. package/dist/transports/websocket.d.ts +10 -0
  22. package/dist/transports/websocket.d.ts.map +1 -0
  23. package/dist/transports/websocket.js +45 -0
  24. package/dist/utils/build-handler-context.d.ts.map +1 -1
  25. package/dist/utils/build-handler-context.js +6 -4
  26. package/dist/utils/deserialize-parameter.d.ts +76 -0
  27. package/dist/utils/deserialize-parameter.d.ts.map +1 -0
  28. package/dist/utils/deserialize-parameter.js +297 -0
  29. package/dist/utils/find-preferred-response-key.d.ts +14 -1
  30. package/dist/utils/find-preferred-response-key.d.ts.map +1 -1
  31. package/dist/utils/find-preferred-response-key.js +41 -7
  32. package/dist/utils/generate-message.d.ts +12 -0
  33. package/dist/utils/generate-message.d.ts.map +1 -0
  34. package/dist/utils/generate-message.js +41 -0
  35. package/dist/utils/normalize-response-body.d.ts +5 -0
  36. package/dist/utils/normalize-response-body.d.ts.map +1 -0
  37. package/dist/utils/normalize-response-body.js +25 -0
  38. package/dist/utils/process-asyncapi-document.d.ts +23 -0
  39. package/dist/utils/process-asyncapi-document.d.ts.map +1 -0
  40. package/dist/utils/process-asyncapi-document.js +56 -0
  41. package/dist/utils/resolve-channels.d.ts +11 -0
  42. package/dist/utils/resolve-channels.d.ts.map +1 -0
  43. package/dist/utils/resolve-channels.js +100 -0
  44. package/dist/utils/validate-request.d.ts +2 -2
  45. package/dist/utils/validate-request.d.ts.map +1 -1
  46. package/dist/utils/validate-request.js +173 -22
  47. package/package.json +5 -3
@@ -0,0 +1,297 @@
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
+ /** Default serialization style per parameter location. */
8
+ const DEFAULT_STYLE = {
9
+ query: 'form',
10
+ cookie: 'form',
11
+ path: 'simple',
12
+ header: 'simple',
13
+ };
14
+ /**
15
+ * Resolve the effective `style` and `explode` for a parameter, applying the OpenAPI defaults.
16
+ *
17
+ * `explode` defaults to `true` only for the `form` style and `false` for every other style, so the
18
+ * default depends on the resolved style rather than the location alone.
19
+ */
20
+ export const resolveSerialization = (location, style, explode) => {
21
+ const resolvedStyle = style ?? DEFAULT_STYLE[location];
22
+ return { style: resolvedStyle, explode: explode ?? resolvedStyle === 'form' };
23
+ };
24
+ /**
25
+ * Whether any composed subschema satisfies the predicate, looking through `anyOf`/`oneOf`/`allOf`.
26
+ *
27
+ * Optional array/object parameters are commonly described as `anyOf: [{ type: 'array' }, { type: 'null' }]`
28
+ * (for example FastAPI/Pydantic `Optional[List[str]]`). Without unwrapping these we would treat the value
29
+ * as a plain string and skip style-aware deserialization. Mirrors `getStructuredType` in
30
+ * `@scalar/workspace-store`'s `de-serialize-parameter`.
31
+ */
32
+ const matchesComposedSchema = (schema, predicate) => {
33
+ for (const keyword of ['anyOf', 'oneOf', 'allOf']) {
34
+ const subSchemas = schema[keyword];
35
+ if (Array.isArray(subSchemas)) {
36
+ for (const subSchema of subSchemas) {
37
+ if (subSchema && typeof subSchema === 'object' && predicate(subSchema)) {
38
+ return true;
39
+ }
40
+ }
41
+ }
42
+ }
43
+ return false;
44
+ };
45
+ /** Whether a resolved schema describes an array, including the OpenAPI 3.1 `type: ['array', 'null']` form. */
46
+ export const isArraySchema = (schema) => {
47
+ if (!schema) {
48
+ return false;
49
+ }
50
+ const type = schema.type;
51
+ if (type === 'array' || (Array.isArray(type) && type.includes('array')) || 'items' in schema) {
52
+ return true;
53
+ }
54
+ return matchesComposedSchema(schema, isArraySchema);
55
+ };
56
+ /** Whether a resolved schema describes an object, including the OpenAPI 3.1 `type: ['object', 'null']` form. */
57
+ export const isObjectSchema = (schema) => {
58
+ if (!schema) {
59
+ return false;
60
+ }
61
+ const type = schema.type;
62
+ if (type === 'object' || (Array.isArray(type) && type.includes('object')) || 'properties' in schema) {
63
+ return true;
64
+ }
65
+ return matchesComposedSchema(schema, isObjectSchema);
66
+ };
67
+ /**
68
+ * Collect the declared property names of an object schema, looking through `anyOf`/`oneOf`/`allOf`.
69
+ *
70
+ * `isObjectSchema` unwraps composed schemas (for example an optional object written as
71
+ * `anyOf: [{ type: 'object', properties: {…} }, { type: 'null' }]`), so property extraction has to do the
72
+ * same. Otherwise the names live on a subschema, the top level looks empty, and exploded `form` objects
73
+ * fall back to free-form gathering — claiming unrelated keys and failing `additionalProperties: false`.
74
+ */
75
+ export const getObjectPropertyNames = (schema) => {
76
+ if (!schema) {
77
+ return [];
78
+ }
79
+ const names = new Set();
80
+ const properties = schema.properties;
81
+ if (properties && typeof properties === 'object') {
82
+ for (const key of Object.keys(properties)) {
83
+ names.add(key);
84
+ }
85
+ }
86
+ for (const keyword of ['anyOf', 'oneOf', 'allOf']) {
87
+ const subSchemas = schema[keyword];
88
+ if (Array.isArray(subSchemas)) {
89
+ for (const subSchema of subSchemas) {
90
+ if (subSchema && typeof subSchema === 'object') {
91
+ for (const name of getObjectPropertyNames(subSchema)) {
92
+ names.add(name);
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+ return [...names];
99
+ };
100
+ const wrap = (value) => (value === undefined ? undefined : [value]);
101
+ // An empty value is an empty list, not a one-element list of the empty string. Otherwise `?ids=`
102
+ // would satisfy a `minItems: 1` array while failing the element type check on `''`.
103
+ const split = (value, delimiter) => value === undefined ? undefined : value === '' ? [] : value.split(delimiter);
104
+ /**
105
+ * Build an exploded array from the repeated values (query) or the single value (other locations).
106
+ *
107
+ * A lone empty value (`?ids=`) is an empty array, not `['']`, matching the non-exploded `split`
108
+ * behaviour so an optional empty array is not rejected on its element type and `minItems` runs as
109
+ * intended. An explicitly repeated empty (`?ids=a&ids=`) keeps every element.
110
+ */
111
+ const explodedArray = (single, multi) => {
112
+ const values = multi ?? wrap(single);
113
+ if (values === undefined) {
114
+ return undefined;
115
+ }
116
+ return values.length === 1 && values[0] === '' ? [] : values;
117
+ };
118
+ /** Drop a leading prefix (for example the `.` of `label` or the `;` of `matrix`) when present. */
119
+ const stripPrefix = (value, prefix) => value.startsWith(prefix) ? value.slice(prefix.length) : value;
120
+ /** Return the part of a `key=value` segment after the first `=`, or the whole segment when there is none. */
121
+ const valueAfterEquals = (segment) => {
122
+ const equals = segment.indexOf('=');
123
+ return equals === -1 ? segment : segment.slice(equals + 1);
124
+ };
125
+ /** Split a `matrix`-encoded value into its `;`-separated segments, dropping the leading `;` and empties. */
126
+ const matrixSegments = (value) => stripPrefix(value, ';')
127
+ .split(';')
128
+ .filter((segment) => segment.length > 0);
129
+ /**
130
+ * Split a `label`-encoded value into its parts, dropping the leading `.`. Per the OpenAPI serialization
131
+ * rules a non-exploded `label` value is comma-separated (`.blue,black,brown`, `.R,100,G,200`) while an
132
+ * exploded one is dot-separated (`.blue.black.brown`, `.R=100.G=200`). An empty value means no parts.
133
+ */
134
+ const labelParts = (value, explode) => {
135
+ const inner = stripPrefix(value, '.');
136
+ return inner === '' ? [] : inner.split(explode ? '.' : ',');
137
+ };
138
+ /** Build an object from a list of `key=value` segments (for example `['R=100', 'G=200']`). */
139
+ const pairsFromList = (parts) => {
140
+ const result = {};
141
+ for (const part of parts) {
142
+ const equals = part.indexOf('=');
143
+ if (equals !== -1) {
144
+ result[part.slice(0, equals)] = part.slice(equals + 1);
145
+ }
146
+ }
147
+ return result;
148
+ };
149
+ /** Build an object from a flat list alternating key, value (for example `['R', '100', 'G', '200']`). */
150
+ const alternatingFromList = (parts) => {
151
+ const result = {};
152
+ // Walk in pairs; a trailing key without a value is ignored.
153
+ for (let index = 0; index + 1 < parts.length; index += 2) {
154
+ const key = parts[index];
155
+ const propertyValue = parts[index + 1];
156
+ if (key !== undefined && propertyValue !== undefined) {
157
+ result[key] = propertyValue;
158
+ }
159
+ }
160
+ return result;
161
+ };
162
+ /**
163
+ * Deserialize a `matrix`-style array (path only). Non-exploded values join the elements after a single
164
+ * `;name=` prefix (`;ids=1,2,3`), while exploded values repeat the prefix per element (`;ids=1;ids=2`).
165
+ */
166
+ const parseMatrixArray = (value, explode) => {
167
+ if (value === undefined) {
168
+ return undefined;
169
+ }
170
+ const segments = matrixSegments(value);
171
+ if (explode) {
172
+ return segments.map(valueAfterEquals);
173
+ }
174
+ // Non-exploded: a single `name=a,b,c` segment whose value is comma-separated.
175
+ const [first] = segments;
176
+ return first === undefined ? [] : valueAfterEquals(first).split(',');
177
+ };
178
+ /**
179
+ * Deserialize a string-encoded array parameter into its elements.
180
+ *
181
+ * Returns `undefined` when the parameter is absent so the caller can enforce `required` separately.
182
+ * Object parameters (`deepObject`, simple/form objects) are not handled yet and are validated as-is.
183
+ */
184
+ export const deserializeArrayParameter = ({ style, explode, single, multi, }) => {
185
+ switch (style) {
186
+ case 'spaceDelimited':
187
+ return explode ? explodedArray(single, multi) : split(single, ' ');
188
+ case 'pipeDelimited':
189
+ return explode ? explodedArray(single, multi) : split(single, '|');
190
+ case 'simple':
191
+ // Path and header arrays are comma-separated; `explode` does not change the delimiter. HTTP allows
192
+ // optional whitespace after the comma in header list values (`a, b, c`), so trim each element.
193
+ return single === undefined ? undefined : single === '' ? [] : single.split(',').map((element) => element.trim());
194
+ case 'label':
195
+ // Path `label` arrays are dot-prefixed. Non-exploded values are comma-separated (`.1,2,3`),
196
+ // exploded values are dot-separated (`.1.2.3`).
197
+ return single === undefined ? undefined : labelParts(single, explode);
198
+ case 'matrix':
199
+ return parseMatrixArray(single, explode);
200
+ default:
201
+ // `form` (and any unrecognised style): an exploded array repeats the key, otherwise it is comma-joined.
202
+ return explode ? explodedArray(single, multi) : split(single, ',');
203
+ }
204
+ };
205
+ /** Parse `R,100,G,200` (a flat list alternating key, value) into an object. */
206
+ const parseAlternating = (value, delimiter) => value === undefined ? undefined : alternatingFromList(value.split(delimiter));
207
+ /** Parse `R=100,G=200` (delimiter-separated `key=value` pairs) into an object. */
208
+ const parsePairs = (value, delimiter) => value === undefined ? undefined : pairsFromList(value.split(delimiter));
209
+ /** Parse `name[R]=100&name[G]=200` from the query map into an object. */
210
+ const parseDeepObject = (query, name) => {
211
+ const prefix = `${name}[`;
212
+ const result = {};
213
+ for (const [key, value] of Object.entries(query)) {
214
+ if (!key.startsWith(prefix) || !key.endsWith(']')) {
215
+ continue;
216
+ }
217
+ const property = key.slice(prefix.length, -1);
218
+ // `deepObject` only defines a single level of nesting, so ignore keys like `name[a][b]` whose
219
+ // property still contains brackets rather than emitting a corrupt `a][b` property.
220
+ if (property.includes('[') || property.includes(']')) {
221
+ continue;
222
+ }
223
+ result[property] = value;
224
+ }
225
+ return Object.keys(result).length > 0 ? result : undefined;
226
+ };
227
+ /**
228
+ * Deserialize a string-encoded object parameter into its properties, following the OpenAPI
229
+ * `style`/`explode` rules. Property values stay as strings so the caller can coerce them against the
230
+ * object's property schemas.
231
+ *
232
+ * Returns `undefined` when the parameter is absent so the caller can enforce `required` separately.
233
+ * Property values are strings, except for repeated query keys (an array-valued property such as
234
+ * `filter[tags]=a&filter[tags]=b`), which stay as a string array.
235
+ */
236
+ export const deserializeObjectParameter = ({ style, explode, single, map, name, propertyNames, reservedKeys, }) => {
237
+ // `deepObject`: properties are encoded as bracketed query keys, e.g. `color[R]=100`.
238
+ if (style === 'deepObject') {
239
+ return map ? parseDeepObject(map, name) : undefined;
240
+ }
241
+ // Exploded `form`: each property is its own top-level key, e.g. `R=100&G=200` (one cookie per property
242
+ // for cookies). With declared properties we gather exactly those; a free-form object (no declared
243
+ // properties) claims every remaining key in the location, excluding keys owned by *other* declared
244
+ // parameters. Its own name stays claimable, so a property named like the parameter is not dropped.
245
+ if (style === 'form' && explode) {
246
+ if (!map) {
247
+ return undefined;
248
+ }
249
+ const keys = propertyNames?.length
250
+ ? propertyNames
251
+ : Object.keys(map).filter((key) => key === name || !reservedKeys?.has(key));
252
+ const result = {};
253
+ for (const property of keys) {
254
+ const value = map[property];
255
+ if (value !== undefined) {
256
+ result[property] = value;
257
+ }
258
+ }
259
+ return Object.keys(result).length > 0 ? result : undefined;
260
+ }
261
+ // Exploded `simple` (path/header): comma-separated `key=value` pairs, e.g. `R=100,G=200`.
262
+ if (style === 'simple' && explode) {
263
+ return parsePairs(single, ',');
264
+ }
265
+ // `label` (path): dot-prefixed. Exploded uses dot-separated `key=value`, e.g. `.R=100.G=200`;
266
+ // non-exploded alternates comma-separated key and value, e.g. `.R,100,G,200`.
267
+ if (style === 'label') {
268
+ if (single === undefined) {
269
+ return undefined;
270
+ }
271
+ const parts = labelParts(single, explode);
272
+ return explode ? pairsFromList(parts) : alternatingFromList(parts);
273
+ }
274
+ // `matrix` (path): semicolon-prefixed. Exploded repeats `;key=value` per property, e.g. `;R=100;G=200`;
275
+ // non-exploded carries everything in one `;name=R,100,G,200` segment.
276
+ if (style === 'matrix') {
277
+ if (single === undefined) {
278
+ return undefined;
279
+ }
280
+ const segments = matrixSegments(single);
281
+ if (explode) {
282
+ return pairsFromList(segments);
283
+ }
284
+ const [first] = segments;
285
+ return first === undefined ? {} : alternatingFromList(valueAfterEquals(first).split(','));
286
+ }
287
+ // `spaceDelimited` / `pipeDelimited` (query): a flat alternating key,value list joined by the
288
+ // delimiter, e.g. `R 100 G 200` or `R|100|G|200`. Only defined for the non-exploded form.
289
+ if (style === 'spaceDelimited') {
290
+ return parseAlternating(single, ' ');
291
+ }
292
+ if (style === 'pipeDelimited') {
293
+ return parseAlternating(single, '|');
294
+ }
295
+ // Non-exploded `form` and `simple`: a flat list alternating key, value, e.g. `R,100,G,200`.
296
+ return parseAlternating(single, ',');
297
+ };
@@ -1,5 +1,18 @@
1
1
  /**
2
- * Find the preferred response key: default, 200, 201 …
2
+ * Find the preferred response key to mock.
3
+ *
4
+ * Preference order:
5
+ * 1. The lowest 2xx success response
6
+ * 2. The lowest non-informational code (3xx/4xx/5xx)
7
+ * 3. `default` — the catch-all for undeclared responses (typically an error)
8
+ * 4. An informational 1xx response, only when nothing else is defined
9
+ *
10
+ * Within each tier an explicit code wins over the range pattern that covers it (e.g. `200` over
11
+ * `2XX`), and range patterns are treated as their lowest member (e.g. `2XX` → `200`). `default`
12
+ * is the catch-all for codes not covered individually, so a defined success or error is preferred
13
+ * over it.
14
+ *
15
+ * @see https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.2.md#responses-object
3
16
  */
4
17
  export declare function findPreferredResponseKey(responses?: string[]): string | undefined;
5
18
  //# sourceMappingURL=find-preferred-response-key.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"find-preferred-response-key.d.ts","sourceRoot":"","sources":["../../src/utils/find-preferred-response-key.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,sBAQ5D"}
1
+ {"version":3,"file":"find-preferred-response-key.d.ts","sourceRoot":"","sources":["../../src/utils/find-preferred-response-key.ts"],"names":[],"mappings":"AAwBA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,wBAAwB,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAiBjF"}
@@ -1,11 +1,45 @@
1
+ /** Matches an explicit HTTP status code (e.g. `200`) or a range pattern (e.g. `2XX`). */
2
+ const STATUS_CODE_REGEX = /^[1-5](\d{2}|XX)$/i;
3
+ /** Whether a status key is a range pattern (e.g. `2XX`) rather than an explicit code. */
4
+ const isRange = (response) => /XX$/i.test(response);
5
+ /** Whether a status key is an informational 1xx response. */
6
+ const isInformational = (response) => response.startsWith('1');
7
+ /** Whether a status key is a 2xx success response. */
8
+ const isSuccess = (response) => response.startsWith('2');
9
+ /** Turn a status key into a comparable number, treating a range like `2XX` as its lowest member (`200`). */
10
+ const toComparableStatus = (response) => Number.parseInt(response.replace(/XX$/i, '00'), 10);
11
+ const sortStatusCodes = (responses) => responses
12
+ .filter((response) => STATUS_CODE_REGEX.test(response))
13
+ .sort((left, right) => {
14
+ const difference = toComparableStatus(left) - toComparableStatus(right);
15
+ // On a tie, prefer an explicit code over a range pattern (e.g. `200` before `2XX`).
16
+ return difference !== 0 ? difference : Number(isRange(left)) - Number(isRange(right));
17
+ });
1
18
  /**
2
- * Find the preferred response key: default, 200, 201 …
19
+ * Find the preferred response key to mock.
20
+ *
21
+ * Preference order:
22
+ * 1. The lowest 2xx success response
23
+ * 2. The lowest non-informational code (3xx/4xx/5xx)
24
+ * 3. `default` — the catch-all for undeclared responses (typically an error)
25
+ * 4. An informational 1xx response, only when nothing else is defined
26
+ *
27
+ * Within each tier an explicit code wins over the range pattern that covers it (e.g. `200` over
28
+ * `2XX`), and range patterns are treated as their lowest member (e.g. `2XX` → `200`). `default`
29
+ * is the catch-all for codes not covered individually, so a defined success or error is preferred
30
+ * over it.
31
+ *
32
+ * @see https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.2.md#responses-object
3
33
  */
4
34
  export function findPreferredResponseKey(responses) {
5
- return (
6
- // Regular status codes
7
- ['default', '200', '201', '204', '404', '500'].find((key) => responses?.includes(key) ?? false) ??
8
- // Lowest status code
9
- responses?.sort()[0] ??
10
- undefined);
35
+ if (!responses?.length) {
36
+ return undefined;
37
+ }
38
+ const statusCodes = sortStatusCodes(responses);
39
+ // Within a tier, prefer an explicit status code over the range pattern that covers it.
40
+ const byPreference = (predicate) => statusCodes.find((response) => predicate(response) && !isRange(response)) ?? statusCodes.find(predicate);
41
+ return (byPreference(isSuccess) ??
42
+ byPreference((response) => !isInformational(response)) ??
43
+ (responses.includes('default') ? 'default' : undefined) ??
44
+ byPreference(isInformational));
11
45
  }
@@ -0,0 +1,12 @@
1
+ import type { MockMessage, ResolvedChannel } from '../transports/types.js';
2
+ /**
3
+ * Generate an encoded mock frame for a channel message — the AsyncAPI analogue of the REST
4
+ * mocker's response generation. Prefers a defined example, otherwise generates a value from the
5
+ * message payload schema with the same `getExampleFromSchema` the HTTP mocker uses.
6
+ *
7
+ * @param channel - The resolved channel to mock a message for.
8
+ * @param messageId - Which message to emit; defaults to the channel's first message.
9
+ * @returns The encoded message, or `null` when the channel declares no messages.
10
+ */
11
+ export declare function generateMessage(channel: ResolvedChannel, messageId?: string): MockMessage | null;
12
+ //# sourceMappingURL=generate-message.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-message.d.ts","sourceRoot":"","sources":["../../src/utils/generate-message.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAmB,MAAM,oBAAoB,CAAA;AAWvF;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,eAAe,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAwBhG"}
@@ -0,0 +1,41 @@
1
+ import { getResolvedRefDeep } from '@scalar/workspace-store/helpers/get-resolved-ref-deep';
2
+ import { getExampleFromSchema } from '@scalar/workspace-store/request-example';
3
+ /** Encode a generated value to a wire string. Strings pass through; everything else is JSON. */
4
+ function encode(value) {
5
+ if (typeof value === 'string') {
6
+ return value;
7
+ }
8
+ return JSON.stringify(value ?? null);
9
+ }
10
+ /**
11
+ * Generate an encoded mock frame for a channel message — the AsyncAPI analogue of the REST
12
+ * mocker's response generation. Prefers a defined example, otherwise generates a value from the
13
+ * message payload schema with the same `getExampleFromSchema` the HTTP mocker uses.
14
+ *
15
+ * @param channel - The resolved channel to mock a message for.
16
+ * @param messageId - Which message to emit; defaults to the channel's first message.
17
+ * @returns The encoded message, or `null` when the channel declares no messages.
18
+ */
19
+ export function generateMessage(channel, messageId) {
20
+ const message = messageId
21
+ ? channel.messages.find((candidate) => candidate.id === messageId)
22
+ : channel.messages[0];
23
+ if (!message) {
24
+ return null;
25
+ }
26
+ let value = null;
27
+ if (message.examples.length > 0) {
28
+ // Prefer an explicit example, mirroring response-example selection in the REST mocker.
29
+ value = message.examples[0];
30
+ }
31
+ else if (message.payload) {
32
+ value = getExampleFromSchema(getResolvedRefDeep(message.payload), {
33
+ emptyString: 'string',
34
+ mode: 'read',
35
+ });
36
+ }
37
+ return {
38
+ data: encode(value),
39
+ event: message.id,
40
+ };
41
+ }
@@ -0,0 +1,5 @@
1
+ import type { OpenAPIV3_1 } from '@scalar/openapi-types';
2
+ type Schema = NonNullable<OpenAPIV3_1.ComponentsObject['schemas']>[string];
3
+ export declare const normalizeResponseBody: (body: unknown, schema: Schema) => unknown;
4
+ export {};
5
+ //# sourceMappingURL=normalize-response-body.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize-response-body.d.ts","sourceRoot":"","sources":["../../src/utils/normalize-response-body.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAGxD,KAAK,MAAM,GAAG,WAAW,CAAC,WAAW,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;AA0B1E,eAAO,MAAM,qBAAqB,GAAI,MAAM,OAAO,EAAE,QAAQ,MAAM,KAAG,OAMrE,CAAA"}
@@ -0,0 +1,25 @@
1
+ import { getResolvedRef } from '@scalar/workspace-store/helpers/get-resolved-ref';
2
+ const isArrayResponseSchema = (schema) => {
3
+ if (!schema) {
4
+ return false;
5
+ }
6
+ const resolvedSchema = getResolvedRef(schema);
7
+ if (!resolvedSchema || typeof resolvedSchema !== 'object') {
8
+ return false;
9
+ }
10
+ if ('type' in resolvedSchema) {
11
+ if (resolvedSchema.type === 'array') {
12
+ return true;
13
+ }
14
+ if (Array.isArray(resolvedSchema.type) && resolvedSchema.type.includes('array')) {
15
+ return true;
16
+ }
17
+ }
18
+ return 'items' in resolvedSchema;
19
+ };
20
+ export const normalizeResponseBody = (body, schema) => {
21
+ if (body === null || body === undefined || Array.isArray(body) || !isArrayResponseSchema(schema)) {
22
+ return body;
23
+ }
24
+ return [body];
25
+ };
@@ -0,0 +1,23 @@
1
+ import type { AsyncApiDocument } from '@scalar/types/asyncapi/3.1';
2
+ /**
3
+ * Processes an AsyncAPI document by bundling external references and wrapping it so internal
4
+ * references stay intact but resolve lazily — the AsyncAPI counterpart of
5
+ * {@link processOpenApiDocument}.
6
+ *
7
+ * Unlike a full dereference, the returned document keeps `$ref` nodes in place. Consumers resolve
8
+ * them on demand with `getResolvedRef` from `@scalar/workspace-store`, which reads the `$ref-value`
9
+ * exposed by the magic proxy. This avoids eagerly flattening (and duplicating) the whole document.
10
+ *
11
+ * Only AsyncAPI 3.1 is supported; 2.x documents should be upgraded before being passed in.
12
+ *
13
+ * @param document - The AsyncAPI document to process. Can be a string (URL/path) or an object.
14
+ * @returns A promise that resolves to the AsyncAPI document with lazily resolvable references.
15
+ * @throws Error if the document cannot be processed or is invalid.
16
+ */
17
+ export declare function processAsyncApiDocument(document: string | Record<string, any> | undefined): Promise<AsyncApiDocument>;
18
+ /**
19
+ * Detects whether a loaded document describes an AsyncAPI API (rather than OpenAPI/Swagger).
20
+ * Used to route an incoming document to the right mock engine.
21
+ */
22
+ export declare function isAsyncApiDocument(document: unknown): boolean;
23
+ //# sourceMappingURL=process-asyncapi-document.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process-asyncapi-document.d.ts","sourceRoot":"","sources":["../../src/utils/process-asyncapi-document.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAElE;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,GACjD,OAAO,CAAC,gBAAgB,CAAC,CAiC3B;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAE7D"}
@@ -0,0 +1,56 @@
1
+ import { bundle } from '@scalar/json-magic/bundle';
2
+ import { fetchUrls, parseJson, parseYaml, readFiles } from '@scalar/json-magic/bundle/plugins/node';
3
+ import { createMagicProxy } from '@scalar/json-magic/magic-proxy';
4
+ /**
5
+ * Processes an AsyncAPI document by bundling external references and wrapping it so internal
6
+ * references stay intact but resolve lazily — the AsyncAPI counterpart of
7
+ * {@link processOpenApiDocument}.
8
+ *
9
+ * Unlike a full dereference, the returned document keeps `$ref` nodes in place. Consumers resolve
10
+ * them on demand with `getResolvedRef` from `@scalar/workspace-store`, which reads the `$ref-value`
11
+ * exposed by the magic proxy. This avoids eagerly flattening (and duplicating) the whole document.
12
+ *
13
+ * Only AsyncAPI 3.1 is supported; 2.x documents should be upgraded before being passed in.
14
+ *
15
+ * @param document - The AsyncAPI document to process. Can be a string (URL/path) or an object.
16
+ * @returns A promise that resolves to the AsyncAPI document with lazily resolvable references.
17
+ * @throws Error if the document cannot be processed or is invalid.
18
+ */
19
+ export async function processAsyncApiDocument(document) {
20
+ // Handle empty/undefined input gracefully with a minimal valid document.
21
+ if (!document || (typeof document === 'object' && Object.keys(document).length === 0)) {
22
+ return {
23
+ asyncapi: '3.1.0',
24
+ info: {
25
+ title: 'Mock API',
26
+ version: '1.0.0',
27
+ },
28
+ channels: {},
29
+ operations: {},
30
+ };
31
+ }
32
+ let bundled;
33
+ try {
34
+ // Bundle external references; parse string inputs (JSON or YAML) along the way.
35
+ bundled = await bundle(document, {
36
+ plugins: [parseJson(), parseYaml(), readFiles(), fetchUrls()],
37
+ treeShake: false,
38
+ });
39
+ }
40
+ catch (error) {
41
+ throw new Error(`Failed to bundle AsyncAPI document: ${error instanceof Error ? error.message : String(error)}`);
42
+ }
43
+ if (!bundled || typeof bundled !== 'object') {
44
+ throw new Error('Bundled document is invalid: expected an object');
45
+ }
46
+ // Wrap the document in a magic proxy so internal references resolve lazily via `$ref-value`.
47
+ // External references were already pulled inline by `bundle` above, so only local `$ref`s remain.
48
+ return createMagicProxy(bundled);
49
+ }
50
+ /**
51
+ * Detects whether a loaded document describes an AsyncAPI API (rather than OpenAPI/Swagger).
52
+ * Used to route an incoming document to the right mock engine.
53
+ */
54
+ export function isAsyncApiDocument(document) {
55
+ return typeof document === 'object' && document !== null && 'asyncapi' in document;
56
+ }
@@ -0,0 +1,11 @@
1
+ import type { AsyncApiDocument } from '@scalar/types/asyncapi/3.1';
2
+ import type { ResolvedChannel } from '../transports/types.js';
3
+ /**
4
+ * Normalize an AsyncAPI 3.1 document into a flat list of {@link ResolvedChannel}s a transport can
5
+ * serve. Channel, message, operation, and server resolution are delegated to
6
+ * `@scalar/workspace-store/channel-example` — the same layer the API client uses to connect to
7
+ * channels — so the mock and the client agree on how a document maps to channels and operations
8
+ * (including operation traits). This function only adapts that output into the transport types.
9
+ */
10
+ export declare function resolveChannels(document: AsyncApiDocument): ResolvedChannel[];
11
+ //# sourceMappingURL=resolve-channels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-channels.d.ts","sourceRoot":"","sources":["../../src/utils/resolve-channels.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAyB,MAAM,4BAA4B,CAAA;AAWzF,OAAO,KAAK,EAAE,eAAe,EAAsC,MAAM,oBAAoB,CAAA;AAkE7F;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,gBAAgB,GAAG,eAAe,EAAE,CA6C7E"}