@scalar/mock-server 0.11.0 → 0.11.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 +8 -0
- package/dist/routes/mock-any-response.d.ts.map +1 -1
- package/dist/routes/mock-any-response.js +9 -7
- package/dist/routes/mock-handler-response.d.ts.map +1 -1
- package/dist/routes/mock-handler-response.js +6 -5
- package/dist/utils/build-handler-context.d.ts.map +1 -1
- package/dist/utils/build-handler-context.js +6 -4
- package/dist/utils/find-preferred-response-key.d.ts +14 -1
- package/dist/utils/find-preferred-response-key.d.ts.map +1 -1
- package/dist/utils/find-preferred-response-key.js +41 -7
- package/dist/utils/normalize-response-body.d.ts +5 -0
- package/dist/utils/normalize-response-body.d.ts.map +1 -0
- package/dist/utils/normalize-response-body.js +25 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @scalar/mock-server
|
|
2
2
|
|
|
3
|
+
## 0.11.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#9518](https://github.com/scalar/scalar/pull/9518): fix: compact generated JSON mock responses so finite JSONL and NDJSON bodies stay valid as single-line events.
|
|
8
|
+
- [#9517](https://github.com/scalar/scalar/pull/9517): fix: improve default response selection when mocking operations with multiple response codes. The mock server now prefers the lowest 2xx success, then the lowest non-informational code, then the `default` catch-all, only falling back to a 1xx response when nothing else is defined. Status code range patterns like `2XX` are supported and treated as their lowest member, with explicit codes taking precedence over the range that covers them.
|
|
9
|
+
- [#9519](https://github.com/scalar/scalar/pull/9519): fix: wrap singular examples in arrays when the response schema is an array.
|
|
10
|
+
|
|
3
11
|
## 0.11.0
|
|
4
12
|
|
|
5
13
|
### Minor Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mock-any-response.d.ts","sourceRoot":"","sources":["../../src/routes/mock-any-response.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAIxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;
|
|
1
|
+
{"version":3,"file":"mock-any-response.d.ts","sourceRoot":"","sources":["../../src/routes/mock-any-response.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAIxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AAOxD;;GAEG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,eAAe;;4PAwGjF"}
|
|
@@ -4,6 +4,7 @@ import { getResolvedRefDeep } from '@scalar/workspace-store/helpers/get-resolved
|
|
|
4
4
|
import { getExampleFromSchema } from '@scalar/workspace-store/request-example';
|
|
5
5
|
import { accepts } from 'hono/accepts';
|
|
6
6
|
import { findPreferredResponseKey } from '../utils/find-preferred-response-key.js';
|
|
7
|
+
import { normalizeResponseBody } from '../utils/normalize-response-body.js';
|
|
7
8
|
import { parsePreferHeader } from '../utils/parse-prefer-header.js';
|
|
8
9
|
import { selectResponseExample } from '../utils/select-response-example.js';
|
|
9
10
|
/**
|
|
@@ -26,8 +27,8 @@ export function mockAnyResponse(c, operation) {
|
|
|
26
27
|
c.status(500);
|
|
27
28
|
return c.json({ error: 'No response defined for this operation.' });
|
|
28
29
|
}
|
|
29
|
-
// Status code
|
|
30
|
-
const statusCode = Number.parseInt(responseKey
|
|
30
|
+
// Status code. `default` and range patterns like `2XX` map to their lowest concrete code (e.g. 200).
|
|
31
|
+
const statusCode = Number.parseInt(responseKey && responseKey !== 'default' ? responseKey.replace(/XX$/i, '00') : '200', 10);
|
|
31
32
|
// Headers
|
|
32
33
|
const headers = selectedResponse?.headers ?? {};
|
|
33
34
|
Object.keys(headers).forEach((header) => {
|
|
@@ -63,14 +64,15 @@ export function mockAnyResponse(c, operation) {
|
|
|
63
64
|
// Body: a named/singular/first example if one is defined, otherwise generate
|
|
64
65
|
// a value from the schema. `Prefer: example=<name>` picks a named example.
|
|
65
66
|
const selectedExample = selectResponseExample(acceptedResponse, prefer.example);
|
|
67
|
+
const responseSchema = acceptedResponse?.schema ? getResolvedRefDeep(acceptedResponse.schema) : undefined;
|
|
66
68
|
const body = selectedExample
|
|
67
|
-
? selectedExample.value
|
|
68
|
-
:
|
|
69
|
-
? getExampleFromSchema(
|
|
69
|
+
? normalizeResponseBody(selectedExample.value, responseSchema)
|
|
70
|
+
: responseSchema
|
|
71
|
+
? normalizeResponseBody(getExampleFromSchema(responseSchema, {
|
|
70
72
|
emptyString: 'string',
|
|
71
73
|
variables: c.req.param(),
|
|
72
74
|
mode: 'read',
|
|
73
|
-
})
|
|
75
|
+
}), responseSchema)
|
|
74
76
|
: null;
|
|
75
77
|
c.status(statusCode);
|
|
76
78
|
return c.body(
|
|
@@ -82,7 +84,7 @@ export function mockAnyResponse(c, operation) {
|
|
|
82
84
|
acceptedContentType?.includes('xml')
|
|
83
85
|
? json2xml(body)
|
|
84
86
|
: // JSON
|
|
85
|
-
JSON.stringify(body
|
|
87
|
+
JSON.stringify(body)
|
|
86
88
|
: typeof body === 'string'
|
|
87
89
|
? // String
|
|
88
90
|
body
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mock-handler-response.d.ts","sourceRoot":"","sources":["../../src/routes/mock-handler-response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"mock-handler-response.d.ts","sourceRoot":"","sources":["../../src/routes/mock-handler-response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAGxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AAoIxD;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,eAAe,gMA8D3F"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { getResolvedRef } from '@scalar/workspace-store/helpers/get-resolved-ref';
|
|
2
|
-
import { getResolvedRefDeep } from '@scalar/workspace-store/helpers/get-resolved-ref-deep';
|
|
3
2
|
import { getExampleFromSchema } from '@scalar/workspace-store/request-example';
|
|
4
3
|
import { accepts } from 'hono/accepts';
|
|
5
4
|
import { buildHandlerContext } from '../utils/build-handler-context.js';
|
|
6
5
|
import { executeHandler } from '../utils/execute-handler.js';
|
|
6
|
+
import { normalizeResponseBody } from '../utils/normalize-response-body.js';
|
|
7
7
|
import { parsePreferHeader } from '../utils/parse-prefer-header.js';
|
|
8
8
|
import { selectResponseExample } from '../utils/select-response-example.js';
|
|
9
9
|
/**
|
|
@@ -40,16 +40,17 @@ function getExampleFromResponse(c, statusCode, responses, exampleName) {
|
|
|
40
40
|
if (!acceptedResponse) {
|
|
41
41
|
return null;
|
|
42
42
|
}
|
|
43
|
+
const responseSchema = acceptedResponse.schema ? getResolvedRef(acceptedResponse.schema) : undefined;
|
|
43
44
|
// Extract example (named, singular, or first) or generate from schema
|
|
44
45
|
const selectedExample = selectResponseExample(acceptedResponse, exampleName);
|
|
45
46
|
return selectedExample
|
|
46
|
-
? selectedExample.value
|
|
47
|
-
:
|
|
48
|
-
? getExampleFromSchema(
|
|
47
|
+
? normalizeResponseBody(selectedExample.value, responseSchema)
|
|
48
|
+
: responseSchema
|
|
49
|
+
? normalizeResponseBody(getExampleFromSchema(responseSchema, {
|
|
49
50
|
emptyString: 'string',
|
|
50
51
|
variables: c.req.param(),
|
|
51
52
|
mode: 'read',
|
|
52
|
-
})
|
|
53
|
+
}), responseSchema)
|
|
53
54
|
: null;
|
|
54
55
|
}
|
|
55
56
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-handler-context.d.ts","sourceRoot":"","sources":["../../src/utils/build-handler-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAIxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"build-handler-context.d.ts","sourceRoot":"","sources":["../../src/utils/build-handler-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAIxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAKnC,OAAO,EAAE,KAAK,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAEjF;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,cAAc,CAAC,CAAA;IAC5D,KAAK,EAAE,OAAO,KAAK,CAAA;IACnB,GAAG,EAAE;QACH,IAAI,EAAE,GAAG,CAAA;QACT,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC9B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAChC,CAAA;IACD,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CACzB,CAAA;AAED;;GAEG;AACH,KAAK,oBAAoB,GAAG;IAC1B,OAAO,EAAE,cAAc,CAAA;IACvB,QAAQ,EAAE,sBAAsB,CAAA;CACjC,CAAA;AA4DD;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,CAAC,EAAE,OAAO,EACV,SAAS,CAAC,EAAE,WAAW,CAAC,eAAe,GACtC,OAAO,CAAC,oBAAoB,CAAC,CAiD/B"}
|
|
@@ -4,6 +4,7 @@ import { getResolvedRefDeep } from '@scalar/workspace-store/helpers/get-resolved
|
|
|
4
4
|
import { getExampleFromSchema } from '@scalar/workspace-store/request-example';
|
|
5
5
|
import { accepts } from 'hono/accepts';
|
|
6
6
|
import { store } from '../libs/store.js';
|
|
7
|
+
import { normalizeResponseBody } from './normalize-response-body.js';
|
|
7
8
|
import { createStoreWrapper } from './store-wrapper.js';
|
|
8
9
|
/**
|
|
9
10
|
* Get example response from OpenAPI spec for a given status code.
|
|
@@ -34,15 +35,16 @@ function getExampleFromResponse(c, statusCode, responses) {
|
|
|
34
35
|
if (!acceptedResponse) {
|
|
35
36
|
return null;
|
|
36
37
|
}
|
|
38
|
+
const responseSchema = acceptedResponse.schema ? getResolvedRefDeep(acceptedResponse.schema) : undefined;
|
|
37
39
|
// Extract example from example property or generate from schema
|
|
38
40
|
return acceptedResponse.example !== undefined
|
|
39
|
-
? acceptedResponse.example
|
|
40
|
-
:
|
|
41
|
-
? getExampleFromSchema(
|
|
41
|
+
? normalizeResponseBody(acceptedResponse.example, responseSchema)
|
|
42
|
+
: responseSchema
|
|
43
|
+
? normalizeResponseBody(getExampleFromSchema(responseSchema, {
|
|
42
44
|
emptyString: 'string',
|
|
43
45
|
variables: c.req.param(),
|
|
44
46
|
mode: 'read',
|
|
45
|
-
})
|
|
47
|
+
}), responseSchema)
|
|
46
48
|
: null;
|
|
47
49
|
}
|
|
48
50
|
/**
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Find the preferred response key
|
|
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":"
|
|
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
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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,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
|
+
};
|
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"swagger",
|
|
17
17
|
"cli"
|
|
18
18
|
],
|
|
19
|
-
"version": "0.11.
|
|
19
|
+
"version": "0.11.1",
|
|
20
20
|
"engines": {
|
|
21
21
|
"node": ">=22"
|
|
22
22
|
},
|
|
@@ -57,10 +57,10 @@
|
|
|
57
57
|
"hono": "^4.12.7",
|
|
58
58
|
"yaml": "^2.8.3",
|
|
59
59
|
"@scalar/helpers": "0.8.2",
|
|
60
|
-
"@scalar/json-magic": "0.12.16",
|
|
61
60
|
"@scalar/openapi-types": "0.9.1",
|
|
61
|
+
"@scalar/json-magic": "0.12.16",
|
|
62
62
|
"@scalar/openapi-upgrader": "0.2.9",
|
|
63
|
-
"@scalar/workspace-store": "0.54.
|
|
63
|
+
"@scalar/workspace-store": "0.54.4"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/node": "^24.1.0",
|