@wylan24/mockgen 0.0.0-stage → 0.1.0-alpha.1

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,215 @@
1
+ import { generate } from './generate.js';
2
+ import { fits, mergeAllOf, typeOf } from './schema.js';
3
+ import { normalizeName } from './heuristics.js';
4
+ // List endpoints are treated as windows onto a fixed, seeded collection:
5
+ // record i always has the same data, `?page=2` shows records 10–19, and when a
6
+ // detail route exists (`/users` + `/users/{id}`), each list item is overlaid with
7
+ // the detail response for its id so the two always agree.
8
+ /** Property names that hold the records in a wrapped list response. */
9
+ const LIST_KEYS = /^(data|items|results|records|content|rows|entries|list|values|nodes|hits|objects|elements)$/;
10
+ /** Sibling names that mark an object as a page wrapper rather than a record with a nested array. */
11
+ const PAGE_HINT = /^(total|totalcount|count|page|pages|next|nextpage|cursor|nextcursor|meta|pagination|pageinfo|links|hasmore|hasnext|limit|offset)$/;
12
+ /** Recognize a list response: a bare array of objects, or an object wrapping one. */
13
+ export function findListShape(schema) {
14
+ if (!schema)
15
+ return undefined;
16
+ if (typeOf(schema) === 'array') {
17
+ return isObjectSchema(schema.items) ? { arraySchema: schema, itemSchema: schema.items } : undefined;
18
+ }
19
+ const props = (schema.allOf ? mergeAllOf(schema) : schema).properties;
20
+ if (!props)
21
+ return undefined;
22
+ const arrays = Object.entries(props).filter(([, s]) => typeOf(s) === 'array' && isObjectSchema(s.items));
23
+ const pick = arrays.find(([k]) => LIST_KEYS.test(normalizeName(k))) ??
24
+ (arrays.length === 1 && Object.keys(props).some((k) => PAGE_HINT.test(normalizeName(k))) ? arrays[0] : undefined);
25
+ return pick && { key: pick[0], arraySchema: pick[1], itemSchema: pick[1].items };
26
+ }
27
+ function isObjectSchema(s) {
28
+ return !!s && (typeOf(s) === 'object' || !!s.allOf);
29
+ }
30
+ /** The `GET /users/{id}` route for list route `GET /users`, if the spec has one. */
31
+ export function findDetailRoute(routes, list) {
32
+ const prefix = `${list.path.replace(/\/$/, '')}/{`;
33
+ return routes.find((r) => r.method === 'GET' && r.path.startsWith(prefix) && r.path.endsWith('}') && !r.path.slice(prefix.length).includes('/'));
34
+ }
35
+ const LIMIT_PARAMS = ['limit', 'per_page', 'perPage', 'page_size', 'pageSize', 'size', 'first'];
36
+ const OFFSET_PARAMS = ['offset', 'skip', 'start'];
37
+ const PAGE_PARAMS = ['page', 'page_number', 'pageNumber'];
38
+ const CURSOR_PARAMS = ['cursor', 'after', 'starting_after', 'page_token', 'pageToken'];
39
+ /** Work out which slice of the collection a request asks for. */
40
+ export function readPage(query, arraySchema, total, defaultLimit = 10) {
41
+ const min = Math.max(1, arraySchema.minItems ?? 1);
42
+ const max = Math.min(1000, arraySchema.maxItems ?? 1000);
43
+ const limit = clamp(firstInt(query, LIMIT_PARAMS) ?? defaultLimit, min, Math.max(min, max));
44
+ const cursor = CURSOR_PARAMS.map((p) => query.get(p)).find(Boolean);
45
+ const page = firstInt(query, PAGE_PARAMS);
46
+ const offset = Math.max(0, (cursor ? decodeCursor(cursor) : undefined) ?? firstInt(query, OFFSET_PARAMS) ?? ((page ?? 1) - 1) * limit);
47
+ return { offset, limit, page: page && page > 0 ? page : Math.floor(offset / limit) + 1, total };
48
+ }
49
+ export function buildPage(input) {
50
+ const { shape, query, total } = input;
51
+ const win = readPage(query, shape.arraySchema, total);
52
+ const item = shape.itemSchema.allOf ? mergeAllOf(shape.itemSchema) : shape.itemSchema;
53
+ const props = item.properties ?? {};
54
+ const idKey = input.idKey && input.idKey in props ? input.idKey : 'id' in props ? 'id' : undefined;
55
+ const items = [];
56
+ const required = new Set(item.required ?? []);
57
+ const projectable = !item.oneOf && !item.anyOf && !!item.properties;
58
+ const detailProps = (input.detailSchema?.allOf ? mergeAllOf(input.detailSchema) : input.detailSchema)?.properties ?? {};
59
+ for (let i = win.offset; i < Math.min(total, win.offset + win.limit); i++) {
60
+ const key = `${input.requestPath}#${i}`;
61
+ const id = idKey ? recordId(props[idKey], i, input.seed, key) : undefined;
62
+ const detail = id !== undefined ? input.detail?.(id) : undefined;
63
+ if (detail && !projectable) {
64
+ // Items that are a oneOf/anyOf can't be projected field by field: generate, then overlay.
65
+ const record = generate(shape.itemSchema, { seed: input.seed, key });
66
+ for (const prop of Object.keys(record))
67
+ if (prop in detail && fits(detail[prop], props[prop]))
68
+ record[prop] = detail[prop];
69
+ items.push(record);
70
+ continue;
71
+ }
72
+ if (!detail) {
73
+ const record = generate(shape.itemSchema, { seed: input.seed, key });
74
+ if (idKey && id !== undefined && fits(id, props[idKey]))
75
+ record[idKey] = id;
76
+ items.push(record);
77
+ continue;
78
+ }
79
+ // The detail record is the source of truth: project it onto the item's fields
80
+ // rather than generating the item too (twice the work, on the hottest path).
81
+ const record = {};
82
+ for (const [prop, propSchema] of Object.entries(props)) {
83
+ if (prop in detail && (detailProps[prop] === propSchema || fits(detail[prop], propSchema)))
84
+ record[prop] = detail[prop];
85
+ else if (required.has(prop))
86
+ record[prop] = generate(propSchema, { seed: input.seed, key: `${key}.${prop}` });
87
+ }
88
+ // Required keys without a property schema still have to be there.
89
+ for (const prop of required)
90
+ if (!(prop in record))
91
+ record[prop] = prop in detail ? detail[prop] : generate({}, { seed: input.seed, key: `${key}.${prop}` });
92
+ items.push(record);
93
+ }
94
+ const hasMore = win.offset + items.length < total;
95
+ const headers = { 'x-total-count': String(total) };
96
+ const links = navLinks(input, win, hasMore);
97
+ const link = [links.next && `<${links.next}>; rel="next"`, links.prev && `<${links.prev}>; rel="prev"`].filter(Boolean).join(', ');
98
+ if (link)
99
+ headers.link = link;
100
+ if (!shape.key)
101
+ return { body: items, headers };
102
+ const body = (input.body && typeof input.body === 'object' ? input.body : {});
103
+ body[shape.key] = items;
104
+ const schema = input.schema.allOf ? mergeAllOf(input.schema) : input.schema;
105
+ fillMeta(body, schema, { ...win, count: items.length, hasMore, links }, shape.key, 0);
106
+ return { body, headers };
107
+ }
108
+ /** Fill pagination fields (`total`, `page`, `next`, `meta.pagination.totalPages`, …) by name. */
109
+ function fillMeta(obj, schema, m, listKey, depth) {
110
+ const props = schema?.properties ?? {};
111
+ const totalPages = Math.max(1, Math.ceil(m.total / m.limit));
112
+ for (const [key, value] of Object.entries(obj)) {
113
+ if (key === listKey)
114
+ continue;
115
+ const s = props[key];
116
+ const n = normalizeName(key);
117
+ const set = (v) => {
118
+ if (fits(v, s))
119
+ obj[key] = v;
120
+ };
121
+ // Last page: nullable `next` becomes null, optional `next` disappears.
122
+ const end = () => {
123
+ if (s && (fits(null, s) || s.nullable))
124
+ obj[key] = null;
125
+ else if (!(schema?.required ?? []).includes(key))
126
+ delete obj[key];
127
+ };
128
+ if (/^(total|totalcount|totalitems|totalresults|totalrecords|totalelements|totalentries|count|totalsize)$/.test(n))
129
+ set(n === 'count' && 'total' in obj ? m.count : m.total); // `count` next to `total` is the page size
130
+ else if (/^(page|currentpage|pagenumber|pageindex)$/.test(n))
131
+ set(m.page);
132
+ else if (/^(perpage|pagesize|limit|size|itemsperpage|pagelength)$/.test(n))
133
+ set(m.limit);
134
+ else if (/^(totalpages|pagecount|pages|lastpage)$/.test(n))
135
+ set(totalPages);
136
+ else if (/^(offset|skip|start)$/.test(n))
137
+ set(m.offset);
138
+ else if (/^(hasmore|hasnext|hasnextpage|more)$/.test(n))
139
+ set(m.hasMore);
140
+ else if (/^(hasprev|hasprevious|haspreviouspage)$/.test(n))
141
+ set(m.offset > 0);
142
+ else if (/^(next|nextpage|nexturl|nextlink|nextcursor|endcursor|cursor)$/.test(n)) {
143
+ if (!m.hasMore)
144
+ end();
145
+ else if (typeof value === 'number' || (s && /integer|number/.test(String(typeOf(s)))))
146
+ set(m.page + 1);
147
+ else
148
+ set(/cursor/.test(n) ? m.links.nextCursor : m.links.next);
149
+ }
150
+ else if (/^(prev|previous|prevpage|previouspage|prevurl|prevlink|startcursor)$/.test(n)) {
151
+ if (m.offset === 0)
152
+ end();
153
+ else if (typeof value === 'number' || (s && /integer|number/.test(String(typeOf(s)))))
154
+ set(m.page - 1);
155
+ else
156
+ set(/cursor/.test(n) ? m.links.prevCursor : m.links.prev);
157
+ }
158
+ else if (depth < 2 && value && typeof value === 'object' && !Array.isArray(value)) {
159
+ fillMeta(value, s && (s.allOf ? mergeAllOf(s) : s), m, undefined, depth + 1);
160
+ }
161
+ }
162
+ }
163
+ /** Record i's id: sequential for numeric ids, otherwise generated from the id schema alone. */
164
+ function recordId(idSchema, i, seed, key) {
165
+ const t = typeOf(idSchema);
166
+ if (t === 'integer' || t === 'number') {
167
+ const id = Math.max(1, Math.ceil(idSchema.minimum ?? 1)) + i;
168
+ if (idSchema.maximum === undefined || id <= idSchema.maximum)
169
+ return id;
170
+ }
171
+ return generate(idSchema, { seed, key: `${key}.id` });
172
+ }
173
+ function navLinks(input, win, hasMore) {
174
+ const link = (offset) => {
175
+ const q = new URLSearchParams(input.query);
176
+ for (const p of CURSOR_PARAMS)
177
+ q.delete(p);
178
+ const usesOffset = OFFSET_PARAMS.some((p) => q.has(p));
179
+ if (usesOffset)
180
+ for (const p of OFFSET_PARAMS)
181
+ q.has(p) && q.set(p, String(offset));
182
+ else
183
+ q.set(PAGE_PARAMS.find((p) => q.has(p)) ?? 'page', String(Math.floor(offset / win.limit) + 1));
184
+ if (!LIMIT_PARAMS.some((p) => q.has(p)))
185
+ q.set('limit', String(win.limit));
186
+ return `${input.origin ?? ''}${input.requestPath}?${q}`;
187
+ };
188
+ const prevOffset = Math.max(0, win.offset - win.limit);
189
+ return {
190
+ next: hasMore ? link(win.offset + win.limit) : undefined,
191
+ prev: win.offset > 0 ? link(prevOffset) : undefined,
192
+ nextCursor: hasMore ? encodeCursor(win.offset + win.limit) : undefined,
193
+ prevCursor: win.offset > 0 ? encodeCursor(prevOffset) : undefined,
194
+ };
195
+ }
196
+ /** Opaque-looking cursors that are really offsets, so any cursor we hand out works. */
197
+ export function encodeCursor(offset) {
198
+ return Buffer.from(`o:${offset}`).toString('base64url');
199
+ }
200
+ function decodeCursor(cursor) {
201
+ const m = /^o:(\d+)$/.exec(Buffer.from(cursor, 'base64url').toString());
202
+ return m ? Number(m[1]) : undefined;
203
+ }
204
+ function firstInt(query, names) {
205
+ for (const name of names) {
206
+ const v = query.get(name);
207
+ if (v !== null && /^-?\d+$/.test(v))
208
+ return Number(v);
209
+ }
210
+ return undefined;
211
+ }
212
+ function clamp(n, min, max) {
213
+ return Math.min(max, Math.max(min, n));
214
+ }
215
+ //# sourceMappingURL=collections.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collections.js","sourceRoot":"","sources":["../src/collections.ts"],"names":[],"mappings":"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","sourcesContent":["import { generate } from './generate.js';\nimport { fits, mergeAllOf, typeOf } from './schema.js';\nimport { normalizeName } from './heuristics.js';\nimport type { MockRoute, Schema } from './model.js';\n\n// List endpoints are treated as windows onto a fixed, seeded collection:\n// record i always has the same data, `?page=2` shows records 10–19, and when a\n// detail route exists (`/users` + `/users/{id}`), each list item is overlaid with\n// the detail response for its id so the two always agree.\n\n/** Property names that hold the records in a wrapped list response. */\nconst LIST_KEYS = /^(data|items|results|records|content|rows|entries|list|values|nodes|hits|objects|elements)$/;\n/** Sibling names that mark an object as a page wrapper rather than a record with a nested array. */\nconst PAGE_HINT = /^(total|totalcount|count|page|pages|next|nextpage|cursor|nextcursor|meta|pagination|pageinfo|links|hasmore|hasnext|limit|offset)$/;\n\nexport interface ListShape {\n /** Property holding the records; undefined when the response is a bare array. */\n key?: string;\n arraySchema: Schema;\n itemSchema: Schema;\n}\n\n/** Recognize a list response: a bare array of objects, or an object wrapping one. */\nexport function findListShape(schema: Schema | undefined): ListShape | undefined {\n if (!schema) return undefined;\n if (typeOf(schema) === 'array') {\n return isObjectSchema(schema.items) ? { arraySchema: schema, itemSchema: schema.items } : undefined;\n }\n const props: Record<string, Schema> | undefined = (schema.allOf ? mergeAllOf(schema) : schema).properties;\n if (!props) return undefined;\n const arrays = Object.entries(props).filter(([, s]) => typeOf(s) === 'array' && isObjectSchema(s.items));\n const pick =\n arrays.find(([k]) => LIST_KEYS.test(normalizeName(k))) ??\n (arrays.length === 1 && Object.keys(props).some((k) => PAGE_HINT.test(normalizeName(k))) ? arrays[0] : undefined);\n return pick && { key: pick[0], arraySchema: pick[1], itemSchema: pick[1].items };\n}\n\nfunction isObjectSchema(s: Schema | undefined): s is Schema {\n return !!s && (typeOf(s) === 'object' || !!s.allOf);\n}\n\n/** The `GET /users/{id}` route for list route `GET /users`, if the spec has one. */\nexport function findDetailRoute(routes: MockRoute[], list: MockRoute): MockRoute | undefined {\n const prefix = `${list.path.replace(/\\/$/, '')}/{`;\n return routes.find(\n (r) => r.method === 'GET' && r.path.startsWith(prefix) && r.path.endsWith('}') && !r.path.slice(prefix.length).includes('/'),\n );\n}\n\nexport interface PageWindow {\n offset: number;\n limit: number;\n /** 1-based. */\n page: number;\n total: number;\n}\n\nconst LIMIT_PARAMS = ['limit', 'per_page', 'perPage', 'page_size', 'pageSize', 'size', 'first'];\nconst OFFSET_PARAMS = ['offset', 'skip', 'start'];\nconst PAGE_PARAMS = ['page', 'page_number', 'pageNumber'];\nconst CURSOR_PARAMS = ['cursor', 'after', 'starting_after', 'page_token', 'pageToken'];\n\n/** Work out which slice of the collection a request asks for. */\nexport function readPage(query: URLSearchParams, arraySchema: Schema, total: number, defaultLimit = 10): PageWindow {\n const min = Math.max(1, arraySchema.minItems ?? 1);\n const max = Math.min(1000, arraySchema.maxItems ?? 1000);\n const limit = clamp(firstInt(query, LIMIT_PARAMS) ?? defaultLimit, min, Math.max(min, max));\n const cursor = CURSOR_PARAMS.map((p) => query.get(p)).find(Boolean);\n const page = firstInt(query, PAGE_PARAMS);\n const offset = Math.max(0, (cursor ? decodeCursor(cursor) : undefined) ?? firstInt(query, OFFSET_PARAMS) ?? ((page ?? 1) - 1) * limit);\n return { offset, limit, page: page && page > 0 ? page : Math.floor(offset / limit) + 1, total };\n}\n\nexport interface BuildPageInput {\n /** The list response's schema. */\n schema: Schema;\n /** Concrete request path, used to seed records and build next/prev links. */\n requestPath: string;\n query: URLSearchParams;\n origin?: string;\n seed?: number;\n shape: ListShape;\n /** Generated response body; for wrapped lists, its metadata is filled in place. */\n body: unknown;\n total: number;\n /** Name of the id property that links items to the detail route. */\n idKey?: string;\n /** Detail response for an id, used to make list items agree with `GET /x/{id}`. */\n detail?: (id: unknown) => Record<string, unknown> | undefined;\n /** The detail route's schema; fields it shares with the item need no re-check. */\n detailSchema?: Schema;\n}\n\nexport function buildPage(input: BuildPageInput): { body: unknown; headers: Record<string, string> } {\n const { shape, query, total } = input;\n const win = readPage(query, shape.arraySchema, total);\n const item = shape.itemSchema.allOf ? mergeAllOf(shape.itemSchema) : shape.itemSchema;\n const props: Record<string, Schema> = item.properties ?? {};\n const idKey = input.idKey && input.idKey in props ? input.idKey : 'id' in props ? 'id' : undefined;\n\n const items: unknown[] = [];\n const required = new Set<string>(item.required ?? []);\n const projectable = !item.oneOf && !item.anyOf && !!item.properties;\n const detailProps: Record<string, Schema> = (input.detailSchema?.allOf ? mergeAllOf(input.detailSchema) : input.detailSchema)?.properties ?? {};\n for (let i = win.offset; i < Math.min(total, win.offset + win.limit); i++) {\n const key = `${input.requestPath}#${i}`;\n const id = idKey ? recordId(props[idKey]!, i, input.seed, key) : undefined;\n const detail = id !== undefined ? input.detail?.(id) : undefined;\n if (detail && !projectable) {\n // Items that are a oneOf/anyOf can't be projected field by field: generate, then overlay.\n const record = generate(shape.itemSchema, { seed: input.seed, key }) as Record<string, unknown>;\n for (const prop of Object.keys(record)) if (prop in detail && fits(detail[prop], props[prop])) record[prop] = detail[prop];\n items.push(record);\n continue;\n }\n if (!detail) {\n const record = generate(shape.itemSchema, { seed: input.seed, key }) as Record<string, unknown>;\n if (idKey && id !== undefined && fits(id, props[idKey])) record[idKey] = id;\n items.push(record);\n continue;\n }\n // The detail record is the source of truth: project it onto the item's fields\n // rather than generating the item too (twice the work, on the hottest path).\n const record: Record<string, unknown> = {};\n for (const [prop, propSchema] of Object.entries(props)) {\n if (prop in detail && (detailProps[prop] === propSchema || fits(detail[prop], propSchema))) record[prop] = detail[prop];\n else if (required.has(prop)) record[prop] = generate(propSchema, { seed: input.seed, key: `${key}.${prop}` });\n }\n // Required keys without a property schema still have to be there.\n for (const prop of required) if (!(prop in record)) record[prop] = prop in detail ? detail[prop] : generate({}, { seed: input.seed, key: `${key}.${prop}` });\n items.push(record);\n }\n\n const hasMore = win.offset + items.length < total;\n const headers: Record<string, string> = { 'x-total-count': String(total) };\n const links = navLinks(input, win, hasMore);\n const link = [links.next && `<${links.next}>; rel=\"next\"`, links.prev && `<${links.prev}>; rel=\"prev\"`].filter(Boolean).join(', ');\n if (link) headers.link = link;\n\n if (!shape.key) return { body: items, headers };\n const body = (input.body && typeof input.body === 'object' ? input.body : {}) as Record<string, unknown>;\n body[shape.key] = items;\n const schema = input.schema.allOf ? mergeAllOf(input.schema) : input.schema;\n fillMeta(body, schema, { ...win, count: items.length, hasMore, links }, shape.key, 0);\n return { body, headers };\n}\n\ninterface MetaInfo extends PageWindow {\n count: number;\n hasMore: boolean;\n links: { next?: string; prev?: string; nextCursor?: string; prevCursor?: string };\n}\n\n/** Fill pagination fields (`total`, `page`, `next`, `meta.pagination.totalPages`, …) by name. */\nfunction fillMeta(obj: Record<string, unknown>, schema: Schema | undefined, m: MetaInfo, listKey: string | undefined, depth: number) {\n const props: Record<string, Schema> = schema?.properties ?? {};\n const totalPages = Math.max(1, Math.ceil(m.total / m.limit));\n for (const [key, value] of Object.entries(obj)) {\n if (key === listKey) continue;\n const s = props[key];\n const n = normalizeName(key);\n const set = (v: unknown) => {\n if (fits(v, s)) obj[key] = v;\n };\n // Last page: nullable `next` becomes null, optional `next` disappears.\n const end = () => {\n if (s && (fits(null, s) || s.nullable)) obj[key] = null;\n else if (!(schema?.required ?? []).includes(key)) delete obj[key];\n };\n\n if (/^(total|totalcount|totalitems|totalresults|totalrecords|totalelements|totalentries|count|totalsize)$/.test(n)) set(n === 'count' && 'total' in obj ? m.count : m.total); // `count` next to `total` is the page size\n else if (/^(page|currentpage|pagenumber|pageindex)$/.test(n)) set(m.page);\n else if (/^(perpage|pagesize|limit|size|itemsperpage|pagelength)$/.test(n)) set(m.limit);\n else if (/^(totalpages|pagecount|pages|lastpage)$/.test(n)) set(totalPages);\n else if (/^(offset|skip|start)$/.test(n)) set(m.offset);\n else if (/^(hasmore|hasnext|hasnextpage|more)$/.test(n)) set(m.hasMore);\n else if (/^(hasprev|hasprevious|haspreviouspage)$/.test(n)) set(m.offset > 0);\n else if (/^(next|nextpage|nexturl|nextlink|nextcursor|endcursor|cursor)$/.test(n)) {\n if (!m.hasMore) end();\n else if (typeof value === 'number' || (s && /integer|number/.test(String(typeOf(s))))) set(m.page + 1);\n else set(/cursor/.test(n) ? m.links.nextCursor : m.links.next);\n } else if (/^(prev|previous|prevpage|previouspage|prevurl|prevlink|startcursor)$/.test(n)) {\n if (m.offset === 0) end();\n else if (typeof value === 'number' || (s && /integer|number/.test(String(typeOf(s))))) set(m.page - 1);\n else set(/cursor/.test(n) ? m.links.prevCursor : m.links.prev);\n } else if (depth < 2 && value && typeof value === 'object' && !Array.isArray(value)) {\n fillMeta(value as Record<string, unknown>, s && (s.allOf ? mergeAllOf(s) : s), m, undefined, depth + 1);\n }\n }\n}\n\n/** Record i's id: sequential for numeric ids, otherwise generated from the id schema alone. */\nfunction recordId(idSchema: Schema, i: number, seed: number | undefined, key: string): unknown {\n const t = typeOf(idSchema);\n if (t === 'integer' || t === 'number') {\n const id = Math.max(1, Math.ceil(idSchema.minimum ?? 1)) + i;\n if (idSchema.maximum === undefined || id <= idSchema.maximum) return id;\n }\n return generate(idSchema, { seed, key: `${key}.id` });\n}\n\nfunction navLinks(input: BuildPageInput, win: PageWindow, hasMore: boolean): MetaInfo['links'] {\n const link = (offset: number) => {\n const q = new URLSearchParams(input.query);\n for (const p of CURSOR_PARAMS) q.delete(p);\n const usesOffset = OFFSET_PARAMS.some((p) => q.has(p));\n if (usesOffset) for (const p of OFFSET_PARAMS) q.has(p) && q.set(p, String(offset));\n else q.set(PAGE_PARAMS.find((p) => q.has(p)) ?? 'page', String(Math.floor(offset / win.limit) + 1));\n if (!LIMIT_PARAMS.some((p) => q.has(p))) q.set('limit', String(win.limit));\n return `${input.origin ?? ''}${input.requestPath}?${q}`;\n };\n const prevOffset = Math.max(0, win.offset - win.limit);\n return {\n next: hasMore ? link(win.offset + win.limit) : undefined,\n prev: win.offset > 0 ? link(prevOffset) : undefined,\n nextCursor: hasMore ? encodeCursor(win.offset + win.limit) : undefined,\n prevCursor: win.offset > 0 ? encodeCursor(prevOffset) : undefined,\n };\n}\n\n/** Opaque-looking cursors that are really offsets, so any cursor we hand out works. */\nexport function encodeCursor(offset: number): string {\n return Buffer.from(`o:${offset}`).toString('base64url');\n}\n\nfunction decodeCursor(cursor: string): number | undefined {\n const m = /^o:(\\d+)$/.exec(Buffer.from(cursor, 'base64url').toString());\n return m ? Number(m[1]) : undefined;\n}\n\nfunction firstInt(query: URLSearchParams, names: string[]): number | undefined {\n for (const name of names) {\n const v = query.get(name);\n if (v !== null && /^-?\\d+$/.test(v)) return Number(v);\n }\n return undefined;\n}\n\nfunction clamp(n: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, n));\n}\n"]}
@@ -0,0 +1,13 @@
1
+ import type { Schema } from './model.js';
2
+ export interface GenerateOptions {
3
+ /** Global seed; the same seed + key always yields the same value. */
4
+ seed?: number;
5
+ /** Distinguishes values under one seed, e.g. `GET /users/42 200`. */
6
+ key?: string;
7
+ /** Recursion limit for circular schemas. */
8
+ maxDepth?: number;
9
+ }
10
+ /** Generate a realistic, deterministic value that satisfies `schema`. */
11
+ export declare function generate(schema: Schema | undefined, opts?: GenerateOptions): unknown;
12
+ /** 32-bit FNV-1a; stable across platforms and Node versions. */
13
+ export declare function hash(input: string): number;
@@ -0,0 +1,377 @@
1
+ import { faker as enFaker } from '@faker-js/faker/locale/en';
2
+ import RandExp from 'randexp';
3
+ import { countMatches, discriminatorOf, fits, mergeAllOf, mergeBranch, typeOf } from './schema.js';
4
+ import { looksLikeDate, looksLikeDateTime, numberByName, stringByName } from './heuristics.js';
5
+ // Faker's date helpers default to "now"; pin them so output never drifts.
6
+ const REF_DATE = new Date('2026-01-01T00:00:00.000Z');
7
+ // The package root loads every locale (~23 MB of RSS); the `locale/en` entry only
8
+ // exports a shared instance, so build a private one from its class and definitions.
9
+ const FakerClass = enFaker.constructor;
10
+ const faker = new FakerClass({ locale: enFaker.rawDefinitions });
11
+ faker.setDefaultRefDate(REF_DATE);
12
+ // Filling every optional field of every nested object explodes on big specs (Stripe
13
+ // objects nest dozens of expandable objects). Past this depth, or once this many
14
+ // values have been generated, only required fields and minItems are filled.
15
+ // 250 keeps a Stripe list page of 10 full records near 15 ms (the target is 20 ms
16
+ // p95) while every top-level field of a record is still present.
17
+ const RICH_DEPTH = 3;
18
+ const VALUE_BUDGET = 250;
19
+ /** Generate a realistic, deterministic value that satisfies `schema`. */
20
+ export function generate(schema, opts = {}) {
21
+ faker.seed(hash(`${opts.seed ?? 1}:${opts.key ?? ''}`));
22
+ return gen(schema ?? {}, { depth: 0, maxDepth: opts.maxDepth ?? 8, budget: { values: VALUE_BUDGET } });
23
+ }
24
+ function gen(schema, ctx) {
25
+ // Real examples beat fake data, unless the spec's example contradicts its own schema.
26
+ if (schema.example !== undefined && fits(schema.example, schema))
27
+ return structuredClone(schema.example);
28
+ const example = Array.isArray(schema.examples) ? schema.examples.find((e) => fits(e, schema)) : undefined;
29
+ if (example !== undefined)
30
+ return structuredClone(example);
31
+ if ('const' in schema)
32
+ return schema.const;
33
+ if (Array.isArray(schema.enum) && schema.enum.length) {
34
+ // Specs sometimes list enum values their own pattern or maxLength rejects.
35
+ const { enum: values, ...rest } = schema;
36
+ const valid = values.filter((v) => v !== null && fits(v, rest));
37
+ const options = valid.length ? valid : values.filter((v) => v !== null);
38
+ return faker.helpers.arrayElement(options.length ? options : values);
39
+ }
40
+ if (Array.isArray(schema.allOf))
41
+ return gen(mergeAllOf(schema), ctx);
42
+ const choices = schema.oneOf ?? schema.anyOf;
43
+ if (Array.isArray(choices) && choices.length)
44
+ return genChoice(schema, choices, Array.isArray(schema.oneOf), ctx);
45
+ switch (typeOf(schema)) {
46
+ case 'null':
47
+ return null;
48
+ case 'object':
49
+ return genObject(schema, ctx);
50
+ case 'array':
51
+ return genArray(schema, ctx);
52
+ case 'integer':
53
+ return genNumber(schema, ctx, true);
54
+ case 'number':
55
+ return genNumber(schema, ctx, false);
56
+ case 'boolean':
57
+ return faker.datatype.boolean();
58
+ case 'string':
59
+ return genString(schema, ctx);
60
+ default:
61
+ return faker.word.noun();
62
+ }
63
+ }
64
+ /**
65
+ * Optional fields real APIs only fill when the client asks (HAL's `_embedded` via
66
+ * `?expand=`). Generating them makes every record several times larger than the real
67
+ * thing. Required ones are still filled.
68
+ */
69
+ const ON_REQUEST = new Set(['_embedded']);
70
+ function lean(ctx) {
71
+ return ctx.minimal === true || ctx.depth > RICH_DEPTH || ctx.budget.values <= 0;
72
+ }
73
+ /**
74
+ * Pick a `oneOf`/`anyOf` branch and generate from it. Branches often overlap (two
75
+ * object shapes with no required fields both accept `{}`), so each candidate is
76
+ * checked: it must satisfy its branch, and for `oneOf` no other branch.
77
+ */
78
+ function genChoice(schema, choices, exclusive, ctx) {
79
+ const rest = choiceBase(schema);
80
+ const disc = discriminatorOf(schema);
81
+ if (disc)
82
+ return genDiscriminated(rest, disc, ctx);
83
+ // Deep in the tree, prefer the cheapest branch (`string` over an expanded object).
84
+ const order = lean(ctx) ? [...choices].sort((a, b) => cost(a) - cost(b)) : faker.helpers.shuffle([...choices]);
85
+ let fallback;
86
+ // A rejected candidate must not eat the budget of the next one, or later
87
+ // candidates come out bare and match every branch.
88
+ const budget = ctx.budget.values;
89
+ // Second pass: a full object often satisfies a loose sibling branch too, while
90
+ // the bare required fields of the same branch don't.
91
+ for (const attempt of [ctx, { ...ctx, minimal: true }]) {
92
+ for (const branch of order.slice(0, 4)) {
93
+ const s = Object.keys(rest).length ? mergeBranch(rest, branch) : branch;
94
+ ctx.budget.values = budget;
95
+ const value = gen(s, attempt);
96
+ if (fallback === undefined)
97
+ fallback = value;
98
+ if (!fits(value, s))
99
+ continue;
100
+ if (exclusive && choices.length > 1 && countMatches(value, choices, 0, 2) !== 1)
101
+ continue;
102
+ return value;
103
+ }
104
+ if (!exclusive)
105
+ break;
106
+ }
107
+ return fallback;
108
+ }
109
+ /** With a discriminator the branch is chosen by value, and the record carries that value. */
110
+ function genDiscriminated(rest, disc, ctx) {
111
+ const shared = Object.keys(rest).length > 0;
112
+ const branchOf = (m) => (shared ? mergeBranch(rest, m.schema) : m.schema);
113
+ // Skip mapping values the branch's own property schema rejects (a spec bug, but common).
114
+ const usable = disc.mapping.filter((m) => {
115
+ const s = branchOf(m);
116
+ const prop = (s.allOf ? mergeAllOf(s) : s).properties?.[disc.property];
117
+ return !prop || fits(m.value, prop);
118
+ });
119
+ const entry = faker.helpers.arrayElement(usable.length ? usable : disc.mapping);
120
+ const value = gen(branchOf(entry), ctx);
121
+ if (value && typeof value === 'object' && !Array.isArray(value))
122
+ value[disc.property] = entry.value;
123
+ return value;
124
+ }
125
+ // Per-schema derived data, cached by identity: schemas never change, records repeat.
126
+ const baseCache = new WeakMap();
127
+ const siblingCache = new WeakMap();
128
+ /** A oneOf/anyOf schema minus its alternatives: what every branch is merged with. */
129
+ function choiceBase(schema) {
130
+ let base = baseCache.get(schema);
131
+ if (!base) {
132
+ const { oneOf, anyOf, discriminator, ...rest } = schema;
133
+ baseCache.set(schema, (base = rest));
134
+ }
135
+ return base;
136
+ }
137
+ function siblingsOf(props) {
138
+ let set = siblingCache.get(props);
139
+ if (!set)
140
+ siblingCache.set(props, (set = new Set(Object.keys(props).map((k) => k.toLowerCase()))));
141
+ return set;
142
+ }
143
+ function cost(s) {
144
+ if (s.enum || 'const' in s)
145
+ return 0;
146
+ const t = typeOf(s);
147
+ if (t === 'object')
148
+ return 2 + (s.required?.length ?? 0);
149
+ if (t === 'array')
150
+ return 2 + (s.minItems ?? 0);
151
+ if (s.allOf || s.oneOf || s.anyOf)
152
+ return 3;
153
+ return t ? 1 : 3;
154
+ }
155
+ function genObject(schema, ctx) {
156
+ const out = {};
157
+ if (ctx.depth >= ctx.maxDepth)
158
+ return out;
159
+ const props = schema.properties ?? {};
160
+ const required = new Set(schema.required ?? []);
161
+ const record = { siblings: siblingsOf(props), owner: ctx.name };
162
+ for (const [name, propSchema] of Object.entries(props)) {
163
+ if (propSchema.writeOnly)
164
+ continue;
165
+ if (!required.has(name) && (lean(ctx) || ON_REQUEST.has(name)))
166
+ continue;
167
+ ctx.budget.values--;
168
+ out[name] = gen(propSchema, { ...ctx, depth: ctx.depth + 1, name, record });
169
+ }
170
+ // Required keys the schema never describes still have to be present.
171
+ const extra = typeof schema.additionalProperties === 'object' ? schema.additionalProperties : {};
172
+ for (const name of required) {
173
+ if (!(name in out) && !props[name]?.writeOnly)
174
+ out[name] = gen(extra, { ...ctx, depth: ctx.depth + 1, name, record });
175
+ }
176
+ if (!schema.properties && schema.additionalProperties && typeof schema.additionalProperties === 'object' && !lean(ctx)) {
177
+ const count = faker.number.int({ min: 1, max: 3 });
178
+ for (let i = 0; i < count; i++) {
179
+ out[faker.word.noun()] = gen(schema.additionalProperties, { ...ctx, depth: ctx.depth + 1, name: undefined });
180
+ }
181
+ }
182
+ return out;
183
+ }
184
+ function genArray(schema, ctx) {
185
+ const min = schema.minItems ?? 0;
186
+ if (ctx.depth >= ctx.maxDepth)
187
+ return [];
188
+ // Nested arrays multiply (a list of invoices of line items of taxes): keep inner ones short.
189
+ const max = Math.max(min, schema.maxItems ?? Math.max(min, ctx.depth > 1 ? 3 : 5));
190
+ const count = lean(ctx) ? min : faker.number.int({ min: Math.max(min, Math.min(2, max)), max });
191
+ const items = schema.items ?? {};
192
+ // Items of a `users` array are users, so keep the singular name as the hint.
193
+ const name = ctx.name?.replace(/ies$/, 'y').replace(/(ss|x|sh|ch|us)es$/, '$1').replace(/([^s])s$/, '$1');
194
+ const out = [];
195
+ const seen = new Set();
196
+ for (let attempts = 0; out.length < count && attempts < count * 5; attempts++) {
197
+ const value = gen(items, { ...ctx, depth: ctx.depth + 1, name, record: undefined });
198
+ if (schema.uniqueItems) {
199
+ const k = JSON.stringify(value);
200
+ if (seen.has(k))
201
+ continue;
202
+ seen.add(k);
203
+ }
204
+ out.push(value);
205
+ }
206
+ return out;
207
+ }
208
+ function genNumber(schema, ctx, integer) {
209
+ let lo = schema.minimum ?? -Infinity;
210
+ let hi = schema.maximum ?? Infinity;
211
+ // 3.0 uses booleans for exclusive bounds; 3.1 uses numbers.
212
+ const step = integer ? 1 : 0.01;
213
+ if (typeof schema.exclusiveMinimum === 'number')
214
+ lo = Math.max(lo, schema.exclusiveMinimum + step);
215
+ else if (schema.exclusiveMinimum === true)
216
+ lo += step;
217
+ if (typeof schema.exclusiveMaximum === 'number')
218
+ hi = Math.min(hi, schema.exclusiveMaximum - step);
219
+ else if (schema.exclusiveMaximum === true)
220
+ hi -= step;
221
+ const hinted = ctx.name ? numberByName(faker, ctx.name, integer || schema.format === 'int64' || schema.format === 'int32') : undefined;
222
+ let value;
223
+ if (hinted !== undefined && hinted >= lo && hinted <= hi) {
224
+ value = hinted;
225
+ }
226
+ else {
227
+ const min = Number.isFinite(lo) ? lo : Number.isFinite(hi) ? hi - 1000 : 0;
228
+ const max = Number.isFinite(hi) ? hi : min + 1000;
229
+ value = integer
230
+ ? faker.number.int({ min: Math.ceil(min), max: Math.floor(max) })
231
+ : faker.number.float({ min, max, fractionDigits: 2 });
232
+ }
233
+ if (schema.multipleOf) {
234
+ const m = schema.multipleOf;
235
+ let snapped = Math.round(value / m) * m;
236
+ if (snapped < lo)
237
+ snapped += m;
238
+ if (snapped > hi)
239
+ snapped -= m;
240
+ value = Number(snapped.toFixed(10));
241
+ }
242
+ if (integer)
243
+ value = Math.round(value);
244
+ return value;
245
+ }
246
+ function genString(schema, ctx) {
247
+ const rec = ctx.record ?? { siblings: new Set() };
248
+ // `avatarUrl: {format: uri}` should still be an avatar, and `email` should match `firstName`.
249
+ const named = ctx.name && NAMED_FORMATS.has(schema.format) ? namedFormat(schema.format, stringByName(faker, ctx.name, rec, schema)) : undefined;
250
+ let value = named ?? byFormat(schema.format) ?? byPattern(schema.pattern);
251
+ if (value === undefined && ctx.name) {
252
+ if (looksLikeDate(ctx.name))
253
+ value = isoDate(faker.date.birthdate());
254
+ else if (looksLikeDateTime(ctx.name))
255
+ value = faker.date.past({ years: 2 }).toISOString();
256
+ else
257
+ value = stringByName(faker, ctx.name, rec, schema);
258
+ }
259
+ value ??= faker.lorem.words({ min: 1, max: 3 });
260
+ if (schema.format || schema.pattern)
261
+ return value;
262
+ const min = schema.minLength ?? 0;
263
+ const max = schema.maxLength ?? Infinity;
264
+ while (value.length < min)
265
+ value += faker.string.alpha({ length: min - value.length, casing: 'lower' });
266
+ if (value.length > max)
267
+ value = value.slice(0, max);
268
+ return value;
269
+ }
270
+ /** Formats where a name-based value (an avatar URL, a matching email) beats a generic one. */
271
+ const NAMED_FORMATS = new Set(['email', 'idn-email', 'uri', 'url', 'iri']);
272
+ /** Keep a name-based value when it also satisfies the generic format. */
273
+ function namedFormat(format, candidate) {
274
+ if (candidate === undefined)
275
+ return undefined;
276
+ if (format === 'email' || format === 'idn-email')
277
+ return /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(candidate) ? candidate : undefined;
278
+ if (format === 'uri' || format === 'url' || format === 'iri')
279
+ return URL.canParse(candidate) ? candidate : undefined;
280
+ return undefined;
281
+ }
282
+ function byFormat(format) {
283
+ switch (format) {
284
+ case 'email':
285
+ case 'idn-email':
286
+ return faker.internet.email().toLowerCase();
287
+ case 'uri':
288
+ case 'url':
289
+ case 'iri':
290
+ return faker.internet.url();
291
+ case 'uri-reference':
292
+ return `/${faker.lorem.slug(2)}`;
293
+ case 'uuid':
294
+ return faker.string.uuid();
295
+ case 'date':
296
+ return isoDate(faker.date.past({ years: 2 }));
297
+ case 'date-time':
298
+ return faker.date.past({ years: 2 }).toISOString();
299
+ case 'time':
300
+ return `${faker.date.past().toISOString().slice(11, 19)}Z`;
301
+ case 'duration':
302
+ return `PT${faker.number.int({ min: 1, max: 59 })}M`;
303
+ case 'ipv4':
304
+ return faker.internet.ipv4();
305
+ case 'ipv6':
306
+ return faker.internet.ipv6();
307
+ case 'hostname':
308
+ case 'idn-hostname':
309
+ return faker.internet.domainName();
310
+ case 'byte':
311
+ return Buffer.from(faker.lorem.words(3)).toString('base64');
312
+ case 'binary':
313
+ return faker.string.alphanumeric(16);
314
+ case 'password':
315
+ return faker.internet.password({ length: 16 });
316
+ case 'json-pointer':
317
+ return `/${faker.word.noun()}/${faker.number.int(9)}`;
318
+ case 'relative-json-pointer':
319
+ return `0/${faker.word.noun()}`;
320
+ case 'uri-template':
321
+ return `https://${faker.internet.domainName()}/items/{id}`;
322
+ case 'iri-reference':
323
+ return `/${faker.lorem.slug(2)}`;
324
+ case 'regex':
325
+ return '^[a-z0-9-]+$';
326
+ default:
327
+ return undefined;
328
+ }
329
+ }
330
+ function byPattern(pattern) {
331
+ if (!pattern)
332
+ return undefined;
333
+ const compiled = patternGenerator(pattern);
334
+ if (!compiled)
335
+ return undefined;
336
+ for (let i = 0; i < 10; i++) {
337
+ const candidate = compiled.gen.gen();
338
+ if (compiled.re.test(candidate))
339
+ return candidate;
340
+ }
341
+ return undefined;
342
+ }
343
+ // Compiling a RandExp per value dominated pattern-heavy specs; compile once per pattern.
344
+ // Its randomness comes from the shared, reseeded faker, so reuse stays deterministic.
345
+ const patternCache = new Map();
346
+ function patternGenerator(pattern) {
347
+ let compiled = patternCache.get(pattern);
348
+ if (compiled === undefined) {
349
+ try {
350
+ const gen = new RandExp(pattern);
351
+ gen.max = 5; // Cap `*` and `+` so values stay short.
352
+ gen.randInt = (min, max) => faker.number.int({ min, max });
353
+ // Keep generated characters printable ASCII unless the pattern says otherwise.
354
+ gen.defaultRange.subtract(0, 31);
355
+ gen.defaultRange.subtract(127, 65535);
356
+ compiled = { re: new RegExp(pattern, 'u'), gen };
357
+ }
358
+ catch {
359
+ compiled = null; // Unsupported regex features fall through to a plain string.
360
+ }
361
+ patternCache.set(pattern, compiled);
362
+ }
363
+ return compiled;
364
+ }
365
+ function isoDate(d) {
366
+ return d.toISOString().slice(0, 10);
367
+ }
368
+ /** 32-bit FNV-1a; stable across platforms and Node versions. */
369
+ export function hash(input) {
370
+ let h = 0x811c9dc5;
371
+ for (let i = 0; i < input.length; i++) {
372
+ h ^= input.charCodeAt(i);
373
+ h = Math.imul(h, 0x01000193);
374
+ }
375
+ return h >>> 0;
376
+ }
377
+ //# sourceMappingURL=generate.js.map