@schmock/openapi 1.1.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 (46) hide show
  1. package/dist/crud-detector.d.ts +35 -0
  2. package/dist/crud-detector.d.ts.map +1 -0
  3. package/dist/crud-detector.js +153 -0
  4. package/dist/generators.d.ts +14 -0
  5. package/dist/generators.d.ts.map +1 -0
  6. package/dist/generators.js +158 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +221 -0
  10. package/dist/normalizer.d.ts +14 -0
  11. package/dist/normalizer.d.ts.map +1 -0
  12. package/dist/normalizer.js +194 -0
  13. package/dist/parser.d.ts +32 -0
  14. package/dist/parser.d.ts.map +1 -0
  15. package/dist/parser.js +282 -0
  16. package/dist/plugin.d.ts +32 -0
  17. package/dist/plugin.d.ts.map +1 -0
  18. package/dist/plugin.js +129 -0
  19. package/dist/seed.d.ts +15 -0
  20. package/dist/seed.d.ts.map +1 -0
  21. package/dist/seed.js +41 -0
  22. package/package.json +45 -0
  23. package/src/__fixtures__/faker-stress-test.openapi.yaml +1030 -0
  24. package/src/__fixtures__/openapi31.json +34 -0
  25. package/src/__fixtures__/petstore-openapi3.json +168 -0
  26. package/src/__fixtures__/petstore-swagger2.json +141 -0
  27. package/src/__fixtures__/scalar-galaxy.yaml +1314 -0
  28. package/src/__fixtures__/stripe-fixtures3.json +6542 -0
  29. package/src/__fixtures__/stripe-spec3.yaml +161621 -0
  30. package/src/__fixtures__/train-travel.yaml +1264 -0
  31. package/src/crud-detector.test.ts +150 -0
  32. package/src/crud-detector.ts +194 -0
  33. package/src/generators.test.ts +214 -0
  34. package/src/generators.ts +212 -0
  35. package/src/index.ts +4 -0
  36. package/src/normalizer.test.ts +253 -0
  37. package/src/normalizer.ts +233 -0
  38. package/src/parser.test.ts +181 -0
  39. package/src/parser.ts +389 -0
  40. package/src/plugin.test.ts +205 -0
  41. package/src/plugin.ts +185 -0
  42. package/src/seed.ts +62 -0
  43. package/src/steps/openapi-crud.steps.ts +132 -0
  44. package/src/steps/openapi-parsing.steps.ts +111 -0
  45. package/src/steps/openapi-seed.steps.ts +94 -0
  46. package/src/stress.test.ts +2814 -0
