@schmock/openapi 1.2.1 → 1.7.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/plugin.js CHANGED
@@ -1,7 +1,11 @@
1
- /// <reference path="../../../types/schmock.d.ts" />
1
+ /// <reference path="../../core/schmock.d.ts" />
2
+ import { generateFromSchema } from "@schmock/faker";
3
+ import Ajv from "ajv";
4
+ import { negotiateContentType } from "./content-negotiation.js";
2
5
  import { detectCrudResources } from "./crud-detector.js";
3
- import { createCreateGenerator, createDeleteGenerator, createListGenerator, createReadGenerator, createStaticGenerator, createUpdateGenerator, } from "./generators.js";
6
+ import { createCreateGenerator, createDeleteGenerator, createListGenerator, createReadGenerator, createStaticGenerator, createUpdateGenerator, findArrayProperty, } from "./generators.js";
4
7
  import { parseSpec } from "./parser.js";
8
+ import { parsePreferHeader } from "./prefer.js";
5
9
  import { loadSeed } from "./seed.js";
6
10
  import { isRecord } from "./utils.js";
7
11
  /**
@@ -22,72 +26,439 @@ export async function openapi(options) {
22
26
  const seedData = options.seed
23
27
  ? loadSeed(options.seed, resources)
24
28
  : new Map();
29
+ // Build a lookup of all parsed paths for process() to reference
30
+ const allParsedPaths = new Map();
31
+ for (const pp of [...spec.paths]) {
32
+ allParsedPaths.set(`${pp.method} ${pp.path}`, pp);
33
+ }
34
+ // Security scheme lookup
35
+ const securitySchemes = spec.securitySchemes;
36
+ const globalSecurity = spec.globalSecurity;
25
37
  return {
26
38
  name: "@schmock/openapi",
27
- version: "1.1.1",
39
+ version: "1.4.0",
28
40
  install(instance) {
29
- // Seed initial state — we need state to be initialized before registering routes
30
- // The state is populated via generator context.state on first request,
31
- // but we need to pre-populate it. We'll use a global config state approach.
32
- // Since generators use ctx.state (the global config state), we set up seed
33
- // data through a setup route that runs on first access, or we just seed
34
- // in the generators themselves.
35
- // Register CRUD routes
41
+ if (options.debug) {
42
+ console.log(`[@schmock/openapi] Detected ${resources.length} CRUD resources, ${nonCrudPaths.length} static routes`);
43
+ }
44
+ // Register CRUD routes with metadata
36
45
  for (const resource of resources) {
37
- registerCrudRoutes(instance, resource, seedData.get(resource.name));
46
+ const override = options.resources?.[resource.name];
47
+ if (override) {
48
+ applyOverrides(resource, override);
49
+ }
50
+ if (options.debug) {
51
+ logResourceDetection(resource, override);
52
+ }
53
+ registerCrudRoutes(instance, resource, seedData.get(resource.name), allParsedPaths);
38
54
  }
39
55
  // Register non-CRUD routes with static generators
40
56
  for (const parsedPath of nonCrudPaths) {
41
57
  const routeKey = `${parsedPath.method} ${parsedPath.path}`;
42
- instance(routeKey, createStaticGenerator(parsedPath));
58
+ const config = {
59
+ "openapi:responses": parsedPath.responses,
60
+ "openapi:path": parsedPath.path,
61
+ "openapi:requestBody": parsedPath.requestBody,
62
+ "openapi:security": parsedPath.security,
63
+ "openapi:callbacks": parsedPath.callbacks,
64
+ };
65
+ instance(routeKey, createStaticGenerator(parsedPath, options.fakerSeed), config);
43
66
  }
44
67
  },
45
68
  process(context, response) {
46
- // Pass through generators handle everything
47
- return { context, response };
69
+ // 1. Security validation (if enabled)
70
+ if (options.security && securitySchemes) {
71
+ const securityResult = validateSecurity(context, securitySchemes, globalSecurity);
72
+ if (securityResult)
73
+ return securityResult;
74
+ }
75
+ // 2. Content negotiation
76
+ const contentResult = processContentNegotiation(context);
77
+ if (contentResult)
78
+ return contentResult;
79
+ // 3. Request validation (if enabled)
80
+ if (options.validateRequests) {
81
+ const validationResult = validateRequestBody(context);
82
+ if (validationResult) {
83
+ return validationResult;
84
+ }
85
+ }
86
+ // 4. Prefer header handling
87
+ const result = processPreferHeader(context, response, options.fakerSeed);
88
+ // 5. Fire callbacks (fire-and-forget, after response is determined)
89
+ const callbacks = context.route["openapi:callbacks"];
90
+ if (callbacks && callbacks.length > 0) {
91
+ fireCallbacks(callbacks, context, result.response);
92
+ }
93
+ return result;
48
94
  },
49
95
  };
50
96
  }
51
- function registerCrudRoutes(instance, resource, seedItems) {
52
- // Create a seeded generator wrapper that initializes state on first call
53
- const ensureSeeded = createSeeder(resource, seedItems);
54
- for (const op of resource.operations) {
55
- switch (op) {
56
- case "list": {
57
- const gen = createListGenerator(resource);
58
- const routeKey = `GET ${resource.basePath}`;
59
- instance(routeKey, wrapWithSeeder(ensureSeeded, gen));
60
- break;
97
+ /**
98
+ * Validate security requirements for the request.
99
+ * Returns 401 if auth is required and missing/invalid.
100
+ */
101
+ function validateSecurity(context, schemes, globalSecurity) {
102
+ // Determine applicable security: operation-level overrides global
103
+ const routeSecurity = context.route["openapi:security"];
104
+ const security = routeSecurity ?? globalSecurity;
105
+ // No security requirements
106
+ if (!security || security.length === 0)
107
+ return undefined;
108
+ // Check each OR group — if any group passes, request is authorized
109
+ for (const group of security) {
110
+ // Empty group = public endpoint (security: [{}])
111
+ if (group.length === 0)
112
+ return undefined;
113
+ // All schemes in the group must pass (AND)
114
+ const allPass = group.every((schemeName) => {
115
+ const scheme = schemes.get(schemeName);
116
+ if (!scheme)
117
+ return false;
118
+ return checkSchemePresence(scheme, context.headers);
119
+ });
120
+ if (allPass)
121
+ return undefined;
122
+ }
123
+ // No group passed — build WWW-Authenticate header
124
+ const wwwAuth = buildWwwAuthenticate(security, schemes);
125
+ const headers = {};
126
+ if (wwwAuth) {
127
+ headers["www-authenticate"] = wwwAuth;
128
+ }
129
+ return {
130
+ context,
131
+ response: [
132
+ 401,
133
+ {
134
+ error: "Unauthorized",
135
+ code: "UNAUTHORIZED",
136
+ },
137
+ headers,
138
+ ],
139
+ };
140
+ }
141
+ function checkSchemePresence(scheme, headers) {
142
+ if (scheme.type === "http") {
143
+ const auth = headers.authorization ?? headers.Authorization ?? "";
144
+ if (scheme.scheme === "bearer") {
145
+ return auth.toLowerCase().startsWith("bearer ");
146
+ }
147
+ if (scheme.scheme === "basic") {
148
+ return auth.toLowerCase().startsWith("basic ");
149
+ }
150
+ // Other http schemes — just check authorization exists
151
+ return auth.length > 0;
152
+ }
153
+ if (scheme.type === "apiKey") {
154
+ if (scheme.in === "header" && scheme.name) {
155
+ const headerName = scheme.name.toLowerCase();
156
+ return (headers[headerName] ?? headers[scheme.name] ?? "") !== "";
157
+ }
158
+ // query and cookie api keys can't be checked from headers alone
159
+ // For simplicity, pass through (they'd need query/cookie context)
160
+ return true;
161
+ }
162
+ // oauth2 / openIdConnect — just check for bearer token
163
+ if (scheme.type === "oauth2" || scheme.type === "openIdConnect") {
164
+ const auth = headers.authorization ?? headers.Authorization ?? "";
165
+ return auth.toLowerCase().startsWith("bearer ");
166
+ }
167
+ return true;
168
+ }
169
+ function buildWwwAuthenticate(security, schemes) {
170
+ const challenges = [];
171
+ for (const group of security) {
172
+ for (const schemeName of group) {
173
+ const scheme = schemes.get(schemeName);
174
+ if (!scheme)
175
+ continue;
176
+ if (scheme.type === "http" && scheme.scheme === "bearer") {
177
+ if (!challenges.includes("Bearer"))
178
+ challenges.push("Bearer");
61
179
  }
62
- case "create": {
63
- const gen = createCreateGenerator(resource);
64
- const routeKey = `POST ${resource.basePath}`;
65
- instance(routeKey, wrapWithSeeder(ensureSeeded, gen));
66
- break;
180
+ else if (scheme.type === "http" && scheme.scheme === "basic") {
181
+ if (!challenges.includes("Basic"))
182
+ challenges.push("Basic");
67
183
  }
68
- case "read": {
69
- const gen = createReadGenerator(resource);
70
- const routeKey = `GET ${resource.itemPath}`;
71
- instance(routeKey, wrapWithSeeder(ensureSeeded, gen));
72
- break;
184
+ else if (scheme.type === "oauth2" || scheme.type === "openIdConnect") {
185
+ if (!challenges.includes("Bearer"))
186
+ challenges.push("Bearer");
73
187
  }
74
- case "update": {
75
- const gen = createUpdateGenerator(resource);
76
- const putKey = `PUT ${resource.itemPath}`;
77
- const patchKey = `PATCH ${resource.itemPath}`;
78
- instance(putKey, wrapWithSeeder(ensureSeeded, gen));
79
- instance(patchKey, wrapWithSeeder(ensureSeeded, gen));
80
- break;
188
+ }
189
+ }
190
+ return challenges.join(", ");
191
+ }
192
+ /**
193
+ * Check Accept header against available content types. Returns 406 if no match.
194
+ */
195
+ function processContentNegotiation(context) {
196
+ const accept = context.headers.accept ?? context.headers.Accept;
197
+ if (!accept || accept === "*/*")
198
+ return undefined;
199
+ const responses = context.route["openapi:responses"];
200
+ if (!responses)
201
+ return undefined;
202
+ // Collect all available content types across responses
203
+ const allContentTypes = new Set();
204
+ for (const entry of responses.values()) {
205
+ if (entry.contentTypes) {
206
+ for (const ct of entry.contentTypes) {
207
+ allContentTypes.add(ct);
81
208
  }
82
- case "delete": {
83
- const gen = createDeleteGenerator(resource);
84
- const routeKey = `DELETE ${resource.itemPath}`;
85
- instance(routeKey, wrapWithSeeder(ensureSeeded, gen));
86
- break;
209
+ }
210
+ }
211
+ // No content types defined in spec → skip negotiation
212
+ if (allContentTypes.size === 0)
213
+ return undefined;
214
+ const matched = negotiateContentType(accept, [...allContentTypes]);
215
+ if (!matched) {
216
+ return {
217
+ context,
218
+ response: [
219
+ 406,
220
+ {
221
+ error: "Not Acceptable",
222
+ code: "NOT_ACCEPTABLE",
223
+ acceptable: [...allContentTypes],
224
+ },
225
+ ],
226
+ };
227
+ }
228
+ return undefined;
229
+ }
230
+ /**
231
+ * Handle Prefer header directives: code=N, example=name, dynamic=true
232
+ */
233
+ function processPreferHeader(context, response, fakerSeed) {
234
+ const preferValue = context.headers.prefer ?? context.headers.Prefer;
235
+ if (!preferValue) {
236
+ return { context, response };
237
+ }
238
+ const prefer = parsePreferHeader(preferValue);
239
+ const responses = context.route["openapi:responses"];
240
+ if (!responses) {
241
+ return { context, response };
242
+ }
243
+ // Prefer: code=N — return the response for that status code
244
+ if (prefer.code !== undefined) {
245
+ const entry = responses.get(prefer.code);
246
+ if (entry) {
247
+ const body = entry.schema
248
+ ? generateResponseBody(entry.schema, fakerSeed)
249
+ : {};
250
+ return { context, response: [prefer.code, body] };
251
+ }
252
+ }
253
+ // Prefer: example=name — find a named example across responses
254
+ if (prefer.example !== undefined) {
255
+ for (const [code, entry] of responses) {
256
+ if (entry.examples?.has(prefer.example)) {
257
+ return {
258
+ context,
259
+ response: [code, entry.examples.get(prefer.example)],
260
+ };
261
+ }
262
+ }
263
+ }
264
+ // Prefer: dynamic=true — regenerate from schema
265
+ if (prefer.dynamic) {
266
+ for (const [code, entry] of responses) {
267
+ if (code >= 200 && code < 300 && entry.schema) {
268
+ const body = generateResponseBody(entry.schema, fakerSeed);
269
+ return { context, response: [code, body] };
270
+ }
271
+ }
272
+ }
273
+ return { context, response };
274
+ }
275
+ const ajv = new Ajv({ allErrors: true });
276
+ /**
277
+ * Validate request body against the spec's requestBody schema.
278
+ * Returns a PluginResult with 400 status if validation fails, or undefined to continue.
279
+ */
280
+ function validateRequestBody(context) {
281
+ const requestBodySchema = context.route["openapi:requestBody"];
282
+ if (!requestBodySchema || context.body === undefined) {
283
+ return undefined;
284
+ }
285
+ const validate = ajv.compile(requestBodySchema);
286
+ if (!validate(context.body)) {
287
+ const errors = validate.errors?.map((e) => ({
288
+ path: e.instancePath || "/",
289
+ message: e.message ?? "validation failed",
290
+ keyword: e.keyword,
291
+ })) ?? [];
292
+ return {
293
+ context,
294
+ response: [
295
+ 400,
296
+ {
297
+ error: "Request validation failed",
298
+ code: "VALIDATION_ERROR",
299
+ details: errors,
300
+ },
301
+ ],
302
+ };
303
+ }
304
+ return undefined;
305
+ }
306
+ /**
307
+ * Resolve a callback URL expression using runtime values.
308
+ * Handles expressions like "{$request.body#/callbackUrl}" and literal URLs.
309
+ */
310
+ function resolveCallbackUrl(expression, context, response) {
311
+ // Replace all runtime expression tokens
312
+ return expression.replace(/\{\$([^}]+)\}/g, (_, expr) => {
313
+ // $request.body#/path — JSON pointer into request body
314
+ if (expr.startsWith("request.body#")) {
315
+ const pointer = expr.slice("request.body#".length);
316
+ const value = resolveJsonPointer(context.body, pointer);
317
+ return typeof value === "string" ? value : "";
318
+ }
319
+ // $request.header.name
320
+ if (expr.startsWith("request.header.")) {
321
+ const headerName = expr.slice("request.header.".length).toLowerCase();
322
+ return context.headers[headerName] ?? "";
323
+ }
324
+ // $request.query.name
325
+ if (expr.startsWith("request.query.")) {
326
+ const queryName = expr.slice("request.query.".length);
327
+ return context.query[queryName] ?? "";
328
+ }
329
+ // $request.path.param
330
+ if (expr.startsWith("request.path.")) {
331
+ const paramName = expr.slice("request.path.".length);
332
+ return context.params[paramName] ?? "";
333
+ }
334
+ // $response.body#/path — JSON pointer into response body
335
+ if (expr.startsWith("response.body#")) {
336
+ const pointer = expr.slice("response.body#".length);
337
+ const responseBody = Array.isArray(response) ? response[1] : response;
338
+ const value = resolveJsonPointer(responseBody, pointer);
339
+ return typeof value === "string" ? value : "";
340
+ }
341
+ return "";
342
+ });
343
+ }
344
+ function resolveJsonPointer(obj, pointer) {
345
+ if (!isRecord(obj) || !pointer.startsWith("/"))
346
+ return undefined;
347
+ const parts = pointer.slice(1).split("/");
348
+ let current = obj;
349
+ for (const part of parts) {
350
+ if (!isRecord(current))
351
+ return undefined;
352
+ current = current[part];
353
+ }
354
+ return current;
355
+ }
356
+ /**
357
+ * Fire callbacks in a fire-and-forget manner.
358
+ * Silently ignores failures — callbacks are best-effort.
359
+ */
360
+ function fireCallbacks(callbacks, context, response) {
361
+ for (const callback of callbacks) {
362
+ const url = resolveCallbackUrl(callback.urlExpression, context, response);
363
+ if (!url || !url.startsWith("http"))
364
+ continue;
365
+ const body = Array.isArray(response) ? response[1] : response;
366
+ // Fire and forget
367
+ void fetch(url, {
368
+ method: callback.method,
369
+ headers: { "content-type": "application/json" },
370
+ body: body !== undefined ? JSON.stringify(body) : undefined,
371
+ }).catch(() => {
372
+ // Silently ignore callback failures
373
+ });
374
+ }
375
+ }
376
+ function generateResponseBody(schema, seed) {
377
+ try {
378
+ return generateFromSchema({ schema, seed });
379
+ }
380
+ catch {
381
+ return {};
382
+ }
383
+ }
384
+ function registerCrudRoutes(instance, resource, seedItems, allParsedPaths) {
385
+ const ensureSeeded = createSeeder(resource, seedItems);
386
+ for (const op of resource.operations) {
387
+ const meta = resource.operationMeta?.get(op);
388
+ const routeEntries = getCrudRouteEntries(op, resource);
389
+ for (const { routeKey, method } of routeEntries) {
390
+ const gen = createCrudGenerator(op, resource, meta);
391
+ const parsedPath = allParsedPaths.get(`${method} ${resource.basePath}`) ??
392
+ allParsedPaths.get(`${method} ${resource.itemPath}`);
393
+ const config = {};
394
+ if (parsedPath) {
395
+ config["openapi:responses"] = parsedPath.responses;
396
+ config["openapi:path"] = parsedPath.path;
397
+ config["openapi:requestBody"] = parsedPath.requestBody;
398
+ config["openapi:security"] = parsedPath.security;
399
+ config["openapi:callbacks"] = parsedPath.callbacks;
87
400
  }
401
+ instance(routeKey, wrapWithSeeder(ensureSeeded, gen), config);
88
402
  }
89
403
  }
90
404
  }
405
+ function getCrudRouteEntries(op, resource) {
406
+ switch (op) {
407
+ case "list":
408
+ return [
409
+ {
410
+ routeKey: `GET ${resource.basePath}`,
411
+ method: "GET",
412
+ },
413
+ ];
414
+ case "create":
415
+ return [
416
+ {
417
+ routeKey: `POST ${resource.basePath}`,
418
+ method: "POST",
419
+ },
420
+ ];
421
+ case "read":
422
+ return [
423
+ {
424
+ routeKey: `GET ${resource.itemPath}`,
425
+ method: "GET",
426
+ },
427
+ ];
428
+ case "update":
429
+ return [
430
+ {
431
+ routeKey: `PUT ${resource.itemPath}`,
432
+ method: "PUT",
433
+ },
434
+ {
435
+ routeKey: `PATCH ${resource.itemPath}`,
436
+ method: "PATCH",
437
+ },
438
+ ];
439
+ case "delete":
440
+ return [
441
+ {
442
+ routeKey: `DELETE ${resource.itemPath}`,
443
+ method: "DELETE",
444
+ },
445
+ ];
446
+ }
447
+ }
448
+ function createCrudGenerator(op, resource, meta) {
449
+ switch (op) {
450
+ case "list":
451
+ return createListGenerator(resource, meta);
452
+ case "create":
453
+ return createCreateGenerator(resource, meta);
454
+ case "read":
455
+ return createReadGenerator(resource, meta);
456
+ case "update":
457
+ return createUpdateGenerator(resource, meta);
458
+ case "delete":
459
+ return createDeleteGenerator(resource, meta);
460
+ }
461
+ }
91
462
  /**
92
463
  * Create a seeder function that initializes collection state once.
93
464
  */
@@ -101,7 +472,6 @@ function createSeeder(resource, seedItems) {
101
472
  state[seededKey] = true;
102
473
  if (seedItems && seedItems.length > 0) {
103
474
  state[stateKey] = [...seedItems];
104
- // Set counter to highest existing ID
105
475
  let maxId = 0;
106
476
  for (const item of seedItems) {
107
477
  if (isRecord(item) && resource.idParam in item) {
@@ -125,3 +495,92 @@ function wrapWithSeeder(seeder, generator) {
125
495
  return generator(ctx);
126
496
  };
127
497
  }
498
+ /**
499
+ * Apply manual overrides to a resource's operation metadata.
500
+ */
501
+ function applyOverrides(resource, override) {
502
+ if (!resource.operationMeta) {
503
+ resource.operationMeta = new Map();
504
+ }
505
+ if (override.listWrapProperty !== undefined ||
506
+ override.listFlat !== undefined) {
507
+ const listMeta = resource.operationMeta.get("list") ?? {};
508
+ if (override.listFlat) {
509
+ delete listMeta.responseSchema;
510
+ }
511
+ else if (override.listWrapProperty && listMeta.responseSchema) {
512
+ const arrayInfo = findArrayProperty(listMeta.responseSchema);
513
+ if (!arrayInfo.property ||
514
+ arrayInfo.property !== override.listWrapProperty) {
515
+ const itemSchema = arrayInfo.itemSchema ?? resource.schema ?? {};
516
+ listMeta.responseSchema = {
517
+ type: "object",
518
+ properties: {
519
+ [override.listWrapProperty]: {
520
+ type: "array",
521
+ items: itemSchema,
522
+ },
523
+ },
524
+ };
525
+ }
526
+ }
527
+ else if (override.listWrapProperty) {
528
+ const itemSchema = resource.schema ?? {};
529
+ listMeta.responseSchema = {
530
+ type: "object",
531
+ properties: {
532
+ [override.listWrapProperty]: {
533
+ type: "array",
534
+ items: itemSchema,
535
+ },
536
+ },
537
+ };
538
+ }
539
+ resource.operationMeta.set("list", listMeta);
540
+ }
541
+ if (override.errorSchema) {
542
+ const errorSchemaMap = new Map();
543
+ errorSchemaMap.set(404, override.errorSchema);
544
+ errorSchemaMap.set(400, override.errorSchema);
545
+ errorSchemaMap.set(409, override.errorSchema);
546
+ for (const op of ["read", "update", "delete"]) {
547
+ const meta = resource.operationMeta.get(op) ?? {};
548
+ meta.errorSchemas = errorSchemaMap;
549
+ resource.operationMeta.set(op, meta);
550
+ }
551
+ }
552
+ }
553
+ /**
554
+ * Log resource detection info for debug mode.
555
+ */
556
+ function logResourceDetection(resource, override) {
557
+ const listMeta = resource.operationMeta?.get("list");
558
+ let listFormat = "flat";
559
+ if (listMeta?.responseSchema) {
560
+ const arrayInfo = findArrayProperty(listMeta.responseSchema);
561
+ if (arrayInfo.property) {
562
+ const hasAllOf = "allOf" in (listMeta.responseSchema ?? {});
563
+ listFormat = `wrapped("${arrayInfo.property}"${hasAllOf ? " via allOf" : ""})`;
564
+ }
565
+ }
566
+ const readMeta = resource.operationMeta?.get("read");
567
+ const errorFormat = readMeta?.errorSchemas?.has(404)
568
+ ? "schema(404)"
569
+ : "default";
570
+ const headerCount = listMeta?.responseHeaders
571
+ ? Object.keys(listMeta.responseHeaders).length
572
+ : 0;
573
+ console.log(`[@schmock/openapi] ${resource.name}: list=${listFormat}, error=${errorFormat}, headers=${headerCount}`);
574
+ if (override) {
575
+ const definedKeys = [];
576
+ if (override.listWrapProperty !== undefined)
577
+ definedKeys.push("listWrapProperty");
578
+ if (override.listFlat !== undefined)
579
+ definedKeys.push("listFlat");
580
+ if (override.errorSchema !== undefined)
581
+ definedKeys.push("errorSchema");
582
+ if (definedKeys.length > 0) {
583
+ console.log(`[@schmock/openapi] Override applied: ${resource.name}.${definedKeys.join(", ")}`);
584
+ }
585
+ }
586
+ }
@@ -0,0 +1,12 @@
1
+ interface PreferDirectives {
2
+ code?: number;
3
+ example?: string;
4
+ dynamic?: boolean;
5
+ }
6
+ /**
7
+ * Parse the RFC 7240 Prefer header for mock-specific directives.
8
+ * Supports: code=N, example=name, dynamic=true
9
+ */
10
+ export declare function parsePreferHeader(value: string): PreferDirectives;
11
+ export {};
12
+ //# sourceMappingURL=prefer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer.d.ts","sourceRoot":"","sources":["../src/prefer.ts"],"names":[],"mappings":"AAEA,UAAU,gBAAgB;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,CAwBjE"}
package/dist/prefer.js ADDED
@@ -0,0 +1,25 @@
1
+ /// <reference path="../../core/schmock.d.ts" />
2
+ /**
3
+ * Parse the RFC 7240 Prefer header for mock-specific directives.
4
+ * Supports: code=N, example=name, dynamic=true
5
+ */
6
+ export function parsePreferHeader(value) {
7
+ const result = {};
8
+ for (const part of value.split(",")) {
9
+ const trimmed = part.trim();
10
+ const codeMatch = trimmed.match(/^code\s*=\s*(\d+)$/);
11
+ if (codeMatch) {
12
+ result.code = Number(codeMatch[1]);
13
+ continue;
14
+ }
15
+ const exampleMatch = trimmed.match(/^example\s*=\s*(.+)$/);
16
+ if (exampleMatch) {
17
+ result.example = exampleMatch[1].trim();
18
+ continue;
19
+ }
20
+ if (trimmed === "dynamic=true" || trimmed === "dynamic") {
21
+ result.dynamic = true;
22
+ }
23
+ }
24
+ return result;
25
+ }
package/dist/seed.js CHANGED
@@ -1,4 +1,4 @@
1
- /// <reference path="../../../types/schmock.d.ts" />
1
+ /// <reference path="../../core/schmock.d.ts" />
2
2
  import { readFileSync } from "node:fs";
3
3
  import { generateSeedItems } from "./generators.js";
4
4
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@schmock/openapi",
3
3
  "description": "OpenAPI/Swagger spec-driven auto-registration plugin for Schmock",
4
- "version": "1.2.1",
4
+ "version": "1.7.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -25,15 +25,17 @@
25
25
  "test:watch": "vitest --watch",
26
26
  "test:bdd": "vitest run --config vitest.config.bdd.ts",
27
27
  "lint": "biome check src/*.ts",
28
- "lint:fix": "biome check --write --unsafe src/*.ts"
28
+ "lint:fix": "biome check --write --unsafe src/*.ts",
29
+ "check:publish": "publint && attw --pack --ignore-rules cjs-resolves-to-esm"
29
30
  },
30
31
  "license": "MIT",
31
32
  "dependencies": {
32
33
  "@apidevtools/swagger-parser": "^12.0.0",
33
- "@schmock/schema": "^1.3.0"
34
+ "@schmock/faker": "^1.7.0",
35
+ "ajv": "^8.17.1"
34
36
  },
35
37
  "peerDependencies": {
36
- "@schmock/core": "^1.0.0"
38
+ "@schmock/core": "^1.7.0"
37
39
  },
38
40
  "devDependencies": {
39
41
  "@amiceli/vitest-cucumber": "^6.2.0",