@schmock/openapi 1.2.1 → 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.
@@ -1,4 +1,4 @@
1
- /// <reference path="../../../types/schmock.d.ts" />
1
+ /// <reference path="../../core/schmock.d.ts" />
2
2
  import { isRecord, toJsonSchema } from "./utils.js";
3
3
  /**
4
4
  * Normalize an OpenAPI schema to pure JSON Schema 7 that json-schema-faker understands.
package/dist/parser.d.ts CHANGED
@@ -1,9 +1,35 @@
1
1
  import type { JSONSchema7 } from "json-schema";
2
+ export interface SecurityScheme {
3
+ type: "apiKey" | "http" | "oauth2" | "openIdConnect";
4
+ /** For apiKey: header, query, or cookie */
5
+ in?: "header" | "query" | "cookie";
6
+ /** For apiKey: the header/query/cookie name */
7
+ name?: string;
8
+ /** For http: bearer, basic, etc. */
9
+ scheme?: string;
10
+ }
2
11
  export interface ParsedSpec {
3
12
  title: string;
4
13
  version: string;
5
14
  basePath: string;
6
15
  paths: ParsedPath[];
16
+ securitySchemes?: Map<string, SecurityScheme>;
17
+ globalSecurity?: string[][];
18
+ }
19
+ export interface ParsedResponseEntry {
20
+ schema?: JSONSchema7;
21
+ description: string;
22
+ headers?: Record<string, Schmock.ResponseHeaderDef>;
23
+ examples?: Map<string, unknown>;
24
+ contentTypes?: string[];
25
+ }
26
+ export interface ParsedCallback {
27
+ /** Runtime expression for the callback URL (e.g. "{$request.body#/callbackUrl}") */
28
+ urlExpression: string;
29
+ /** HTTP method for the callback request */
30
+ method: Schmock.HttpMethod;
31
+ /** JSON Schema for the callback request body */
32
+ requestBody?: JSONSchema7;
7
33
  }