@@ -0,0 +1,14 @@
1
+ import type { JSONSchema7 } from "json-schema";
2
+ /**
3
+ * Normalize an OpenAPI schema to pure JSON Schema 7 that json-schema-faker understands.
4
+ *
5
+ * Transforms applied:
6
+ * - nullable: true -> oneOf with null type
7
+ * - discriminator -> required + enum on branches
8
+ * - readOnly/writeOnly -> strip based on direction
9
+ * - example -> default (if default not set)
10
+ * - exclusiveMinimum/exclusiveMaximum boolean -> number format
11
+ * - x-* extensions -> stripped
12
+ */
13
+ export declare function normalizeSchema(schema: Record<string, unknown>, direction: "request" | "response"): JSONSchema7;
14
+ //# sourceMappingURL=normalizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalizer.d.ts","sourceRoot":"","sources":["../src/normalizer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAa/C;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,EAAE,SAAS,GAAG,UAAU,GAChC,WAAW,CAEb"}
@@ -0,0 +1,194 @@
1
+ /// <reference path="../../../types/schmock.d.ts" />
2
+ function isRecord(value) {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4
+ }
5
+ function toJsonSchema(node) {
6
+ // Object.assign merges unknown-keyed properties into JSONSchema7.
7
+ // This is safe because the normalizer has already ensured the shape
8
+ // is valid JSON Schema 7 before calling this function.
9
+ return Object.assign({}, node);
10
+ }
11
+ /**
12
+ * Normalize an OpenAPI schema to pure JSON Schema 7 that json-schema-faker understands.
13
+ *
14
+ * Transforms applied:
15
+ * - nullable: true -> oneOf with null type
16
+ * - discriminator -> required + enum on branches
17
+ * - readOnly/writeOnly -> strip based on direction
18
+ * - example -> default (if default not set)
19
+ * - exclusiveMinimum/exclusiveMaximum boolean -> number format
20
+ * - x-* extensions -> stripped
21
+ */
22
+ export function normalizeSchema(schema, direction) {
23
+ return normalizeNode(structuredClone(schema), direction, new WeakSet());
24
+ }
25
+ function normalizeNode(node, direction, visited) {
26
+ if (!node || typeof node !== "object" || Array.isArray(node)) {
27
+ return toJsonSchema({});
28
+ }
29
+ // Circular reference detection — break cycles
30
+ if (visited.has(node)) {
31
+ return toJsonSchema({});
32
+ }
33
+ visited.add(node);
34
+ // Strip x-* extensions
35
+ for (const key of Object.keys(node)) {
36
+ if (key.startsWith("x-")) {
37
+ delete node[key];
38
+ }
39
+ }
40
+ // Handle nullable: true -> oneOf with null
41
+ if (node.nullable === true) {
42
+ delete node.nullable;
43
+ // Only wrap if not already a composition with null
44
+ const copy = { ...node };
45
+ delete copy.nullable;
46
+ return {
47
+ oneOf: [normalizeNode(copy, direction, visited), { type: "null" }],
48
+ };
49
+ }
50
+ delete node.nullable;
51
+ // Handle discriminator
52
+ if (node.discriminator && isRecord(node.discriminator)) {
53
+ const disc = node.discriminator;
54
+ const propName = disc.propertyName;
55
+ if (typeof propName === "string" && Array.isArray(node.oneOf)) {
56
+ const mappingRaw = disc.mapping ?? {};
57
+ const mapping = isRecord(mappingRaw) ? mappingRaw : {};
58
+ // mapping keys ARE the discriminator values (e.g. "dog", "cat")
59
+ // mapping values are $ref strings — after dereference they can't be matched to branches
60
+ // Use key order to correspond to oneOf branch order
61
+ const discriminatorValues = Object.keys(mapping);
62
+ node.oneOf = node.oneOf
63
+ .filter((branch) => isRecord(branch))
64
+ .map((branch, index) => {
65
+ const normalized = normalizeNode(branch, direction, visited);
66
+ // Ensure discriminator property is required
67
+ if (isRecord(normalized)) {
68
+ const required = Array.isArray(normalized.required)
69
+ ? [...normalized.required]
70
+ : [];
71
+ if (!required.includes(propName)) {
72
+ required.push(propName);
73
+ }
74
+ normalized.required = required;
75
+ // Add enum constraint for the discriminator value
76
+ const mappingValue = discriminatorValues[index];
77
+ if (mappingValue && isRecord(normalized.properties)) {
78
+ const props = normalized.properties;
79
+ const existingRaw = props[propName] ?? {};
80
+ const existing = isRecord(existingRaw) ? existingRaw : {};
81
+ props[propName] = { ...existing, enum: [mappingValue] };
82
+ }
83
+ }
84
+ return normalized;
85
+ });
86
+ }
87
+ delete node.discriminator;
88
+ }
89
+ // Handle readOnly/writeOnly on properties
90
+ if (isRecord(node.properties)) {
91
+ const props = node.properties;
92
+ const required = Array.isArray(node.required)
93
+ ? node.required.filter((r) => typeof r === "string")
94
+ : [];
95
+ const keysToRemove = [];
96
+ for (const [propName, propSchemaRaw] of Object.entries(props)) {
97
+ if (!isRecord(propSchemaRaw))
98
+ continue;
99
+ const propSchema = propSchemaRaw;
100
+ // readOnly fields: remove from request schemas
101
+ if (direction === "request" && propSchema.readOnly === true) {
102
+ keysToRemove.push(propName);
103
+ continue;
104
+ }
105
+ // writeOnly fields: remove from response schemas
106
+ if (direction === "response" && propSchema.writeOnly === true) {
107
+ keysToRemove.push(propName);
108
+ continue;
109
+ }
110
+ // Clean up the flags after handling
111
+ delete propSchema.readOnly;
112
+ delete propSchema.writeOnly;
113
+ // Recurse into property
114
+ props[propName] = normalizeNode(propSchema, direction, visited);
115
+ }
116
+ for (const key of keysToRemove) {
117
+ delete props[key];
118
+ const reqIdx = required.indexOf(key);
119
+ if (reqIdx !== -1) {
120
+ required.splice(reqIdx, 1);
121
+ }
122
+ }
123
+ if (required.length > 0) {
124
+ node.required = required;
125
+ }
126
+ else if (keysToRemove.length > 0 && Array.isArray(node.required)) {
127
+ // If we removed all required fields, clean up
128
+ if (required.length === 0) {
129
+ delete node.required;
130
+ }
131
+ }
132
+ }
133
+ // Handle example -> default
134
+ if ("example" in node && !("default" in node)) {
135
+ node.default = node.example;
136
+ }
137
+ delete node.example;
138
+ // Handle exclusiveMinimum/exclusiveMaximum boolean -> number
139
+ if (node.exclusiveMinimum === true && typeof node.minimum === "number") {
140
+ node.exclusiveMinimum = node.minimum;
141
+ delete node.minimum;
142
+ }
143
+ else if (node.exclusiveMinimum === false) {
144
+ delete node.exclusiveMinimum;
145
+ }
146
+ if (node.exclusiveMaximum === true && typeof node.maximum === "number") {
147
+ node.exclusiveMaximum = node.maximum;
148
+ delete node.maximum;
149
+ }
150
+ else if (node.exclusiveMaximum === false) {
151
+ delete node.exclusiveMaximum;
152
+ }
153
+ // Recurse into items (array schema)
154
+ if (node.items) {
155
+ if (Array.isArray(node.items)) {
156
+ node.items = node.items.map((item) => isRecord(item) ? normalizeNode(item, direction, visited) : item);
157
+ }
158
+ else if (isRecord(node.items)) {
159
+ node.items = normalizeNode(node.items, direction, visited);
160
+ }
161
+ }
162
+ // Recurse into additionalProperties
163
+ if (isRecord(node.additionalProperties)) {
164
+ node.additionalProperties = normalizeNode(node.additionalProperties, direction, visited);
165
+ }
166
+ // Recurse into composition keywords
167
+ for (const keyword of ["allOf", "anyOf", "oneOf"]) {
168
+ const keywordValue = node[keyword];
169
+ if (Array.isArray(keywordValue)) {
170
+ node[keyword] = keywordValue.map((branch) => isRecord(branch) ? normalizeNode(branch, direction, visited) : branch);
171
+ }
172
+ }
173
+ // Recurse into not
174
+ if (isRecord(node.not)) {
175
+ node.not = normalizeNode(node.not, direction, visited);
176
+ }
177
+ // Recurse into conditional
178
+ for (const keyword of ["if", "then", "else"]) {
179
+ const keywordValue = node[keyword];
180
+ if (isRecord(keywordValue)) {
181
+ node[keyword] = normalizeNode(keywordValue, direction, visited);
182
+ }
183
+ }
184
+ // Recurse into patternProperties
185
+ if (isRecord(node.patternProperties)) {
186
+ const pp = node.patternProperties;
187
+ for (const [pattern, schema] of Object.entries(pp)) {
188
+ if (isRecord(schema)) {
189
+ pp[pattern] = normalizeNode(schema, direction, visited);
190
+ }
191
+ }
192
+ }
193
+ return toJsonSchema(node);
194
+ }
@@ -0,0 +1,32 @@
1
+ import type { JSONSchema7 } from "json-schema";
2
+ export interface ParsedSpec {
3
+ title: string;
4
+ version: string;
5
+ basePath: string;
6
+ paths: ParsedPath[];
7
+ }
8
+ export interface ParsedPath {
9
+ /** Express-style path e.g. "/pets/:petId" */
10
+ path: string;
11
+ method: Schmock.HttpMethod;
12
+ operationId?: string;
13
+ parameters: ParsedParameter[];
14
+ requestBody?: JSONSchema7;
15
+ responses: Map<number, {
16
+ schema?: JSONSchema7;
17
+ description: string;
18
+ }>;
19
+ tags: string[];
20
+ }
21
+ export interface ParsedParameter {
22
+ name: string;
23
+ in: "path" | "query" | "header";
24
+ required: boolean;
25
+ schema?: JSONSchema7;
26
+ }
27
+ /**
28
+ * Parse an OpenAPI/Swagger spec into a normalized internal model.
29
+ * Supports Swagger 2.0, OpenAPI 3.0, and 3.1.
30
+ */
31
+ export declare function parseSpec(source: string | object): Promise<ParsedSpec>;
32
+ //# sourceMappingURL=parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI/C,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtE,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAkDD;;;GAGG;AACH,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAuH5E"}
package/dist/parser.js ADDED
@@ -0,0 +1,282 @@
1
+ /// <reference path="../../../types/schmock.d.ts" />
2
+ import SwaggerParser from "@apidevtools/swagger-parser";
3
+ import { toHttpMethod } from "@schmock/core";
4
+ import { normalizeSchema } from "./normalizer.js";
5
+ const HTTP_METHOD_KEYS = new Set([
6
+ "get",
7
+ "post",
8
+ "put",
9
+ "delete",
10
+ "patch",
11
+ "head",
12
+ "options",
13
+ ]);
14
+ function isRecord(value) {
15
+ return typeof value === "object" && value !== null && !Array.isArray(value);
16
+ }
17
+ function isOpenApiDocument(value) {
18
+ return isRecord(value) && ("swagger" in value || "openapi" in value);
19
+ }
20
+ function getString(value) {
21
+ return typeof value === "string" ? value : undefined;
22
+ }
23
+ function getBoolean(value, fallback) {
24
+ return typeof value === "boolean" ? value : fallback;
25
+ }
26
+ /**
27
+ * Strip root-level x-* extensions from a spec object.
28
+ * These may contain $ref to external docs (e.g. markdown files)
29
+ * that swagger-parser cannot resolve.
30
+ */
31
+ function stripRootExtensions(spec) {
32
+ for (const key of Object.keys(spec)) {
33
+ if (key.startsWith("x-")) {
34
+ Reflect.deleteProperty(spec, key);
35
+ }
36
+ }
37
+ }
38
+ /**
39
+ * Ensure a paths key exists on a spec object (required by swagger-parser validation).
40
+ */
41
+ function ensurePathsKey(spec) {
42
+ if (!("paths" in spec)) {
43
+ Object.assign(spec, { paths: {} });
44
+ }
45
+ }
46
+ /**
47
+ * Parse an OpenAPI/Swagger spec into a normalized internal model.
48
+ * Supports Swagger 2.0, OpenAPI 3.0, and 3.1.
49
+ */
50
+ export async function parseSpec(source) {
51
+ let api;
52
+ if (typeof source === "string") {
53
+ // Parse raw YAML/JSON first (no ref resolution)
54
+ const raw = await SwaggerParser.parse(source);
55
+ stripRootExtensions(raw);
56
+ ensurePathsKey(raw);
57
+ api = await SwaggerParser.dereference(raw);
58
+ }
59
+ else if (isOpenApiDocument(source)) {
60
+ const copy = structuredClone(source);
61
+ stripRootExtensions(copy);
62
+ ensurePathsKey(copy);
63
+ api = await SwaggerParser.dereference(copy);
64
+ }
65
+ else {
66
+ throw new Error("Invalid OpenAPI spec: must be a string path or an OpenAPI document object");
67
+ }
68
+ const isSwagger2 = "swagger" in api && typeof api.swagger === "string";
69
+ const title = api.info?.title ?? "Untitled";
70
+ const version = api.info?.version ?? "0.0.0";
71
+ let basePath = "";
72
+ if (isSwagger2 && "basePath" in api) {
73
+ const bp = api.basePath;
74
+ basePath = typeof bp === "string" ? bp : "";
75
+ }
76
+ else if ("servers" in api &&
77
+ Array.isArray(api.servers) &&
78
+ api.servers.length > 0) {
79
+ const firstServer = api.servers[0];
80
+ if (isRecord(firstServer) && typeof firstServer.url === "string") {
81
+ try {
82
+ const url = new URL(firstServer.url, "http://localhost");
83
+ basePath = url.pathname === "/" ? "" : url.pathname;
84
+ }
85
+ catch {
86
+ basePath = "";
87
+ }
88
+ }
89
+ }
90
+ // Strip trailing slash from basePath
91
+ if (basePath.endsWith("/") && basePath !== "/") {
92
+ basePath = basePath.slice(0, -1);
93
+ }
94
+ const paths = [];
95
+ const rawPaths = "paths" in api && isRecord(api.paths) ? api.paths : undefined;
96
+ if (!rawPaths) {
97
+ return { title, version, basePath, paths };
98
+ }
99
+ for (const [pathTemplate, pathItemRaw] of Object.entries(rawPaths)) {
100
+ if (!isRecord(pathItemRaw))
101
+ continue;
102
+ const pathItem = pathItemRaw;
103
+ // Extract path-level parameters
104
+ const pathLevelParams = extractParameters(Array.isArray(pathItem.parameters) ? pathItem.parameters : undefined, isSwagger2);
105
+ for (const methodKey of Object.keys(pathItem)) {
106
+ if (!HTTP_METHOD_KEYS.has(methodKey))
107
+ continue;
108
+ const operation = pathItem[methodKey];
109
+ if (!isRecord(operation))
110
+ continue;
111
+ const method = toHttpMethod(methodKey.toUpperCase());
112
+ // Merge path-level + operation-level parameters (operation wins)
113
+ const operationParams = extractParameters(Array.isArray(operation.parameters) ? operation.parameters : undefined, isSwagger2);
114
+ const mergedParams = mergeParameters(pathLevelParams, operationParams);
115
+ // Extract request body
116
+ let requestBody;
117
+ if (isSwagger2) {
118
+ requestBody = extractSwagger2RequestBody(mergedParams);
119
+ }
120
+ else {
121
+ requestBody = extractOpenApi3RequestBody(isRecord(operation.requestBody) ? operation.requestBody : undefined);
122
+ }
123
+ // Extract responses
124
+ const responses = extractResponses(isRecord(operation.responses) ? operation.responses : undefined, isSwagger2);
125
+ // Convert path template: {petId} -> :petId
126
+ const expressPath = convertPathTemplate(pathTemplate);
127
+ const tags = Array.isArray(operation.tags)
128
+ ? operation.tags.filter((t) => typeof t === "string")
129
+ : [];
130
+ // Filter out body parameters from the final parameter list (Swagger 2.0)
131
+ const filteredParams = mergedParams.filter(isNotBodyParam);
132
+ paths.push({
133
+ path: expressPath,
134
+ method,
135
+ operationId: getString(operation.operationId),
136
+ parameters: filteredParams,
137
+ requestBody,
138
+ responses,
139
+ tags,
140
+ });
141
+ }
142
+ }
143
+ return { title, version, basePath, paths };
144
+ }
145
+ function isValidParamLocation(location, isSwagger2) {
146
+ const validLocations = isSwagger2
147
+ ? ["path", "query", "header", "body"]
148
+ : ["path", "query", "header"];
149
+ return validLocations.includes(location);
150
+ }
151
+ function isNotBodyParam(param) {
152
+ return param.in !== "body";
153
+ }
154
+ function extractParameters(params, isSwagger2) {
155
+ if (!params || !Array.isArray(params))
156
+ return [];
157
+ return params
158
+ .filter((p) => isRecord(p))
159
+ .map((p) => {
160
+ const location = getString(p.in);
161
+ if (!location || !isValidParamLocation(location, isSwagger2)) {
162
+ return null;
163
+ }
164
+ let schema;
165
+ if (isSwagger2) {
166
+ // Swagger 2.0: schema is inline on the parameter (type, format, etc.)
167
+ if (location === "body") {
168
+ schema = isRecord(p.schema)
169
+ ? normalizeSchema(p.schema, "request")
170
+ : undefined;
171
+ }
172
+ else {
173
+ schema = p.type
174
+ ? normalizeSchema({ type: p.type, format: p.format, enum: p.enum }, "request")
175
+ : undefined;
176
+ }
177
+ }
178
+ else {
179
+ // OpenAPI 3.x: schema is nested
180
+ schema = isRecord(p.schema)
181
+ ? normalizeSchema(p.schema, "request")
182
+ : undefined;
183
+ }
184
+ const name = getString(p.name);
185
+ if (!name)
186
+ return null;
187
+ return {
188
+ name,
189
+ in: location,
190
+ required: getBoolean(p.required, false),
191
+ schema,
192
+ };
193
+ })
194
+ .filter((p) => p !== null);
195
+ }
196
+ function mergeParameters(pathLevel, operationLevel) {
197
+ const merged = new Map();
198
+ // Path-level first
199
+ for (const p of pathLevel) {
200
+ merged.set(`${p.in}:${p.name}`, p);
201
+ }
202
+ // Operation-level overwrites
203
+ for (const p of operationLevel) {
204
+ merged.set(`${p.in}:${p.name}`, p);
205
+ }
206
+ return [...merged.values()];
207
+ }
208
+ function extractSwagger2RequestBody(params) {
209
+ const bodyParam = params.find((p) => p.in === "body");
210
+ return bodyParam?.schema;
211
+ }
212
+ function extractOpenApi3RequestBody(requestBody) {
213
+ if (!requestBody)
214
+ return undefined;
215
+ const content = isRecord(requestBody.content)
216
+ ? requestBody.content
217
+ : undefined;
218
+ if (!content)
219
+ return undefined;
220
+ const jsonEntry = findJsonContent(content);
221
+ if (!jsonEntry)
222
+ return undefined;
223
+ const schema = isRecord(jsonEntry.schema) ? jsonEntry.schema : undefined;
224
+ if (!schema)
225
+ return undefined;
226
+ return normalizeSchema(schema, "request");
227
+ }
228
+ function extractResponses(responses, isSwagger2) {
229
+ const result = new Map();
230
+ if (!responses)
231
+ return result;
232
+ for (const [statusCode, response] of Object.entries(responses)) {
233
+ if (statusCode === "default")
234
+ continue;
235
+ if (!isRecord(response))
236
+ continue;
237
+ const code = Number.parseInt(statusCode, 10);
238
+ if (Number.isNaN(code))
239
+ continue;
240
+ const description = getString(response.description) ?? "";
241
+ let schema;
242
+ if (isSwagger2) {
243
+ // Swagger 2.0: schema is directly on the response
244
+ if (isRecord(response.schema)) {
245
+ schema = normalizeSchema(response.schema, "response");
246
+ }
247
+ }
248
+ else {
249
+ // OpenAPI 3.x: schema is nested in content
250
+ const content = isRecord(response.content) ? response.content : undefined;
251
+ if (content) {
252
+ const jsonEntry = findJsonContent(content);
253
+ if (jsonEntry && isRecord(jsonEntry.schema)) {
254
+ schema = normalizeSchema(jsonEntry.schema, "response");
255
+ }
256
+ }
257
+ }
258
+ result.set(code, { schema, description });
259
+ }
260
+ return result;
261
+ }
262
+ /**
263
+ * Find the best JSON-like content type entry from an OpenAPI content map.
264
+ * Prefers application/json, then any *+json or *json* type.
265
+ */
266
+ function findJsonContent(content) {
267
+ // Prefer exact application/json
268
+ if (isRecord(content["application/json"])) {
269
+ return content["application/json"];
270
+ }
271
+ // Try any JSON-like content type (application/problem+json, etc.)
272
+ for (const [type, value] of Object.entries(content)) {
273
+ if (type.includes("json") && isRecord(value)) {
274
+ return value;
275
+ }
276
+ }
277
+ // Fallback to first content type
278
+ return Object.values(content).find((v) => isRecord(v));
279
+ }
280
+ function convertPathTemplate(path) {
281
+ return path.replace(/\{([^}]+)\}/g, ":$1");
282
+ }
@@ -0,0 +1,32 @@
1
+ import type { SeedConfig, SeedSource } from "./seed.js";
2
+ export type { SeedConfig, SeedSource };
3
+ export interface OpenApiOptions {
4
+ /** File path or inline spec object */
5
+ spec: string | object;
6
+ /** Optional seed data per resource */
7
+ seed?: SeedConfig;
8
+ /** Validate request bodies (default: true) */
9
+ validateRequests?: boolean;
10
+ /** Validate response bodies (default: false) */
11
+ validateResponses?: boolean;
12
+ /** Query features for list endpoints */
13
+ queryFeatures?: {
14
+ pagination?: boolean;
15
+ sorting?: boolean;
16
+ filtering?: boolean;
17
+ };
18
+ }
19
+ /**
20
+ * Create an OpenAPI plugin that auto-registers CRUD routes from a spec.
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const mock = schmock();
25
+ * mock.pipe(await openapi({
26
+ * spec: "./petstore.yaml",
27
+ * seed: { pets: { count: 10 } },
28
+ * }));
29
+ * ```
30
+ */
31
+ export declare function openapi(options: OpenApiOptions): Promise<Schmock.Plugin>;
32
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAGxD,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC;AAMvC,MAAM,WAAW,cAAc;IAC7B,sCAAsC;IACtC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,sCAAsC;IACtC,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,8CAA8C;IAC9C,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,gDAAgD;IAChD,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,wCAAwC;IACxC,aAAa,CAAC,EAAE;QACd,UAAU,CAAC,EAAE,OAAO,CAAC;QACrB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,SAAS,CAAC,EAAE,OAAO,CAAC;KACrB,CAAC;CACH;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,OAAO,CAC3B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAwCzB"}