@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,17 @@
1
+ import type { MockModel, MockRoute } from './model.js';
2
+ export declare const METHODS: readonly ['get', 'put', 'post', 'delete', 'patch', 'options', 'head', 'trace'];
3
+ export declare class SpecLoadError extends Error {
4
+ /** Absolute path of the spec; the message shows paths relative to the working directory. */
5
+ readonly file: string;
6
+ constructor(message: string,
7
+ /** Absolute path of the spec; the message shows paths relative to the working directory. */
8
+ file: string);
9
+ }
10
+ /** Load an OpenAPI 3.0/3.1 file (YAML or JSON, multi-file `$ref`s allowed) into the normalized model. */
11
+ export declare function loadOpenApi(file: string): Promise<MockModel>;
12
+ /** Literal segments win over params: `/users/me` must match before `/users/{id}`. */
13
+ export declare function sortRoutes(routes: MockRoute[]): MockRoute[];
14
+ export declare function compilePath(template: string): {
15
+ pattern: RegExp;
16
+ paramNames: string[];
17
+ };
@@ -0,0 +1,209 @@
1
+ import path from 'node:path';
2
+ import $RefParser from '@apidevtools/json-schema-ref-parser';
3
+ import { DISCRIMINATOR, fits, mergeAllOf } from './schema.js';
4
+ export const METHODS = ['get', 'put', 'post', 'delete', 'patch', 'options', 'head', 'trace'];
5
+ export class SpecLoadError extends Error {
6
+ file;
7
+ constructor(message,
8
+ /** Absolute path of the spec; the message shows paths relative to the working directory. */
9
+ file) {
10
+ super(relativePaths(`${file}: ${message.replace(/, open '[^']*'$/, '')}`));
11
+ this.file = file;
12
+ this.name = 'SpecLoadError';
13
+ }
14
+ }
15
+ /**
16
+ * Shorten absolute paths under the working directory, in any spelling the ref parser
17
+ * uses: `C:\a\b`, `c:/a/b` (Windows) or `/a/b`. Paths elsewhere are left as they are.
18
+ */
19
+ function relativePaths(text, cwd = process.cwd()) {
20
+ const segments = cwd.split(/[\\/]/).filter(Boolean).map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
21
+ const prefix = (cwd.startsWith('/') ? '[\\\\/]' : '') + segments.join('[\\\\/]') + '[\\\\/]';
22
+ return text.replace(new RegExp(prefix, process.platform === 'win32' ? 'gi' : 'g'), '');
23
+ }
24
+ /** Load an OpenAPI 3.0/3.1 file (YAML or JSON, multi-file `$ref`s allowed) into the normalized model. */
25
+ export async function loadOpenApi(file) {
26
+ const abs = path.resolve(file);
27
+ const parser = new $RefParser();
28
+ let doc;
29
+ try {
30
+ // Circular refs are kept as object cycles; the generator caps recursion depth.
31
+ doc = await parser.dereference(abs, { dereference: { circular: true } });
32
+ }
33
+ catch (err) {
34
+ throw new SpecLoadError(err.message, abs);
35
+ }
36
+ if (doc?.swagger) {
37
+ throw new SpecLoadError(`Swagger ${doc.swagger} is not supported; convert it to OpenAPI 3 first (e.g. with swagger2openapi).`, abs);
38
+ }
39
+ if (typeof doc?.openapi !== 'string' || !doc.openapi.startsWith('3.')) {
40
+ throw new SpecLoadError('not an OpenAPI 3.x document (missing or unsupported `openapi` field).', abs);
41
+ }
42
+ const warnings = [];
43
+ const routes = [];
44
+ resolveDiscriminators(doc, (ref) => {
45
+ try {
46
+ return parser.$refs.get(ref);
47
+ }
48
+ catch {
49
+ return undefined;
50
+ }
51
+ });
52
+ for (const [routePath, pathItem] of Object.entries(doc.paths ?? {})) {
53
+ if (!pathItem || typeof pathItem !== 'object')
54
+ continue;
55
+ for (const method of METHODS) {
56
+ const op = pathItem[method];
57
+ if (!op)
58
+ continue;
59
+ const responses = collectResponses(op.responses ?? {}, `${method.toUpperCase()} ${routePath}`, warnings);
60
+ routes.push({
61
+ method: method.toUpperCase(),
62
+ path: routePath,
63
+ ...compilePath(routePath),
64
+ operationId: op.operationId,
65
+ responses,
66
+ });
67
+ }
68
+ }
69
+ sortRoutes(routes);
70
+ return {
71
+ openapi: doc.openapi,
72
+ title: doc.info?.title ?? path.basename(abs),
73
+ version: doc.info?.version ?? '',
74
+ basePath: basePathOf(doc.servers?.[0]?.url),
75
+ routes,
76
+ warnings,
77
+ sources: parser.$refs.paths().filter((p) => !/^https?:/.test(p)),
78
+ };
79
+ }
80
+ /**
81
+ * Attach each `discriminator`'s value → branch mapping to its schema. Dereferencing
82
+ * turned the mapping's `$ref` strings into nothing we can follow, so they're resolved
83
+ * here, by identity. Branches without an explicit mapping use their component name,
84
+ * as the spec says. Only discriminators that cover every branch are attached.
85
+ */
86
+ function resolveDiscriminators(doc, resolve) {
87
+ const names = new Map();
88
+ for (const [name, s] of Object.entries(doc.components?.schemas ?? {}))
89
+ if (s && typeof s === 'object')
90
+ names.set(s, name);
91
+ const lookup = (ref) => (/[#/]/.test(ref) ? resolve(ref) : doc.components?.schemas?.[ref]);
92
+ const seen = new Set();
93
+ const visit = (node) => {
94
+ if (!node || typeof node !== 'object' || seen.has(node))
95
+ return;
96
+ seen.add(node);
97
+ if (Array.isArray(node))
98
+ return node.forEach(visit);
99
+ const s = node;
100
+ const branches = s.oneOf ?? s.anyOf;
101
+ const property = s.discriminator?.propertyName;
102
+ if (typeof property === 'string' && Array.isArray(branches)) {
103
+ const mapping = [];
104
+ for (const [value, ref] of Object.entries(s.discriminator.mapping ?? {})) {
105
+ const target = lookup(ref);
106
+ if (target && branches.includes(target))
107
+ mapping.push({ value, schema: target });
108
+ }
109
+ for (const b of branches) {
110
+ if (mapping.some((m) => m.schema === b))
111
+ continue;
112
+ // Implicit mapping is the component name, but most specs instead pin the
113
+ // property to one value (`type: {enum: [track]}`); prefer whichever fits.
114
+ const prop = (b.allOf ? mergeAllOf(b) : b).properties?.[property];
115
+ const pinned = prop && ('const' in prop ? [prop.const] : Array.isArray(prop.enum) && prop.enum.length === 1 ? prop.enum : undefined);
116
+ const name = names.get(b);
117
+ const value = pinned?.[0] ?? (name && (!prop || fits(name, prop)) ? name : undefined);
118
+ if (typeof value === 'string')
119
+ mapping.push({ value, schema: b });
120
+ }
121
+ if (branches.every((b) => mapping.some((m) => m.schema === b)))
122
+ s[DISCRIMINATOR] = { property, mapping };
123
+ }
124
+ for (const v of Object.values(s))
125
+ visit(v);
126
+ };
127
+ visit(doc);
128
+ }
129
+ function collectResponses(responses, label, warnings) {
130
+ const out = [];
131
+ for (const [code, res] of Object.entries(responses)) {
132
+ let status;
133
+ let isDefault = false;
134
+ if (code === 'default') {
135
+ status = 200;
136
+ isDefault = true;
137
+ }
138
+ else if (/^[1-5]XX$/i.test(code)) {
139
+ status = Number(code[0]) * 100;
140
+ }
141
+ else if (/^\d{3}$/.test(code)) {
142
+ status = Number(code);
143
+ }
144
+ else {
145
+ warnings.push(`${label}: ignoring unknown response code "${code}"`);
146
+ continue;
147
+ }
148
+ const [contentType, media] = pickMedia(res?.content);
149
+ const example = media ? firstExample(media) : undefined;
150
+ out.push({
151
+ status,
152
+ isDefault,
153
+ contentType,
154
+ schema: media?.schema,
155
+ example: example?.value,
156
+ hasExample: example !== undefined,
157
+ });
158
+ }
159
+ return out;
160
+ }
161
+ function pickMedia(content) {
162
+ if (!content)
163
+ return [undefined, undefined];
164
+ const types = Object.keys(content);
165
+ const json = types.find((t) => t === 'application/json') ??
166
+ types.find((t) => /[/+]json\b/.test(t)) ??
167
+ types.find((t) => t === '*/*') ??
168
+ types[0];
169
+ return json ? [json, content[json]] : [undefined, undefined];
170
+ }
171
+ function firstExample(media) {
172
+ if (media.example !== undefined)
173
+ return { value: media.example };
174
+ const first = media.examples && Object.values(media.examples)[0];
175
+ if (first && 'value' in first)
176
+ return { value: first.value };
177
+ return undefined;
178
+ }
179
+ /** Literal segments win over params: `/users/me` must match before `/users/{id}`. */
180
+ export function sortRoutes(routes) {
181
+ return routes.sort((a, b) => a.paramNames.length - b.paramNames.length);
182
+ }
183
+ export function compilePath(template) {
184
+ const paramNames = [];
185
+ const source = template
186
+ .split('/')
187
+ .map((seg) => seg.replace(/\{([^}]+)\}|[^{]+/g, (m, name) => {
188
+ if (name) {
189
+ paramNames.push(name);
190
+ return '([^/]+)';
191
+ }
192
+ return m.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
193
+ }))
194
+ .join('/');
195
+ return { pattern: new RegExp(`^${source}/?$`), paramNames };
196
+ }
197
+ function basePathOf(url) {
198
+ if (typeof url !== 'string')
199
+ return '';
200
+ try {
201
+ const p = new URL(url, 'http://localhost').pathname.replace(/\/+$/, '');
202
+ // Server variables like `/{version}` can't be matched literally; ignore them.
203
+ return p.includes('{') ? '' : p;
204
+ }
205
+ catch {
206
+ return '';
207
+ }
208
+ }
209
+ //# sourceMappingURL=openapi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi.js","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"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","sourcesContent":["import path from 'node:path';\nimport $RefParser from '@apidevtools/json-schema-ref-parser';\nimport type { MockModel, MockResponse, MockRoute, Schema } from './model.js';\nimport { DISCRIMINATOR, type Discriminator, fits, mergeAllOf } from './schema.js';\n\nexport const METHODS = ['get', 'put', 'post', 'delete', 'patch', 'options', 'head', 'trace'] as const;\n\nexport class SpecLoadError extends Error {\n constructor(\n message: string,\n /** Absolute path of the spec; the message shows paths relative to the working directory. */\n readonly file: string,\n ) {\n super(relativePaths(`${file}: ${message.replace(/, open '[^']*'$/, '')}`));\n this.name = 'SpecLoadError';\n }\n}\n\n/**\n * Shorten absolute paths under the working directory, in any spelling the ref parser\n * uses: `C:\\a\\b`, `c:/a/b` (Windows) or `/a/b`. Paths elsewhere are left as they are.\n */\nfunction relativePaths(text: string, cwd = process.cwd()): string {\n const segments = cwd.split(/[\\\\/]/).filter(Boolean).map((s) => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'));\n const prefix = (cwd.startsWith('/') ? '[\\\\\\\\/]' : '') + segments.join('[\\\\\\\\/]') + '[\\\\\\\\/]';\n return text.replace(new RegExp(prefix, process.platform === 'win32' ? 'gi' : 'g'), '');\n}\n\n/** Load an OpenAPI 3.0/3.1 file (YAML or JSON, multi-file `$ref`s allowed) into the normalized model. */\nexport async function loadOpenApi(file: string): Promise<MockModel> {\n const abs = path.resolve(file);\n const parser = new $RefParser();\n let doc: any;\n try {\n // Circular refs are kept as object cycles; the generator caps recursion depth.\n doc = await parser.dereference(abs, { dereference: { circular: true } });\n } catch (err) {\n throw new SpecLoadError((err as Error).message, abs);\n }\n\n if (doc?.swagger) {\n throw new SpecLoadError(\n `Swagger ${doc.swagger} is not supported; convert it to OpenAPI 3 first (e.g. with swagger2openapi).`,\n abs,\n );\n }\n if (typeof doc?.openapi !== 'string' || !doc.openapi.startsWith('3.')) {\n throw new SpecLoadError('not an OpenAPI 3.x document (missing or unsupported `openapi` field).', abs);\n }\n\n const warnings: string[] = [];\n const routes: MockRoute[] = [];\n resolveDiscriminators(doc, (ref) => {\n try {\n return parser.$refs.get(ref) as Schema;\n } catch {\n return undefined;\n }\n });\n\n for (const [routePath, pathItem] of Object.entries<any>(doc.paths ?? {})) {\n if (!pathItem || typeof pathItem !== 'object') continue;\n for (const method of METHODS) {\n const op = pathItem[method];\n if (!op) continue;\n const responses = collectResponses(op.responses ?? {}, `${method.toUpperCase()} ${routePath}`, warnings);\n routes.push({\n method: method.toUpperCase(),\n path: routePath,\n ...compilePath(routePath),\n operationId: op.operationId,\n responses,\n });\n }\n }\n\n sortRoutes(routes);\n\n return {\n openapi: doc.openapi,\n title: doc.info?.title ?? path.basename(abs),\n version: doc.info?.version ?? '',\n basePath: basePathOf(doc.servers?.[0]?.url),\n routes,\n warnings,\n sources: parser.$refs.paths().filter((p) => !/^https?:/.test(p)),\n };\n}\n\n/**\n * Attach each `discriminator`'s value → branch mapping to its schema. Dereferencing\n * turned the mapping's `$ref` strings into nothing we can follow, so they're resolved\n * here, by identity. Branches without an explicit mapping use their component name,\n * as the spec says. Only discriminators that cover every branch are attached.\n */\nfunction resolveDiscriminators(doc: any, resolve: (ref: string) => Schema | undefined) {\n const names = new Map<object, string>();\n for (const [name, s] of Object.entries<Schema>(doc.components?.schemas ?? {})) if (s && typeof s === 'object') names.set(s, name);\n const lookup = (ref: string) => (/[#/]/.test(ref) ? resolve(ref) : doc.components?.schemas?.[ref]);\n\n const seen = new Set<object>();\n const visit = (node: unknown) => {\n if (!node || typeof node !== 'object' || seen.has(node)) return;\n seen.add(node);\n if (Array.isArray(node)) return node.forEach(visit);\n const s = node as Schema;\n const branches: Schema[] | undefined = s.oneOf ?? s.anyOf;\n const property = s.discriminator?.propertyName;\n if (typeof property === 'string' && Array.isArray(branches)) {\n const mapping: Discriminator['mapping'] = [];\n for (const [value, ref] of Object.entries<string>(s.discriminator.mapping ?? {})) {\n const target = lookup(ref);\n if (target && branches.includes(target)) mapping.push({ value, schema: target });\n }\n for (const b of branches) {\n if (mapping.some((m) => m.schema === b)) continue;\n // Implicit mapping is the component name, but most specs instead pin the\n // property to one value (`type: {enum: [track]}`); prefer whichever fits.\n const prop = (b.allOf ? mergeAllOf(b) : b).properties?.[property];\n const pinned = prop && ('const' in prop ? [prop.const] : Array.isArray(prop.enum) && prop.enum.length === 1 ? prop.enum : undefined);\n const name = names.get(b);\n const value = pinned?.[0] ?? (name && (!prop || fits(name, prop)) ? name : undefined);\n if (typeof value === 'string') mapping.push({ value, schema: b });\n }\n if (branches.every((b) => mapping.some((m) => m.schema === b))) (s as any)[DISCRIMINATOR] = { property, mapping } satisfies Discriminator;\n }\n for (const v of Object.values(s)) visit(v);\n };\n visit(doc);\n}\n\nfunction collectResponses(responses: Record<string, any>, label: string, warnings: string[]): MockResponse[] {\n const out: MockResponse[] = [];\n for (const [code, res] of Object.entries(responses)) {\n let status: number;\n let isDefault = false;\n if (code === 'default') {\n status = 200;\n isDefault = true;\n } else if (/^[1-5]XX$/i.test(code)) {\n status = Number(code[0]) * 100;\n } else if (/^\\d{3}$/.test(code)) {\n status = Number(code);\n } else {\n warnings.push(`${label}: ignoring unknown response code \"${code}\"`);\n continue;\n }\n\n const [contentType, media] = pickMedia(res?.content);\n const example = media ? firstExample(media) : undefined;\n out.push({\n status,\n isDefault,\n contentType,\n schema: media?.schema as Schema | undefined,\n example: example?.value,\n hasExample: example !== undefined,\n });\n }\n return out;\n}\n\nfunction pickMedia(content: Record<string, any> | undefined): [string | undefined, any] {\n if (!content) return [undefined, undefined];\n const types = Object.keys(content);\n const json =\n types.find((t) => t === 'application/json') ??\n types.find((t) => /[/+]json\\b/.test(t)) ??\n types.find((t) => t === '*/*') ??\n types[0];\n return json ? [json, content[json]] : [undefined, undefined];\n}\n\nfunction firstExample(media: any): { value: unknown } | undefined {\n if (media.example !== undefined) return { value: media.example };\n const first = media.examples && Object.values<any>(media.examples)[0];\n if (first && 'value' in first) return { value: first.value };\n return undefined;\n}\n\n/** Literal segments win over params: `/users/me` must match before `/users/{id}`. */\nexport function sortRoutes(routes: MockRoute[]): MockRoute[] {\n return routes.sort((a, b) => a.paramNames.length - b.paramNames.length);\n}\n\nexport function compilePath(template: string): { pattern: RegExp; paramNames: string[] } {\n const paramNames: string[] = [];\n const source = template\n .split('/')\n .map((seg) =>\n seg.replace(/\\{([^}]+)\\}|[^{]+/g, (m, name?: string) => {\n if (name) {\n paramNames.push(name);\n return '([^/]+)';\n }\n return m.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n }),\n )\n .join('/');\n return { pattern: new RegExp(`^${source}/?$`), paramNames };\n}\n\nfunction basePathOf(url: unknown): string {\n if (typeof url !== 'string') return '';\n try {\n const p = new URL(url, 'http://localhost').pathname.replace(/\\/+$/, '');\n // Server variables like `/{version}` can't be matched literally; ignore them.\n return p.includes('{') ? '' : p;\n } catch {\n return '';\n }\n}\n"]}
@@ -0,0 +1,14 @@
1
+ import type { MockModel } from './model.js';
2
+ export interface ParsedSampleName {
3
+ method: string;
4
+ path: string;
5
+ status?: number;
6
+ }
7
+ /** Parse a sample file's path (relative to the sample folder) into a route; undefined if it isn't one. */
8
+ export declare function parseSampleName(relative: string): ParsedSampleName | undefined;
9
+ /** Load a folder of sample JSON responses into the normalized model. */
10
+ export declare function loadSamples(dir: string): Promise<MockModel>;
11
+ /** Serialize any model as an OpenAPI 3.1 document, so sample users can graduate to a spec. */
12
+ export declare function toOpenApi(model: MockModel): Record<string, unknown>;
13
+ /** `toOpenApi` rendered as YAML or JSON, picked by the output file's extension. */
14
+ export declare function formatOpenApi(model: MockModel, outFile?: string): string;
@@ -0,0 +1,115 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { stringify } from 'yaml';
4
+ import { SchemaInferrer } from './infer.js';
5
+ import { compilePath, METHODS, sortRoutes } from './openapi.js';
6
+ /** Parse a sample file's path (relative to the sample folder) into a route; undefined if it isn't one. */
7
+ export function parseSampleName(relative) {
8
+ const parts = relative.split(/[\\/]/).filter(Boolean);
9
+ const file = parts.pop();
10
+ if (!file?.toLowerCase().endsWith('.json'))
11
+ return undefined;
12
+ const tokens = file.slice(0, -'.json'.length).replace(/~[^.]*$/, '').split('.');
13
+ const method = tokens.shift().toLowerCase();
14
+ if (!METHODS.includes(method))
15
+ return undefined;
16
+ let status;
17
+ if (tokens.length && /^[1-5]\d\d$/.test(tokens.at(-1)))
18
+ status = Number(tokens.pop());
19
+ const segments = [...parts, ...tokens].map((seg) => seg.replace(/^\[([^\]]+)\]$/, '{$1}'));
20
+ if (segments.some((s) => s === ''))
21
+ return undefined;
22
+ return { method: method.toUpperCase(), path: `/${segments.join('/')}`, status };
23
+ }
24
+ /** Load a folder of sample JSON responses into the normalized model. */
25
+ export async function loadSamples(dir) {
26
+ const root = path.resolve(dir);
27
+ const warnings = [];
28
+ // route key → status → { inferrer, empty }
29
+ const groups = new Map();
30
+ for (const file of await listJsonFiles(root)) {
31
+ const rel = path.relative(root, file);
32
+ const parsed = parseSampleName(rel);
33
+ if (!parsed) {
34
+ warnings.push(`${rel}: ignored; name it like GET.users.json or users/GET.[id].json`);
35
+ continue;
36
+ }
37
+ const text = (await fs.readFile(file, 'utf8')).trim();
38
+ let body;
39
+ if (text) {
40
+ try {
41
+ body = JSON.parse(text);
42
+ }
43
+ catch (err) {
44
+ warnings.push(`${rel}: invalid JSON, skipped (${err.message})`);
45
+ continue;
46
+ }
47
+ }
48
+ const status = parsed.status ?? (!text ? 204 : parsed.method === 'POST' ? 201 : 200);
49
+ const key = `${parsed.method} ${parsed.path}`;
50
+ let group = groups.get(key);
51
+ if (!group)
52
+ groups.set(key, (group = { method: parsed.method, path: parsed.path, statuses: new Map() }));
53
+ let entry = group.statuses.get(status);
54
+ if (!entry)
55
+ group.statuses.set(status, (entry = { inferrer: new SchemaInferrer(), empty: true }));
56
+ if (text) {
57
+ entry.inferrer.add(body);
58
+ entry.empty = false;
59
+ }
60
+ }
61
+ const routes = [];
62
+ for (const group of groups.values()) {
63
+ const responses = [...group.statuses]
64
+ .sort(([a], [b]) => a - b)
65
+ .map(([status, { inferrer, empty }]) => ({
66
+ status,
67
+ hasExample: false,
68
+ ...(empty ? {} : { schema: inferrer.schema(), contentType: 'application/json' }),
69
+ }));
70
+ routes.push({ method: group.method, path: group.path, ...compilePath(group.path), responses });
71
+ }
72
+ if (!routes.length)
73
+ warnings.push(`${root}: no sample files found`);
74
+ return { openapi: '3.1.0', title: path.basename(root), version: '', basePath: '', routes: sortRoutes(routes), warnings, sources: [root] };
75
+ }
76
+ async function listJsonFiles(dir) {
77
+ const entries = await fs.readdir(dir, { withFileTypes: true, recursive: true });
78
+ return entries
79
+ .filter((e) => e.isFile() && e.name.toLowerCase().endsWith('.json'))
80
+ .map((e) => path.join(e.parentPath, e.name))
81
+ // Skip dot-folders and node_modules inside the sample folder, not above it:
82
+ // `.mocks/` is a common place to keep samples.
83
+ .filter((f) => !path.relative(dir, f).split(path.sep).some((seg) => seg === 'node_modules' || seg.startsWith('.')))
84
+ .sort();
85
+ }
86
+ /** Serialize any model as an OpenAPI 3.1 document, so sample users can graduate to a spec. */
87
+ export function toOpenApi(model) {
88
+ const paths = {};
89
+ // Plain code-unit order: localeCompare depends on the machine's ICU data.
90
+ for (const route of [...model.routes].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))) {
91
+ const item = (paths[route.path] ??= {});
92
+ if (route.paramNames.length && !item.parameters) {
93
+ item.parameters = route.paramNames.map((name) => ({ name, in: 'path', required: true, schema: { type: 'string' } }));
94
+ }
95
+ const responses = {};
96
+ for (const res of route.responses) {
97
+ responses[res.isDefault ? 'default' : String(res.status)] = {
98
+ description: 'Inferred from samples',
99
+ ...(res.schema ? { content: { [res.contentType ?? 'application/json']: { schema: res.schema } } } : {}),
100
+ };
101
+ }
102
+ item[route.method.toLowerCase()] = { responses };
103
+ }
104
+ return {
105
+ openapi: '3.1.0',
106
+ info: { title: model.title, version: model.version || '0.0.0' },
107
+ paths,
108
+ };
109
+ }
110
+ /** `toOpenApi` rendered as YAML or JSON, picked by the output file's extension. */
111
+ export function formatOpenApi(model, outFile) {
112
+ const doc = toOpenApi(model);
113
+ return outFile?.toLowerCase().endsWith('.json') ? `${JSON.stringify(doc, null, 2)}\n` : stringify(doc, { aliasDuplicateObjects: false });
114
+ }
115
+ //# sourceMappingURL=samples.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"samples.js","sourceRoot":"","sources":["../src/samples.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport { stringify } from 'yaml';\nimport { SchemaInferrer } from './infer.js';\nimport type { MockModel, MockResponse, MockRoute } from './model.js';\nimport { compilePath, METHODS, sortRoutes } from './openapi.js';\n\n// A sample folder maps file names to routes:\n//\n// GET.users.json → GET /users\n// GET.users.[id].json → GET /users/{id} ({id} works too)\n// users/GET.[id].json → GET /users/{id} (folders are path segments)\n// GET.users.[id].404.json → the 404 response of GET /users/{id}\n// GET.users.[id]~alice.json → another sample for the same response\n// DELETE.users.[id].json → empty file = no body (204)\n//\n// All samples for one route + status are merged into one inferred schema.\n\nexport interface ParsedSampleName {\n method: string;\n path: string;\n status?: number;\n}\n\n/** Parse a sample file's path (relative to the sample folder) into a route; undefined if it isn't one. */\nexport function parseSampleName(relative: string): ParsedSampleName | undefined {\n const parts = relative.split(/[\\\\/]/).filter(Boolean);\n const file = parts.pop();\n if (!file?.toLowerCase().endsWith('.json')) return undefined;\n\n const tokens = file.slice(0, -'.json'.length).replace(/~[^.]*$/, '').split('.');\n const method = tokens.shift()!.toLowerCase();\n if (!(METHODS as readonly string[]).includes(method)) return undefined;\n\n let status: number | undefined;\n if (tokens.length && /^[1-5]\\d\\d$/.test(tokens.at(-1)!)) status = Number(tokens.pop());\n\n const segments = [...parts, ...tokens].map((seg) => seg.replace(/^\\[([^\\]]+)\\]$/, '{$1}'));\n if (segments.some((s) => s === '')) return undefined;\n return { method: method.toUpperCase(), path: `/${segments.join('/')}`, status };\n}\n\n/** Load a folder of sample JSON responses into the normalized model. */\nexport async function loadSamples(dir: string): Promise<MockModel> {\n const root = path.resolve(dir);\n const warnings: string[] = [];\n // route key → status → { inferrer, empty }\n const groups = new Map<string, { method: string; path: string; statuses: Map<number, { inferrer: SchemaInferrer; empty: boolean }> }>();\n\n for (const file of await listJsonFiles(root)) {\n const rel = path.relative(root, file);\n const parsed = parseSampleName(rel);\n if (!parsed) {\n warnings.push(`${rel}: ignored; name it like GET.users.json or users/GET.[id].json`);\n continue;\n }\n\n const text = (await fs.readFile(file, 'utf8')).trim();\n let body: unknown;\n if (text) {\n try {\n body = JSON.parse(text);\n } catch (err) {\n warnings.push(`${rel}: invalid JSON, skipped (${(err as Error).message})`);\n continue;\n }\n }\n\n const status = parsed.status ?? (!text ? 204 : parsed.method === 'POST' ? 201 : 200);\n const key = `${parsed.method} ${parsed.path}`;\n let group = groups.get(key);\n if (!group) groups.set(key, (group = { method: parsed.method, path: parsed.path, statuses: new Map() }));\n let entry = group.statuses.get(status);\n if (!entry) group.statuses.set(status, (entry = { inferrer: new SchemaInferrer(), empty: true }));\n if (text) {\n entry.inferrer.add(body);\n entry.empty = false;\n }\n }\n\n const routes: MockRoute[] = [];\n for (const group of groups.values()) {\n const responses: MockResponse[] = [...group.statuses]\n .sort(([a], [b]) => a - b)\n .map(([status, { inferrer, empty }]) => ({\n status,\n hasExample: false,\n ...(empty ? {} : { schema: inferrer.schema(), contentType: 'application/json' }),\n }));\n routes.push({ method: group.method, path: group.path, ...compilePath(group.path), responses });\n }\n\n if (!routes.length) warnings.push(`${root}: no sample files found`);\n return { openapi: '3.1.0', title: path.basename(root), version: '', basePath: '', routes: sortRoutes(routes), warnings, sources: [root] };\n}\n\nasync function listJsonFiles(dir: string): Promise<string[]> {\n const entries = await fs.readdir(dir, { withFileTypes: true, recursive: true });\n return entries\n .filter((e) => e.isFile() && e.name.toLowerCase().endsWith('.json'))\n .map((e) => path.join(e.parentPath, e.name))\n // Skip dot-folders and node_modules inside the sample folder, not above it:\n // `.mocks/` is a common place to keep samples.\n .filter((f) => !path.relative(dir, f).split(path.sep).some((seg) => seg === 'node_modules' || seg.startsWith('.')))\n .sort();\n}\n\n/** Serialize any model as an OpenAPI 3.1 document, so sample users can graduate to a spec. */\nexport function toOpenApi(model: MockModel): Record<string, unknown> {\n const paths: Record<string, Record<string, unknown>> = {};\n // Plain code-unit order: localeCompare depends on the machine's ICU data.\n for (const route of [...model.routes].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))) {\n const item = (paths[route.path] ??= {});\n if (route.paramNames.length && !item.parameters) {\n item.parameters = route.paramNames.map((name) => ({ name, in: 'path', required: true, schema: { type: 'string' } }));\n }\n const responses: Record<string, unknown> = {};\n for (const res of route.responses) {\n responses[res.isDefault ? 'default' : String(res.status)] = {\n description: 'Inferred from samples',\n ...(res.schema ? { content: { [res.contentType ?? 'application/json']: { schema: res.schema } } } : {}),\n };\n }\n item[route.method.toLowerCase()] = { responses };\n }\n return {\n openapi: '3.1.0',\n info: { title: model.title, version: model.version || '0.0.0' },\n paths,\n };\n}\n\n/** `toOpenApi` rendered as YAML or JSON, picked by the output file's extension. */\nexport function formatOpenApi(model: MockModel, outFile?: string): string {\n const doc = toOpenApi(model);\n return outFile?.toLowerCase().endsWith('.json') ? `${JSON.stringify(doc, null, 2)}\\n` : stringify(doc, { aliasDuplicateObjects: false });\n}\n"]}
@@ -0,0 +1,38 @@
1
+ import type { Schema } from './model.js';
2
+ export declare function typeOf(schema: Schema): string | undefined;
3
+ /**
4
+ * OpenAPI `discriminator`s, resolved at load time. Plain JSON Schema can't tell
5
+ * `oneOf` branches apart when they differ only by a discriminator value, so the
6
+ * generator, `fits` and the validator all pick the branch from this mapping.
7
+ */
8
+ export declare const DISCRIMINATOR: unique symbol;
9
+ export interface Discriminator {
10
+ property: string;
11
+ /** One entry per `oneOf`/`anyOf` branch. */
12
+ mapping: {
13
+ value: string;
14
+ schema: Schema;
15
+ }[];
16
+ }
17
+ export declare function discriminatorOf(schema: Schema): Discriminator | undefined;
18
+ /**
19
+ * Merge `allOf` members into one schema: properties and required lists combine.
20
+ * A member with `additionalProperties: false` only allows its own properties, so
21
+ * optional properties from other members are dropped (that's what a validator
22
+ * enforces). Required ones are kept: such a spec can't be satisfied anyway, and
23
+ * a realistic value beats a placeholder.
24
+ *
25
+ * Schemas are never mutated, so results are cached by identity: generation merges
26
+ * the same schemas for every record.
27
+ */
28
+ export declare function mergeAllOf(schema: Schema): Schema;
29
+ /** `mergeAllOf({allOf: [base, branch]})`, cached per (base, branch) pair. */
30
+ export declare function mergeBranch(base: Schema, branch: Schema): Schema;
31
+ /**
32
+ * Whether `value` validates against `schema` — a compact validator covering the
33
+ * keywords OpenAPI uses, used to reject bad spec examples and to keep `oneOf`
34
+ * branches from overlapping. Returns false when there is no schema at all.
35
+ */
36
+ export declare function fits(value: unknown, schema: Schema | undefined): boolean;
37
+ /** How many of `schemas` the value matches, stopping early at `stopAt`. */
38
+ export declare function countMatches(value: unknown, schemas: Schema[], depth?: number, stopAt?: number): number;