@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.
@@ -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
+ }
@@ -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;AAC/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;CACtB;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"}
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"}
@@ -1,4 +1,5 @@
1
- /// <reference path="../../../types/schmock.d.ts" />
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 && listSchema.type === "array" && listSchema.items) {
73
- const items = Array.isArray(listSchema.items)
74
- ? listSchema.items[0]
75
- : listSchema.items;
76
- if (isRecord(items)) {
77
- schema = schema ?? toJsonSchema(items);
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]) {
@@ -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
- export declare function createListGenerator(resource: CrudResource): Schmock.GeneratorFunction;
5
- export declare function createCreateGenerator(resource: CrudResource): Schmock.GeneratorFunction;
6
- export declare function createReadGenerator(resource: CrudResource): Schmock.GeneratorFunction;
7
- export declare function createUpdateGenerator(resource: CrudResource): Schmock.GeneratorFunction;
8
- export declare function createDeleteGenerator(resource: CrudResource): Schmock.GeneratorFunction;
9
- export declare function createStaticGenerator(parsedPath: ParsedPath): Schmock.GeneratorFunction;
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
@@ -1 +1 @@
1
- {"version":3,"file":"generators.d.ts","sourceRoot":"","sources":["../src/generators.ts"],"names":[],"mappings":"AAGA,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;AAwC9C,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,YAAY,GACrB,OAAO,CAAC,iBAAiB,CAK3B;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,YAAY,GACrB,OAAO,CAAC,iBAAiB,CAkB3B;AAED,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,YAAY,GACrB,OAAO,CAAC,iBAAiB,CAY3B;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,YAAY,GACrB,OAAO,CAAC,iBAAiB,CAoB3B;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,YAAY,GACrB,OAAO,CAAC,iBAAiB,CAa3B;AAED,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,iBAAiB,CAiC3B;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,GACd,OAAO,EAAE,CAWX"}
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"}
@@ -1,7 +1,108 @@
1
- /// <reference path="../../../types/schmock.d.ts" />
2
- import { generateFromSchema } from "@schmock/schema";
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
- return [...collection];
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 toTuple(404, { error: "Not found", code: "NOT_FOUND" });
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 toTuple(404, { error: "Not found", code: "NOT_FOUND" });
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 toTuple(404, { error: "Not found", code: "NOT_FOUND" });
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))