8
34
  export interface ParsedPath {
9
35
  /** Express-style path e.g. "/pets/:petId" */
@@ -12,11 +38,12 @@ export interface ParsedPath {
12
38
  operationId?: string;
13
39
  parameters: ParsedParameter[];
14
40
  requestBody?: JSONSchema7;
15
- responses: Map<number, {
16
- schema?: JSONSchema7;
17
- description: string;
18
- }>;
41
+ responses: Map<number, ParsedResponseEntry>;
19
42
  tags: string[];
43
+ /** Per-operation security requirements (each entry is OR, keys within are AND) */
44
+ security?: string[][];
45
+ /** OAS3 callbacks defined on this operation */
46
+ callbacks?: ParsedCallback[];
20
47
  }
21
48
  export interface ParsedParameter {
22
49
  name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK/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;AA8CD;;;GAGG;AACH,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAuH5E"}
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK/C,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,eAAe,CAAC;IACrD,2CAA2C;IAC3C,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;IACnC,+CAA+C;IAC/C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC9C,cAAc,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACpD,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,oFAAoF;IACpF,aAAa,EAAE,MAAM,CAAC;IACtB,2CAA2C;IAC3C,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC;IAC3B,gDAAgD;IAChD,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;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,mBAAmB,CAAC,CAAC;IAC5C,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC;IACtB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,cAAc,EAAE,CAAC;CAC9B;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;AA8CD;;;GAGG;AACH,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CA2I5E"}
package/dist/parser.js CHANGED
@@ -1,4 +1,4 @@
1
- /// <reference path="../../../types/schmock.d.ts" />
1
+ /// <reference path="../../core/schmock.d.ts" />
2
2
  import SwaggerParser from "@apidevtools/swagger-parser";
3
3
  import { toHttpMethod } from "@schmock/core";
4
4
  import { normalizeSchema } from "./normalizer.js";
@@ -89,10 +89,14 @@ export async function parseSpec(source) {
89
89
  if (basePath.endsWith("/") && basePath !== "/") {
90
90
  basePath = basePath.slice(0, -1);
91
91
  }
92
+ // Extract security schemes
93
+ const securitySchemes = extractSecuritySchemes(api, isSwagger2);
94
+ const globalSecurityRaw = "security" in api ? api.security : undefined;
95
+ const globalSecurity = extractSecurityRequirements(Array.isArray(globalSecurityRaw) ? globalSecurityRaw : undefined);
92
96
  const paths = [];
93
97
  const rawPaths = "paths" in api && isRecord(api.paths) ? api.paths : undefined;
94
98
  if (!rawPaths) {
95
- return { title, version, basePath, paths };
99
+ return { title, version, basePath, paths, securitySchemes, globalSecurity };
96
100
  }
97
101
  for (const [pathTemplate, pathItemRaw] of Object.entries(rawPaths)) {
98
102
  if (!isRecord(pathItemRaw))
@@ -125,6 +129,14 @@ export async function parseSpec(source) {
125
129
  const tags = Array.isArray(operation.tags)
126
130
  ? operation.tags.filter((t) => typeof t === "string")
127
131
  : [];
132
+ // Extract per-operation security
133
+ const operationSecurity = Array.isArray(operation.security)
134
+ ? extractSecurityRequirements(operation.security)
135
+ : undefined;
136
+ // Extract OAS3 callbacks
137
+ const callbacks = !isSwagger2 && isRecord(operation.callbacks)
138
+ ? extractCallbacks(operation.callbacks)
139
+ : undefined;
128
140
  // Filter out body parameters from the final parameter list (Swagger 2.0)
129
141
  const filteredParams = mergedParams.filter(isNotBodyParam);
130
142
  paths.push({
@@ -135,10 +147,12 @@ export async function parseSpec(source) {
135
147
  requestBody,
136
148
  responses,
137
149
  tags,
150
+ security: operationSecurity,
151
+ callbacks,
138
152
  });
139
153
  }
140
154
  }
141
- return { title, version, basePath, paths };
155
+ return { title, version, basePath, paths, securitySchemes, globalSecurity };
142
156
  }
143
157
  function isValidParamLocation(location, isSwagger2) {
144
158
  const validLocations = isSwagger2
@@ -237,26 +251,87 @@ function extractResponses(responses, isSwagger2) {
237
251
  continue;
238
252
  const description = getString(response.description) ?? "";
239
253
  let schema;
254
+ let examples;
255
+ let contentTypes;
240
256
  if (isSwagger2) {
241
- // Swagger 2.0: schema is directly on the response
242
257
  if (isRecord(response.schema)) {
243
258
  schema = normalizeSchema(response.schema, "response");
244
259
  }
260
+ // Swagger 2.0 single example
261
+ if (response.examples !== undefined && isRecord(response.examples)) {
262
+ examples = new Map();
263
+ for (const [key, value] of Object.entries(response.examples)) {
264
+ examples.set(key, value);
265
+ }
266
+ }
245
267
  }
246
268
  else {
247
- // OpenAPI 3.x: schema is nested in content
248
269
  const content = isRecord(response.content) ? response.content : undefined;
249
270
  if (content) {
271
+ contentTypes = Object.keys(content);
250
272
  const jsonEntry = findJsonContent(content);
251
273
  if (jsonEntry && isRecord(jsonEntry.schema)) {
252
274
  schema = normalizeSchema(jsonEntry.schema, "response");
253
275
  }
276
+ // OAS3 named examples
277
+ if (jsonEntry) {
278
+ examples = extractExamples(jsonEntry);
279
+ }
254
280
  }
255
281
  }
256
- result.set(code, { schema, description });
282
+ const headers = extractResponseHeaders(response, isSwagger2);
283
+ result.set(code, { schema, description, headers, examples, contentTypes });
257
284
  }
258
285
  return result;
259
286
  }
287
+ function extractExamples(contentEntry) {
288
+ const result = new Map();
289
+ // Single `example` value
290
+ if ("example" in contentEntry && contentEntry.example !== undefined) {
291
+ result.set("default", contentEntry.example);
292
+ }
293
+ // Named `examples` map
294
+ if (isRecord(contentEntry.examples)) {
295
+ for (const [name, exampleObj] of Object.entries(contentEntry.examples)) {
296
+ if (isRecord(exampleObj) && "value" in exampleObj) {
297
+ result.set(name, exampleObj.value);
298
+ }
299
+ }
300
+ }
301
+ return result.size > 0 ? result : undefined;
302
+ }
303
+ function extractResponseHeaders(response, isSwagger2) {
304
+ const rawHeaders = isRecord(response.headers) ? response.headers : undefined;
305
+ if (!rawHeaders)
306
+ return undefined;
307
+ const headers = {};
308
+ let hasHeaders = false;
309
+ for (const [name, headerRaw] of Object.entries(rawHeaders)) {
310
+ if (!isRecord(headerRaw))
311
+ continue;
312
+ const desc = getString(headerRaw.description) ?? "";
313
+ let headerSchema;
314
+ if (isSwagger2) {
315
+ // Swagger 2.0: type/format/enum are inline on the header
316
+ if (headerRaw.type) {
317
+ headerSchema = normalizeSchema({
318
+ type: headerRaw.type,
319
+ format: headerRaw.format,
320
+ enum: headerRaw.enum,
321
+ }, "response");
322
+ }
323
+ }
324
+ else {
325
+ // OpenAPI 3.x: schema is nested
326
+ if (isRecord(headerRaw.schema)) {
327
+ headerSchema = normalizeSchema(headerRaw.schema, "response");
328
+ }
329
+ }
330
+ headers[name] = { schema: headerSchema, description: desc };
331
+ hasHeaders = true;
332
+ }
333
+ return hasHeaders ? headers : undefined;
334
+ }
260
335
  /**
261
336
  * Find the best JSON-like content type entry from an OpenAPI content map.
262
337
  * Prefers application/json, then any *+json or *json* type.
@@ -278,3 +353,130 @@ function findJsonContent(content) {
278
353
  function convertPathTemplate(path) {
279
354
  return path.replace(/\{([^}]+)\}/g, ":$1");
280
355
  }
356
+ function extractSecuritySchemes(api, isSwagger2) {
357
+ const schemes = new Map();
358
+ let rawSchemes;
359
+ if (isSwagger2) {
360
+ // Swagger 2.0: securityDefinitions
361
+ if ("securityDefinitions" in api) {
362
+ const defs = api.securityDefinitions;
363
+ if (isRecord(defs)) {
364
+ rawSchemes = defs;
365
+ }
366
+ }
367
+ }
368
+ else {
369
+ // OpenAPI 3.x: components.securitySchemes
370
+ if ("components" in api && isRecord(api.components)) {
371
+ const comp = api.components;
372
+ if ("securitySchemes" in comp && isRecord(comp.securitySchemes)) {
373
+ rawSchemes = comp.securitySchemes;
374
+ }
375
+ }
376
+ }
377
+ if (!rawSchemes)
378
+ return schemes.size > 0 ? schemes : undefined;
379
+ for (const [name, schemeDef] of Object.entries(rawSchemes)) {
380
+ if (!isRecord(schemeDef))
381
+ continue;
382
+ const type = getString(schemeDef.type);
383
+ if (!type)
384
+ continue;
385
+ const scheme = toSecurityScheme(type, schemeDef, isSwagger2);
386
+ if (scheme) {
387
+ schemes.set(name, scheme);
388
+ }
389
+ }
390
+ return schemes.size > 0 ? schemes : undefined;
391
+ }
392
+ const SECURITY_SCHEME_TYPES = new Set([
393
+ "apiKey",
394
+ "http",
395
+ "oauth2",
396
+ "openIdConnect",
397
+ ]);
398
+ const API_KEY_LOCATIONS = new Set(["header", "query", "cookie"]);
399
+ function toSecurityScheme(type, def, isSwagger2) {
400
+ // Handle Swagger 2.0 basic auth
401
+ if (isSwagger2 && type === "basic") {
402
+ return { type: "http", scheme: "basic" };
403
+ }
404
+ if (!SECURITY_SCHEME_TYPES.has(type))
405
+ return undefined;
406
+ const scheme = {
407
+ type: type === "apiKey"
408
+ ? "apiKey"
409
+ : type === "http"
410
+ ? "http"
411
+ : type === "oauth2"
412
+ ? "oauth2"
413
+ : "openIdConnect",
414
+ };
415
+ if (type === "apiKey") {
416
+ const location = getString(def.in);
417
+ if (location && API_KEY_LOCATIONS.has(location)) {
418
+ scheme.in =
419
+ location === "header"
420
+ ? "header"
421
+ : location === "query"
422
+ ? "query"
423
+ : "cookie";
424
+ }
425
+ scheme.name = getString(def.name);
426
+ }
427
+ else if (type === "http") {
428
+ scheme.scheme = getString(def.scheme);
429
+ }
430
+ return scheme;
431
+ }
432
+ /**
433
+ * Extract security requirements from a security array.
434
+ * Each entry in the array is an OR condition (any can match).
435
+ * Each entry is an object where keys are scheme names (AND within).
436
+ * Returns array of string arrays: [[schemeA, schemeB], [schemeC]] means (A AND B) OR C.
437
+ * An empty array entry means "no auth required" (public).
438
+ */
439
+ function extractSecurityRequirements(security) {
440
+ if (!security || security.length === 0)
441
+ return undefined;
442
+ const result = [];
443
+ for (const entry of security) {
444
+ if (!isRecord(entry))
445
+ continue;
446
+ result.push(Object.keys(entry));
447
+ }
448
+ return result.length > 0 ? result : undefined;
449
+ }
450
+ /**
451
+ * Extract OAS3 callbacks from an operation.
452
+ * Callbacks structure: { callbackName: { urlExpression: { method: { requestBody, ... } } } }
453
+ */
454
+ function extractCallbacks(callbacks) {
455
+ const result = [];
456
+ for (const callbackObj of Object.values(callbacks)) {
457
+ if (!isRecord(callbackObj))
458
+ continue;
459
+ // Each key is a URL expression like "{$request.body#/callbackUrl}"
460
+ for (const [urlExpression, pathItem] of Object.entries(callbackObj)) {
461
+ if (!isRecord(pathItem))
462
+ continue;
463
+ for (const methodKey of Object.keys(pathItem)) {
464
+ if (!HTTP_METHOD_KEYS.has(methodKey))
465
+ continue;
466
+ const operation = pathItem[methodKey];
467
+ if (!isRecord(operation))
468
+ continue;
469
+ let reqBody;
470
+ if (isRecord(operation.requestBody)) {
471
+ reqBody = extractOpenApi3RequestBody(operation.requestBody);
472
+ }
473
+ result.push({
474
+ urlExpression,
475
+ method: toHttpMethod(methodKey.toUpperCase()),
476
+ requestBody: reqBody,
477
+ });
478
+ }
479
+ }
480
+ }
481
+ return result.length > 0 ? result : undefined;
482
+ }
package/dist/plugin.d.ts CHANGED
@@ -5,7 +5,7 @@ export interface OpenApiOptions {
5
5
  spec: string | object;
6
6
  /** Optional seed data per resource */
7
7
  seed?: SeedConfig;
8
- /** Validate request bodies (default: true) */
8
+ /** Validate request bodies (default: false) */
9
9
  validateRequests?: boolean;
10
10
  /** Validate response bodies (default: false) */
11
11
  validateResponses?: boolean;
@@ -15,6 +15,14 @@ export interface OpenApiOptions {
15
15
  sorting?: boolean;
16
16
  filtering?: boolean;
17
17
  };
18
+ /** Override auto-detected response format per resource */
19
+ resources?: Record<string, Schmock.ResourceOverride>;
20
+ /** Log auto-detection decisions to console (default: false) */
21
+ debug?: boolean;
22
+ /** Seed for deterministic random generation */
23
+ fakerSeed?: number;
24
+ /** Validate security schemes (API key, Bearer, Basic) (default: false) */
25
+ security?: boolean;
18
26
  }
19
27
  /**
20
28
  * Create an OpenAPI plugin that auto-registers CRUD routes from a spec.
@@ -1 +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;AAIxD,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC;AAEvC,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"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAIxD,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC;AAEvC,MAAM,WAAW,cAAc;IAC7B,sCAAsC;IACtC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,sCAAsC;IACtC,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,+CAA+C;IAC/C,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;IACF,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACrD,+DAA+D;IAC/D,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,OAAO,CAC3B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CA0GzB"}