@schmock/openapi 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/content-negotiation.d.ts +7 -0
- package/dist/content-negotiation.d.ts.map +1 -0
- package/dist/content-negotiation.js +46 -0
- package/dist/crud-detector.d.ts +2 -0
- package/dist/crud-detector.d.ts.map +1 -1
- package/dist/crud-detector.js +55 -8
- package/dist/generators.d.ts +27 -7
- package/dist/generators.d.ts.map +1 -1
- package/dist/generators.js +190 -18
- package/dist/index.js +159 -159
- package/dist/normalizer.js +1 -1
- package/dist/parser.d.ts +31 -4
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +208 -6
- package/dist/plugin.d.ts +9 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +506 -47
- package/dist/prefer.d.ts +12 -0
- package/dist/prefer.d.ts.map +1 -0
- package/dist/prefer.js +25 -0
- package/dist/seed.js +1 -1
- package/package.json +5 -3
- package/src/content-negotiation.ts +53 -0
- package/src/crud-detector.ts +65 -8
- package/src/generators.test.ts +270 -0
- package/src/generators.ts +237 -11
- package/src/index.ts +1 -1
- package/src/normalizer.ts +1 -1
- package/src/parser.ts +292 -12
- package/src/plugin.ts +655 -51
- package/src/prefer.ts +37 -0
- package/src/seed.ts +1 -1
- package/src/steps/callback-mocking.steps.ts +164 -0
- package/src/steps/content-negotiation.steps.ts +107 -0
- package/src/steps/errors-mode.steps.ts +95 -0
- package/src/steps/openapi-compliance.steps.ts +427 -0
- package/src/steps/prefer-header.steps.ts +140 -0
- package/src/steps/security-validation.steps.ts +183 -0
- package/src/stress.test.ts +92 -35
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Negotiate the best content type match for an Accept header.
|
|
3
|
+
* Supports quality values (q=0.8) and wildcards (*\/*).
|
|
4
|
+
* Returns the matched content type or null if no match.
|
|
5
|
+
*/
|
|
6
|
+
export declare function negotiateContentType(accept: string, available: string[]): string | null;
|
|
7
|
+
//# sourceMappingURL=content-negotiation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-negotiation.d.ts","sourceRoot":"","sources":["../src/content-negotiation.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EAAE,GAClB,MAAM,GAAG,IAAI,CA4Cf"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Negotiate the best content type match for an Accept header.
|
|
3
|
+
* Supports quality values (q=0.8) and wildcards (*\/*).
|
|
4
|
+
* Returns the matched content type or null if no match.
|
|
5
|
+
*/
|
|
6
|
+
export function negotiateContentType(accept, available) {
|
|
7
|
+
if (!accept || accept === "*/*") {
|
|
8
|
+
return available[0] ?? null;
|
|
9
|
+
}
|
|
10
|
+
// Parse Accept header into sorted entries by quality
|
|
11
|
+
const entries = accept
|
|
12
|
+
.split(",")
|
|
13
|
+
.map((part) => {
|
|
14
|
+
const [type, ...params] = part.trim().split(";");
|
|
15
|
+
let q = 1;
|
|
16
|
+
for (const param of params) {
|
|
17
|
+
const match = param.trim().match(/^q\s*=\s*([\d.]+)$/);
|
|
18
|
+
if (match) {
|
|
19
|
+
q = Number.parseFloat(match[1]);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return { type: type.trim(), q };
|
|
23
|
+
})
|
|
24
|
+
.sort((a, b) => b.q - a.q);
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
if (entry.q === 0)
|
|
27
|
+
continue;
|
|
28
|
+
// Wildcard matches anything
|
|
29
|
+
if (entry.type === "*/*") {
|
|
30
|
+
return available[0] ?? null;
|
|
31
|
+
}
|
|
32
|
+
// Type wildcard (e.g., "application/*")
|
|
33
|
+
if (entry.type.endsWith("/*")) {
|
|
34
|
+
const prefix = entry.type.slice(0, -1);
|
|
35
|
+
const match = available.find((ct) => ct.startsWith(prefix));
|
|
36
|
+
if (match)
|
|
37
|
+
return match;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
// Exact match
|
|
41
|
+
if (available.includes(entry.type)) {
|
|
42
|
+
return entry.type;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
package/dist/crud-detector.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface CrudResource {
|
|
|
14
14
|
operations: CrudOperation[];
|
|
15
15
|
/** Response schema for the resource item */
|
|
16
16
|
schema?: JSONSchema7;
|
|
17
|
+
/** Per-operation metadata auto-detected from spec */
|
|
18
|
+
operationMeta?: Map<CrudOperation, Schmock.CrudOperationMeta>;
|
|
17
19
|
}
|
|
18
20
|
interface DetectionResult {
|
|
19
21
|
resources: CrudResource[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crud-detector.d.ts","sourceRoot":"","sources":["../src/crud-detector.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"crud-detector.d.ts","sourceRoot":"","sources":["../src/crud-detector.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE7E,MAAM,WAAW,YAAY;IAC3B,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,qCAAqC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,+BAA+B;IAC/B,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,4CAA4C;IAC5C,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,qDAAqD;IACrD,aAAa,CAAC,EAAE,GAAG,CAAC,aAAa,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAC;CAC/D;AAED,UAAU,eAAe;IACvB,SAAS,EAAE,YAAY,EAAE,CAAC;IAC1B,+CAA+C;IAC/C,YAAY,EAAE,UAAU,EAAE,CAAC;CAC5B;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,eAAe,CA6BxE"}
|
package/dist/crud-detector.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
/// <reference path="
|
|
1
|
+
/// <reference path="../../core/schmock.d.ts" />
|
|
2
|
+
import { findArrayProperty } from "./generators.js";
|
|
2
3
|
import { isRecord, toJsonSchema } from "./utils.js";
|
|
3
4
|
/**
|
|
4
5
|
* Detect CRUD resource patterns from parsed OpenAPI paths.
|
|
@@ -61,25 +62,38 @@ function buildResource(basePath, paths) {
|
|
|
61
62
|
let itemPath = "";
|
|
62
63
|
let idParam = "";
|
|
63
64
|
let schema;
|
|
65
|
+
const operationMeta = new Map();
|
|
64
66
|
for (const p of paths) {
|
|
65
67
|
const isCollection = p.path === basePath;
|
|
66
68
|
const isItem = !isCollection && p.path.startsWith(basePath);
|
|
67
69
|
if (isCollection) {
|
|
68
70
|
if (p.method === "GET") {
|
|
69
71
|
operations.push("list");
|
|
70
|
-
// Try to extract item schema from list response (array items)
|
|
71
72
|
const listSchema = getSuccessResponseSchema(p);
|
|
72
|
-
if (listSchema
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
if (listSchema) {
|
|
74
|
+
// Extract item schema from both flat arrays and wrapped lists
|
|
75
|
+
const arrayInfo = findArrayProperty(listSchema);
|
|
76
|
+
if (arrayInfo.itemSchema) {
|
|
77
|
+
schema = schema ?? arrayInfo.itemSchema;
|
|
78
|
+
}
|
|
79
|
+
else if (listSchema.type === "array" && listSchema.items) {
|
|
80
|
+
// Fallback: direct flat array
|
|
81
|
+
const items = Array.isArray(listSchema.items)
|
|
82
|
+
? listSchema.items[0]
|
|
83
|
+
: listSchema.items;
|
|
84
|
+
if (isRecord(items)) {
|
|
85
|
+
schema = schema ?? toJsonSchema(items);
|
|
86
|
+
}
|
|
78
87
|
}
|
|
79
88
|
}
|
|
89
|
+
// Capture list operation metadata
|
|
90
|
+
const meta = buildOperationMeta(p, "list");
|
|
91
|
+
operationMeta.set("list", meta);
|
|
80
92
|
}
|
|
81
93
|
else if (p.method === "POST") {
|
|
82
94
|
operations.push("create");
|
|
95
|
+
const meta = buildOperationMeta(p, "create");
|
|
96
|
+
operationMeta.set("create", meta);
|
|
83
97
|
}
|
|
84
98
|
}
|
|
85
99
|
else if (isItem) {
|
|
@@ -97,14 +111,20 @@ function buildResource(basePath, paths) {
|
|
|
97
111
|
if (p.method === "GET") {
|
|
98
112
|
operations.push("read");
|
|
99
113
|
schema = schema ?? getSuccessResponseSchema(p);
|
|
114
|
+
const meta = buildOperationMeta(p, "read");
|
|
115
|
+
operationMeta.set("read", meta);
|
|
100
116
|
}
|
|
101
117
|
else if (p.method === "PUT" || p.method === "PATCH") {
|
|
102
118
|
if (!operations.includes("update")) {
|
|
103
119
|
operations.push("update");
|
|
120
|
+
const meta = buildOperationMeta(p, "update");
|
|
121
|
+
operationMeta.set("update", meta);
|
|
104
122
|
}
|
|
105
123
|
}
|
|
106
124
|
else if (p.method === "DELETE") {
|
|
107
125
|
operations.push("delete");
|
|
126
|
+
const meta = buildOperationMeta(p, "delete");
|
|
127
|
+
operationMeta.set("delete", meta);
|
|
108
128
|
}
|
|
109
129
|
}
|
|
110
130
|
}
|
|
@@ -137,8 +157,35 @@ function buildResource(basePath, paths) {
|
|
|
137
157
|
idParam,
|
|
138
158
|
operations,
|
|
139
159
|
schema,
|
|
160
|
+
operationMeta: operationMeta.size > 0 ? operationMeta : undefined,
|
|
140
161
|
};
|
|
141
162
|
}
|
|
163
|
+
function buildOperationMeta(p, _operation) {
|
|
164
|
+
const meta = {};
|
|
165
|
+
// Capture full success response schema
|
|
166
|
+
const responseSchema = getSuccessResponseSchema(p);
|
|
167
|
+
if (responseSchema) {
|
|
168
|
+
meta.responseSchema = responseSchema;
|
|
169
|
+
}
|
|
170
|
+
// Capture success response headers
|
|
171
|
+
for (const [code, resp] of p.responses) {
|
|
172
|
+
if (code >= 200 && code < 300 && resp.headers) {
|
|
173
|
+
meta.responseHeaders = resp.headers;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Capture error response schemas (4xx)
|
|
178
|
+
const errorSchemas = new Map();
|
|
179
|
+
for (const [code, resp] of p.responses) {
|
|
180
|
+
if (code >= 400 && code < 600 && resp.schema) {
|
|
181
|
+
errorSchemas.set(code, resp.schema);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (errorSchemas.size > 0) {
|
|
185
|
+
meta.errorSchemas = errorSchemas;
|
|
186
|
+
}
|
|
187
|
+
return meta;
|
|
188
|
+
}
|
|
142
189
|
function getSuccessResponseSchema(p) {
|
|
143
190
|
// Try 200, then 201, then first 2xx
|
|
144
191
|
for (const code of [200, 201]) {
|
package/dist/generators.d.ts
CHANGED
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
import type { JSONSchema7 } from "json-schema";
|
|
2
2
|
import type { CrudResource } from "./crud-detector.js";
|
|
3
3
|
import type { ParsedPath } from "./parser.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Result of finding the array property in a response schema.
|
|
6
|
+
* If property is undefined, the schema is a flat array (or unknown).
|
|
7
|
+
*/
|
|
8
|
+
interface ArrayPropertyInfo {
|
|
9
|
+
/** Property name holding the array (e.g. "data"), undefined for flat arrays */
|
|
10
|
+
property?: string;
|
|
11
|
+
/** Schema for the array items */
|
|
12
|
+
itemSchema?: JSONSchema7;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Find which property in a response schema holds the array of items.
|
|
16
|
+
* Handles flat arrays, object wrappers (Stripe), and allOf compositions (Scalar Galaxy).
|
|
17
|
+
*/
|
|
18
|
+
export declare function findArrayProperty(schema: JSONSchema7): ArrayPropertyInfo;
|
|
19
|
+
/**
|
|
20
|
+
* Generate header values from spec-defined response header definitions.
|
|
21
|
+
*/
|
|
22
|
+
export declare function generateHeaderValues(headerDefs: Record<string, Schmock.ResponseHeaderDef> | undefined): Record<string, string>;
|
|
23
|
+
export declare function createListGenerator(resource: CrudResource, meta?: Schmock.CrudOperationMeta): Schmock.GeneratorFunction;
|
|
24
|
+
export declare function createCreateGenerator(resource: CrudResource, meta?: Schmock.CrudOperationMeta): Schmock.GeneratorFunction;
|
|
25
|
+
export declare function createReadGenerator(resource: CrudResource, meta?: Schmock.CrudOperationMeta): Schmock.GeneratorFunction;
|
|
26
|
+
export declare function createUpdateGenerator(resource: CrudResource, meta?: Schmock.CrudOperationMeta): Schmock.GeneratorFunction;
|
|
27
|
+
export declare function createDeleteGenerator(resource: CrudResource, meta?: Schmock.CrudOperationMeta): Schmock.GeneratorFunction;
|
|
28
|
+
export declare function createStaticGenerator(parsedPath: ParsedPath, seed?: number): Schmock.GeneratorFunction;
|
|
10
29
|
/**
|
|
11
30
|
* Generate seed items for a resource using its schema.
|
|
12
31
|
*/
|
|
13
|
-
export declare function generateSeedItems(schema: JSONSchema7, count: number, idParam: string): unknown[];
|
|
32
|
+
export declare function generateSeedItems(schema: JSONSchema7, count: number, idParam: string, seed?: number): unknown[];
|
|
33
|
+
export {};
|
|
14
34
|
//# sourceMappingURL=generators.d.ts.map
|
package/dist/generators.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generators.d.ts","sourceRoot":"","sources":["../src/generators.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"generators.d.ts","sourceRoot":"","sources":["../src/generators.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAK9C;;;GAGG;AACH,UAAU,iBAAiB;IACzB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iCAAiC;IACjC,UAAU,CAAC,EAAE,WAAW,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,WAAW,GAAG,iBAAiB,CA4CxE;AAiBD;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,SAAS,GAChE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAaxB;AAuED,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,YAAY,EACtB,IAAI,CAAC,EAAE,OAAO,CAAC,iBAAiB,GAC/B,OAAO,CAAC,iBAAiB,CAyB3B;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,YAAY,EACtB,IAAI,CAAC,EAAE,OAAO,CAAC,iBAAiB,GAC/B,OAAO,CAAC,iBAAiB,CAoB3B;AAED,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,YAAY,EACtB,IAAI,CAAC,EAAE,OAAO,CAAC,iBAAiB,GAC/B,OAAO,CAAC,iBAAiB,CAc3B;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,YAAY,EACtB,IAAI,CAAC,EAAE,OAAO,CAAC,iBAAiB,GAC/B,OAAO,CAAC,iBAAiB,CAsB3B;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,YAAY,EACtB,IAAI,CAAC,EAAE,OAAO,CAAC,iBAAiB,GAC/B,OAAO,CAAC,iBAAiB,CAa3B;AAED,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,UAAU,EACtB,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,iBAAiB,CAiC3B;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,EAAE,CAWX"}
|
package/dist/generators.js
CHANGED
|
@@ -1,7 +1,108 @@
|
|
|
1
|
-
/// <reference path="
|
|
2
|
-
import {
|
|
1
|
+
/// <reference path="../../core/schmock.d.ts" />
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { generateFromSchema } from "@schmock/faker";
|
|
3
4
|
import { isRecord } from "./utils.js";
|
|
4
5
|
const COLLECTION_STATE_PREFIX = "openapi:collections:";
|
|
6
|
+
/**
|
|
7
|
+
* Find which property in a response schema holds the array of items.
|
|
8
|
+
* Handles flat arrays, object wrappers (Stripe), and allOf compositions (Scalar Galaxy).
|
|
9
|
+
*/
|
|
10
|
+
export function findArrayProperty(schema) {
|
|
11
|
+
if (!schema || typeof schema === "boolean")
|
|
12
|
+
return {};
|
|
13
|
+
// Case 1: flat array
|
|
14
|
+
if (schema.type === "array") {
|
|
15
|
+
const items = Array.isArray(schema.items) ? schema.items[0] : schema.items;
|
|
16
|
+
const itemSchema = isRecord(items) ? items : undefined;
|
|
17
|
+
return { itemSchema };
|
|
18
|
+
}
|
|
19
|
+
// Case 2: object with properties — scan for the array property
|
|
20
|
+
if (schema.type === "object" && isRecord(schema.properties)) {
|
|
21
|
+
return findArrayInProperties(schema.properties);
|
|
22
|
+
}
|
|
23
|
+
// Case 3: allOf — merge branches into one virtual object, then scan
|
|
24
|
+
if (Array.isArray(schema.allOf)) {
|
|
25
|
+
const merged = {};
|
|
26
|
+
for (const branch of schema.allOf) {
|
|
27
|
+
if (isRecord(branch) && isRecord(branch.properties)) {
|
|
28
|
+
for (const [key, value] of Object.entries(branch.properties)) {
|
|
29
|
+
if (isRecord(value)) {
|
|
30
|
+
merged[key] = value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (Object.keys(merged).length > 0) {
|
|
36
|
+
return findArrayInProperties(merged);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Case 4: anyOf/oneOf — try first branch
|
|
40
|
+
for (const keyword of ["anyOf", "oneOf"]) {
|
|
41
|
+
const branches = schema[keyword];
|
|
42
|
+
if (Array.isArray(branches) && branches.length > 0) {
|
|
43
|
+
const first = branches[0];
|
|
44
|
+
if (isRecord(first)) {
|
|
45
|
+
return findArrayProperty(first);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
function findArrayInProperties(properties) {
|
|
52
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
53
|
+
if (!isRecord(value))
|
|
54
|
+
continue;
|
|
55
|
+
const prop = value;
|
|
56
|
+
if (prop.type === "array" && prop.items) {
|
|
57
|
+
const items = Array.isArray(prop.items) ? prop.items[0] : prop.items;
|
|
58
|
+
const itemSchema = isRecord(items) ? items : undefined;
|
|
59
|
+
return { property: key, itemSchema };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Generate header values from spec-defined response header definitions.
|
|
66
|
+
*/
|
|
67
|
+
export function generateHeaderValues(headerDefs) {
|
|
68
|
+
if (!headerDefs)
|
|
69
|
+
return {};
|
|
70
|
+
const headers = {};
|
|
71
|
+
for (const [name, def] of Object.entries(headerDefs)) {
|
|
72
|
+
const value = generateSingleHeaderValue(def.schema);
|
|
73
|
+
if (value !== undefined) {
|
|
74
|
+
headers[name] = value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return headers;
|
|
78
|
+
}
|
|
79
|
+
function generateSingleHeaderValue(schema) {
|
|
80
|
+
if (!schema || typeof schema === "boolean")
|
|
81
|
+
return undefined;
|
|
82
|
+
// Has enum → first value
|
|
83
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
84
|
+
return String(schema.enum[0]);
|
|
85
|
+
}
|
|
86
|
+
// Has default (from example → default normalization)
|
|
87
|
+
if ("default" in schema && schema.default !== undefined) {
|
|
88
|
+
return String(schema.default);
|
|
89
|
+
}
|
|
90
|
+
// Format-based generation
|
|
91
|
+
if (schema.format === "uuid") {
|
|
92
|
+
return randomUUID();
|
|
93
|
+
}
|
|
94
|
+
if (schema.format === "date-time") {
|
|
95
|
+
return new Date().toISOString();
|
|
96
|
+
}
|
|
97
|
+
// Type-based fallback
|
|
98
|
+
if (schema.type === "integer" || schema.type === "number") {
|
|
99
|
+
return "0";
|
|
100
|
+
}
|
|
101
|
+
if (schema.type === "string") {
|
|
102
|
+
return "";
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
5
106
|
function toTuple(status, body) {
|
|
6
107
|
return [status, body];
|
|
7
108
|
}
|
|
@@ -27,13 +128,30 @@ function getNextId(state, resourceName) {
|
|
|
27
128
|
state[counterKey] = next;
|
|
28
129
|
return next;
|
|
29
130
|
}
|
|
30
|
-
export function createListGenerator(resource) {
|
|
131
|
+
export function createListGenerator(resource, meta) {
|
|
132
|
+
// Pre-compute wrapper info at setup time
|
|
133
|
+
const wrapperInfo = meta?.responseSchema
|
|
134
|
+
? findArrayProperty(meta.responseSchema)
|
|
135
|
+
: undefined;
|
|
136
|
+
const headerDefs = meta?.responseHeaders;
|
|
31
137
|
return (ctx) => {
|
|
32
138
|
const collection = getCollection(ctx.state, resource.name);
|
|
33
|
-
|
|
139
|
+
const items = [...collection];
|
|
140
|
+
// If no wrapper detected or flat array, return items directly
|
|
141
|
+
if (!wrapperInfo?.property || !meta?.responseSchema) {
|
|
142
|
+
return addHeaders(items, headerDefs);
|
|
143
|
+
}
|
|
144
|
+
// Generate the full wrapper skeleton from schema, then inject live data
|
|
145
|
+
const skeleton = generateWrapperSkeleton(meta.responseSchema);
|
|
146
|
+
if (isRecord(skeleton)) {
|
|
147
|
+
skeleton[wrapperInfo.property] = items;
|
|
148
|
+
return addHeaders(skeleton, headerDefs);
|
|
149
|
+
}
|
|
150
|
+
return addHeaders(items, headerDefs);
|
|
34
151
|
};
|
|
35
152
|
}
|
|
36
|
-
export function createCreateGenerator(resource) {
|
|
153
|
+
export function createCreateGenerator(resource, meta) {
|
|
154
|
+
const headerDefs = meta?.responseHeaders;
|
|
37
155
|
return (ctx) => {
|
|
38
156
|
const collection = getCollection(ctx.state, resource.name);
|
|
39
157
|
const id = getNextId(ctx.state, resource.name);
|
|
@@ -48,27 +166,29 @@ export function createCreateGenerator(resource) {
|
|
|
48
166
|
item = { [resource.idParam]: id };
|
|
49
167
|
}
|
|
50
168
|
collection.push(item);
|
|
51
|
-
return toTuple(201, item);
|
|
169
|
+
return addHeaders(toTuple(201, item), headerDefs);
|
|
52
170
|
};
|
|
53
171
|
}
|
|
54
|
-
export function createReadGenerator(resource) {
|
|
172
|
+
export function createReadGenerator(resource, meta) {
|
|
173
|
+
const headerDefs = meta?.responseHeaders;
|
|
55
174
|
return (ctx) => {
|
|
56
175
|
const collection = getCollection(ctx.state, resource.name);
|
|
57
176
|
const idValue = ctx.params[resource.idParam];
|
|
58
177
|
const item = findById(collection, resource.idParam, idValue);
|
|
59
178
|
if (!item) {
|
|
60
|
-
return
|
|
179
|
+
return generateErrorResponse(404, meta);
|
|
61
180
|
}
|
|
62
|
-
return item;
|
|
181
|
+
return addHeaders(item, headerDefs);
|
|
63
182
|
};
|
|
64
183
|
}
|
|
65
|
-
export function createUpdateGenerator(resource) {
|
|
184
|
+
export function createUpdateGenerator(resource, meta) {
|
|
185
|
+
const headerDefs = meta?.responseHeaders;
|
|
66
186
|
return (ctx) => {
|
|
67
187
|
const collection = getCollection(ctx.state, resource.name);
|
|
68
188
|
const idValue = ctx.params[resource.idParam];
|
|
69
189
|
const index = findIndexById(collection, resource.idParam, idValue);
|
|
70
190
|
if (index === -1) {
|
|
71
|
-
return
|
|
191
|
+
return generateErrorResponse(404, meta);
|
|
72
192
|
}
|
|
73
193
|
const existingRaw = collection[index];
|
|
74
194
|
const existing = isRecord(existingRaw) ? existingRaw : {};
|
|
@@ -78,22 +198,22 @@ export function createUpdateGenerator(resource) {
|
|
|
78
198
|
[resource.idParam]: existing[resource.idParam], // Preserve ID
|
|
79
199
|
};
|
|
80
200
|
collection[index] = updated;
|
|
81
|
-
return updated;
|
|
201
|
+
return addHeaders(updated, headerDefs);
|
|
82
202
|
};
|
|
83
203
|
}
|
|
84
|
-
export function createDeleteGenerator(resource) {
|
|
204
|
+
export function createDeleteGenerator(resource, meta) {
|
|
85
205
|
return (ctx) => {
|
|
86
206
|
const collection = getCollection(ctx.state, resource.name);
|
|
87
207
|
const idValue = ctx.params[resource.idParam];
|
|
88
208
|
const index = findIndexById(collection, resource.idParam, idValue);
|
|
89
209
|
if (index === -1) {
|
|
90
|
-
return
|
|
210
|
+
return generateErrorResponse(404, meta);
|
|
91
211
|
}
|
|
92
212
|
collection.splice(index, 1);
|
|
93
213
|
return toTuple(204, undefined);
|
|
94
214
|
};
|
|
95
215
|
}
|
|
96
|
-
export function createStaticGenerator(parsedPath) {
|
|
216
|
+
export function createStaticGenerator(parsedPath, seed) {
|
|
97
217
|
// Get the success response schema
|
|
98
218
|
let responseSchema;
|
|
99
219
|
for (const code of [200, 201]) {
|
|
@@ -114,7 +234,7 @@ export function createStaticGenerator(parsedPath) {
|
|
|
114
234
|
return () => {
|
|
115
235
|
if (responseSchema) {
|
|
116
236
|
try {
|
|
117
|
-
return generateFromSchema({ schema: responseSchema });
|
|
237
|
+
return generateFromSchema({ schema: responseSchema, seed });
|
|
118
238
|
}
|
|
119
239
|
catch (error) {
|
|
120
240
|
console.warn(`[@schmock/openapi] Schema generation failed for ${parsedPath.method} ${parsedPath.path}:`, error instanceof Error ? error.message : error);
|
|
@@ -127,10 +247,10 @@ export function createStaticGenerator(parsedPath) {
|
|
|
127
247
|
/**
|
|
128
248
|
* Generate seed items for a resource using its schema.
|
|
129
249
|
*/
|
|
130
|
-
export function generateSeedItems(schema, count, idParam) {
|
|
250
|
+
export function generateSeedItems(schema, count, idParam, seed) {
|
|
131
251
|
const items = [];
|
|
132
252
|
for (let i = 0; i < count; i++) {
|
|
133
|
-
const generated = generateFromSchema({ schema });
|
|
253
|
+
const generated = generateFromSchema({ schema, seed });
|
|
134
254
|
const item = isRecord(generated)
|
|
135
255
|
? generated
|
|
136
256
|
: { value: generated };
|
|
@@ -139,6 +259,58 @@ export function generateSeedItems(schema, count, idParam) {
|
|
|
139
259
|
}
|
|
140
260
|
return items;
|
|
141
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Generate an error response using the spec's error schema if available,
|
|
264
|
+
* or fall back to the default { error, code } format.
|
|
265
|
+
*/
|
|
266
|
+
function generateErrorResponse(status, meta) {
|
|
267
|
+
const errorSchema = meta?.errorSchemas?.get(status);
|
|
268
|
+
if (errorSchema) {
|
|
269
|
+
try {
|
|
270
|
+
const body = generateFromSchema({ schema: errorSchema });
|
|
271
|
+
return toTuple(status, body);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
// Fall through to default
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Default error format
|
|
278
|
+
const defaults = {
|
|
279
|
+
404: { error: "Not found", code: "NOT_FOUND" },
|
|
280
|
+
400: { error: "Bad request", code: "BAD_REQUEST" },
|
|
281
|
+
409: { error: "Conflict", code: "CONFLICT" },
|
|
282
|
+
};
|
|
283
|
+
return toTuple(status, defaults[status] ?? { error: "Error", code: "ERROR" });
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Generate a skeleton object from a response schema.
|
|
287
|
+
* Used to create wrapper objects (e.g. { data: [], has_more: false, object: "list" })
|
|
288
|
+
*/
|
|
289
|
+
function generateWrapperSkeleton(schema) {
|
|
290
|
+
try {
|
|
291
|
+
return generateFromSchema({ schema });
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
return {};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* If response headers are defined, convert a response value into a triple [status, body, headers].
|
|
299
|
+
* Otherwise return the value as-is.
|
|
300
|
+
*/
|
|
301
|
+
function addHeaders(value, headerDefs) {
|
|
302
|
+
const headers = generateHeaderValues(headerDefs);
|
|
303
|
+
if (Object.keys(headers).length === 0) {
|
|
304
|
+
return value;
|
|
305
|
+
}
|
|
306
|
+
// If already a tuple [status, body] or [status, body, headers]
|
|
307
|
+
if (Array.isArray(value) && value.length >= 2) {
|
|
308
|
+
const status = typeof value[0] === "number" ? value[0] : 200;
|
|
309
|
+
return [status, value[1], headers];
|
|
310
|
+
}
|
|
311
|
+
// Plain value → [200, body, headers]
|
|
312
|
+
return [200, value, headers];
|
|
313
|
+
}
|
|
142
314
|
function findById(collection, idParam, idValue) {
|
|
143
315
|
return collection.find((item) => {
|
|
144
316
|
if (!isRecord(item))
|