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

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,139 @@
1
+ export function personOf(faker, rec) {
2
+ rec.person ??= { first: faker.person.firstName(), last: faker.person.lastName() };
3
+ return rec.person;
4
+ }
5
+ /** Normalize `created_at`, `createdAt`, `Created-At` to `createdat`. */
6
+ export function normalizeName(name) {
7
+ return name.toLowerCase().replace(/[^a-z0-9]/g, '');
8
+ }
9
+ const has = (rec, ...names) => names.some((n) => rec.siblings.has(n));
10
+ const STRING_RULES = [
11
+ [/^(firstname|givenname|forename)$/, (f, r) => personOf(f, r).first],
12
+ [/^(lastname|surname|familyname)$/, (f, r) => personOf(f, r).last],
13
+ [/^(fullname|displayname|author|authorname|ownername|customername|contactname)$/, (f, r) => fullName(f, r)],
14
+ [
15
+ /^name$/,
16
+ (f, r) => {
17
+ const owner = normalizeName(r.owner ?? '');
18
+ if (/(product|item|sku)$/.test(owner) || has(r, 'price', 'sku', 'cost', 'stock', 'inventory'))
19
+ return f.commerce.productName();
20
+ if (/(team|group|department|squad|unit)$/.test(owner))
21
+ return `${f.word.adjective()} ${f.word.noun()}`.replace(/^\w/, (c) => c.toUpperCase());
22
+ if (/(org|organization|organisation|company|employer|account|workspace|tenant|vendor|brand)$/.test(owner))
23
+ return f.company.name();
24
+ if (/(project|repo|repository|app|application|service)$/.test(owner))
25
+ return f.lorem.slug(2);
26
+ if (/(category|tag|label|genre|topic)$/.test(owner))
27
+ return f.commerce.department();
28
+ if (has(r, 'domain', 'industry', 'employees', 'website'))
29
+ return f.company.name();
30
+ return fullName(f, r);
31
+ },
32
+ ],
33
+ [/^(username|login|handle|nickname|screenname)$/, (f, r) => username(f, r)],
34
+ [/email/, (f, r) => email(f, r)],
35
+ [/(phone|mobile|telephone|^tel$|fax)/, (f) => f.phone.number({ style: 'international' })],
36
+ [/(avatar|profileimage|profilepicture|profilephoto)/, (f) => f.image.avatar()],
37
+ [/(image|photo|picture|thumbnail|logo|banner|cover)(url)?$/, (f) => f.image.url({ width: 640, height: 480 })],
38
+ [/(url|uri|website|homepage|link|href)$/, (f) => f.internet.url()],
39
+ [/^(company|companyname|organization|organisation|employer)$/, (f) => f.company.name()],
40
+ [/^(jobtitle|position|role|occupation)$/, (f) => f.person.jobTitle()],
41
+ [/^(street|streetaddress|address|addressline1?|line1)$/, (f) => f.location.streetAddress()],
42
+ [/^(addressline2|line2)$/, (f) => f.location.secondaryAddress()],
43
+ [/^(city|town)$/, (f) => f.location.city()],
44
+ [/^state$/, (f, r) => (has(r, 'city', 'zip', 'zipcode', 'postcode', 'postalcode', 'country') ? f.location.state() : f.helpers.arrayElement(['active', 'pending', 'inactive']))],
45
+ [/^(province|region)$/, (f) => f.location.state()],
46
+ [/^(zip|zipcode|postcode|postalcode)$/, (f) => f.location.zipCode()],
47
+ [/^(countrycode|countryiso)$/, (f) => f.location.countryCode()],
48
+ [/^country$/, (f) => f.location.country()],
49
+ [/^(timezone|tz)$/, (f) => f.location.timeZone()],
50
+ [/^(currency|currencycode)$/, (f) => f.finance.currencyCode()],
51
+ [/^(locale|language|lang)$/, (f) => f.helpers.arrayElement(['en-US', 'en-GB', 'de-DE', 'fr-FR', 'es-ES', 'ja-JP'])],
52
+ [/(color|colour)$/, (f) => f.color.human()],
53
+ [/^(hex|hexcolor)$/, (f) => f.color.rgb()],
54
+ [/^slug$/, (f) => f.lorem.slug(3)],
55
+ [/^sku$/, (f) => f.string.alphanumeric({ length: 8, casing: 'upper' })],
56
+ [/^(ip|ipaddress|ipv4)$/, (f) => f.internet.ipv4()],
57
+ [/^ipv6$/, (f) => f.internet.ipv6()],
58
+ [/^(domain|hostname|host)$/, (f) => f.internet.domainName()],
59
+ [/^(useragent)$/, (f) => f.internet.userAgent()],
60
+ [/^(password|secret)$/, (f) => f.internet.password({ length: 16 })],
61
+ [/^(title|headline|subject)$/, (f) => sentence(f.lorem.sentence({ min: 3, max: 7 }))],
62
+ [/^(description|summary|bio|about|excerpt)$/, (f) => f.lorem.sentences({ min: 1, max: 3 })],
63
+ [/^(body|content|text|comment|message|note|notes)$/, (f) => f.lorem.paragraph()],
64
+ [/^status$/, (f) => f.helpers.arrayElement(['active', 'pending', 'inactive'])],
65
+ [/^(type|kind|category)$/, (f) => f.commerce.department().toLowerCase()],
66
+ [/^(tag|label)$/, (f) => f.word.noun()],
67
+ [/^(token|accesstoken|refreshtoken|apikey)$/, (f) => f.string.alphanumeric(32)],
68
+ ];
69
+ /** A realistic string for this property name, or undefined when no rule applies. */
70
+ export function stringByName(f, name, rec, schema) {
71
+ if (isIdName(name))
72
+ return f.string.uuid();
73
+ const rule = ruleFor(name);
74
+ return rule ? rule(f, rec, schema) : undefined;
75
+ }
76
+ // Field names repeat endlessly (every record has an `email`), so remember which
77
+ // rule each name hits instead of running ~40 regexes per string.
78
+ const ruleCache = new Map();
79
+ function ruleFor(name) {
80
+ let rule = ruleCache.get(name);
81
+ if (rule === undefined) {
82
+ const n = normalizeName(name);
83
+ rule = STRING_RULES.find(([re]) => re.test(n))?.[1] ?? null;
84
+ ruleCache.set(name, rule);
85
+ }
86
+ return rule;
87
+ }
88
+ /** Whether a string property name implies a timestamp when the schema gives no `format`. */
89
+ export function looksLikeDateTime(name) {
90
+ return /^(timestamp|datetime|time|date)$/i.test(name) || /([a-z0-9](At|On|Date|Time|Timestamp)|_(at|on|date|time|timestamp))$/.test(name);
91
+ }
92
+ /** `id`, `userId`, `user_id`, `uuid` — but not `paid` or `valid`. */
93
+ export function isIdName(name) {
94
+ return /^(id|uuid|guid)$/i.test(name) || /([a-z0-9](Id|ID|Uuid)|[_-](id|uuid))$/.test(name);
95
+ }
96
+ export function looksLikeDate(name) {
97
+ const n = normalizeName(name);
98
+ return /(birthday|birthdate|dob|dateofbirth)$/.test(n) || /^(date|day)$/.test(n);
99
+ }
100
+ const NUMBER_RULES = [
101
+ [/(price|amount|cost|balance|salary|fee|subtotal|tax|revenue)$/, (f, int) => (int ? f.number.int({ min: 100, max: 100_000 }) : f.number.float({ min: 1, max: 1000, fractionDigits: 2 }))],
102
+ [/^age$/, (f) => f.number.int({ min: 18, max: 85 })],
103
+ [/^(lat|latitude)$/, (f) => f.location.latitude()],
104
+ [/^(lng|lon|long|longitude)$/, (f) => f.location.longitude()],
105
+ [/^(rating|stars|score)$/, (f, int) => (int ? f.number.int({ min: 1, max: 5 }) : f.number.float({ min: 1, max: 5, fractionDigits: 1 }))],
106
+ [/(percent|percentage|ratio)$/, (f, int) => (int ? f.number.int({ min: 0, max: 100 }) : f.number.float({ min: 0, max: 1, fractionDigits: 2 }))],
107
+ [/^(page|pagenumber|currentpage)$/, (f) => f.number.int({ min: 1, max: 5 })],
108
+ [/^(pagesize|perpage|limit|per)$/, (f) => f.helpers.arrayElement([10, 20, 25, 50])],
109
+ [/^(year)$/, (f) => f.number.int({ min: 1990, max: 2026 })],
110
+ [/(count|total|quantity|qty|stock|size|length|number)$/, (f) => f.number.int({ min: 0, max: 100 })],
111
+ ];
112
+ /** A realistic number for this property name, or undefined when no rule applies. */
113
+ export function numberByName(f, name, integer) {
114
+ if (isIdName(name))
115
+ return f.number.int({ min: 1, max: 10_000 });
116
+ if (integer && looksLikeDateTime(name))
117
+ return Math.floor(f.date.past({ years: 2 }).getTime() / 1000);
118
+ const n = normalizeName(name);
119
+ for (const [re, gen] of NUMBER_RULES)
120
+ if (re.test(n))
121
+ return gen(f, integer);
122
+ return undefined;
123
+ }
124
+ function fullName(f, r) {
125
+ const p = personOf(f, r);
126
+ return `${p.first} ${p.last}`;
127
+ }
128
+ function username(f, r) {
129
+ const p = personOf(f, r);
130
+ return f.internet.username({ firstName: p.first, lastName: p.last });
131
+ }
132
+ function email(f, r) {
133
+ const p = personOf(f, r);
134
+ return f.internet.email({ firstName: p.first, lastName: p.last }).toLowerCase();
135
+ }
136
+ function sentence(s) {
137
+ return s.replace(/\.$/, '');
138
+ }
139
+ //# sourceMappingURL=heuristics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heuristics.js","sourceRoot":"","sources":["../src/heuristics.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Faker } from '@faker-js/faker';\nimport type { Schema } from './model.js';\n\n/** Per-object context so a record's name, username, email and avatar agree with each other. */\nexport interface RecordContext {\n /** Property names of the object being generated. */\n siblings: Set<string>;\n /** Name of the property holding this object, e.g. `team` for `team.name`. */\n owner?: string;\n person?: { first: string; last: string };\n}\n\nexport function personOf(faker: Faker, rec: RecordContext) {\n rec.person ??= { first: faker.person.firstName(), last: faker.person.lastName() };\n return rec.person;\n}\n\n/** Normalize `created_at`, `createdAt`, `Created-At` to `createdat`. */\nexport function normalizeName(name: string): string {\n return name.toLowerCase().replace(/[^a-z0-9]/g, '');\n}\n\ntype StringRule = [RegExp, (f: Faker, rec: RecordContext, schema: Schema) => string];\n\nconst has = (rec: RecordContext, ...names: string[]) => names.some((n) => rec.siblings.has(n));\n\nconst STRING_RULES: StringRule[] = [\n [/^(firstname|givenname|forename)$/, (f, r) => personOf(f, r).first],\n [/^(lastname|surname|familyname)$/, (f, r) => personOf(f, r).last],\n [/^(fullname|displayname|author|authorname|ownername|customername|contactname)$/, (f, r) => fullName(f, r)],\n [\n /^name$/,\n (f, r) => {\n const owner = normalizeName(r.owner ?? '');\n if (/(product|item|sku)$/.test(owner) || has(r, 'price', 'sku', 'cost', 'stock', 'inventory')) return f.commerce.productName();\n if (/(team|group|department|squad|unit)$/.test(owner)) return `${f.word.adjective()} ${f.word.noun()}`.replace(/^\\w/, (c) => c.toUpperCase());\n if (/(org|organization|organisation|company|employer|account|workspace|tenant|vendor|brand)$/.test(owner)) return f.company.name();\n if (/(project|repo|repository|app|application|service)$/.test(owner)) return f.lorem.slug(2);\n if (/(category|tag|label|genre|topic)$/.test(owner)) return f.commerce.department();\n if (has(r, 'domain', 'industry', 'employees', 'website')) return f.company.name();\n return fullName(f, r);\n },\n ],\n [/^(username|login|handle|nickname|screenname)$/, (f, r) => username(f, r)],\n [/email/, (f, r) => email(f, r)],\n [/(phone|mobile|telephone|^tel$|fax)/, (f) => f.phone.number({ style: 'international' })],\n [/(avatar|profileimage|profilepicture|profilephoto)/, (f) => f.image.avatar()],\n [/(image|photo|picture|thumbnail|logo|banner|cover)(url)?$/, (f) => f.image.url({ width: 640, height: 480 })],\n [/(url|uri|website|homepage|link|href)$/, (f) => f.internet.url()],\n [/^(company|companyname|organization|organisation|employer)$/, (f) => f.company.name()],\n [/^(jobtitle|position|role|occupation)$/, (f) => f.person.jobTitle()],\n [/^(street|streetaddress|address|addressline1?|line1)$/, (f) => f.location.streetAddress()],\n [/^(addressline2|line2)$/, (f) => f.location.secondaryAddress()],\n [/^(city|town)$/, (f) => f.location.city()],\n [/^state$/, (f, r) => (has(r, 'city', 'zip', 'zipcode', 'postcode', 'postalcode', 'country') ? f.location.state() : f.helpers.arrayElement(['active', 'pending', 'inactive']))],\n [/^(province|region)$/, (f) => f.location.state()],\n [/^(zip|zipcode|postcode|postalcode)$/, (f) => f.location.zipCode()],\n [/^(countrycode|countryiso)$/, (f) => f.location.countryCode()],\n [/^country$/, (f) => f.location.country()],\n [/^(timezone|tz)$/, (f) => f.location.timeZone()],\n [/^(currency|currencycode)$/, (f) => f.finance.currencyCode()],\n [/^(locale|language|lang)$/, (f) => f.helpers.arrayElement(['en-US', 'en-GB', 'de-DE', 'fr-FR', 'es-ES', 'ja-JP'])],\n [/(color|colour)$/, (f) => f.color.human()],\n [/^(hex|hexcolor)$/, (f) => f.color.rgb()],\n [/^slug$/, (f) => f.lorem.slug(3)],\n [/^sku$/, (f) => f.string.alphanumeric({ length: 8, casing: 'upper' })],\n [/^(ip|ipaddress|ipv4)$/, (f) => f.internet.ipv4()],\n [/^ipv6$/, (f) => f.internet.ipv6()],\n [/^(domain|hostname|host)$/, (f) => f.internet.domainName()],\n [/^(useragent)$/, (f) => f.internet.userAgent()],\n [/^(password|secret)$/, (f) => f.internet.password({ length: 16 })],\n [/^(title|headline|subject)$/, (f) => sentence(f.lorem.sentence({ min: 3, max: 7 }))],\n [/^(description|summary|bio|about|excerpt)$/, (f) => f.lorem.sentences({ min: 1, max: 3 })],\n [/^(body|content|text|comment|message|note|notes)$/, (f) => f.lorem.paragraph()],\n [/^status$/, (f) => f.helpers.arrayElement(['active', 'pending', 'inactive'])],\n [/^(type|kind|category)$/, (f) => f.commerce.department().toLowerCase()],\n [/^(tag|label)$/, (f) => f.word.noun()],\n [/^(token|accesstoken|refreshtoken|apikey)$/, (f) => f.string.alphanumeric(32)],\n];\n\n/** A realistic string for this property name, or undefined when no rule applies. */\nexport function stringByName(f: Faker, name: string, rec: RecordContext, schema: Schema): string | undefined {\n if (isIdName(name)) return f.string.uuid();\n const rule = ruleFor(name);\n return rule ? rule(f, rec, schema) : undefined;\n}\n\n// Field names repeat endlessly (every record has an `email`), so remember which\n// rule each name hits instead of running ~40 regexes per string.\nconst ruleCache = new Map<string, StringRule[1] | null>();\n\nfunction ruleFor(name: string): StringRule[1] | null {\n let rule = ruleCache.get(name);\n if (rule === undefined) {\n const n = normalizeName(name);\n rule = STRING_RULES.find(([re]) => re.test(n))?.[1] ?? null;\n ruleCache.set(name, rule);\n }\n return rule;\n}\n\n/** Whether a string property name implies a timestamp when the schema gives no `format`. */\nexport function looksLikeDateTime(name: string): boolean {\n return /^(timestamp|datetime|time|date)$/i.test(name) || /([a-z0-9](At|On|Date|Time|Timestamp)|_(at|on|date|time|timestamp))$/.test(name);\n}\n\n/** `id`, `userId`, `user_id`, `uuid` — but not `paid` or `valid`. */\nexport function isIdName(name: string): boolean {\n return /^(id|uuid|guid)$/i.test(name) || /([a-z0-9](Id|ID|Uuid)|[_-](id|uuid))$/.test(name);\n}\n\nexport function looksLikeDate(name: string): boolean {\n const n = normalizeName(name);\n return /(birthday|birthdate|dob|dateofbirth)$/.test(n) || /^(date|day)$/.test(n);\n}\n\ntype NumberRule = [RegExp, (f: Faker, integer: boolean) => number];\n\nconst NUMBER_RULES: NumberRule[] = [\n [/(price|amount|cost|balance|salary|fee|subtotal|tax|revenue)$/, (f, int) => (int ? f.number.int({ min: 100, max: 100_000 }) : f.number.float({ min: 1, max: 1000, fractionDigits: 2 }))],\n [/^age$/, (f) => f.number.int({ min: 18, max: 85 })],\n [/^(lat|latitude)$/, (f) => f.location.latitude()],\n [/^(lng|lon|long|longitude)$/, (f) => f.location.longitude()],\n [/^(rating|stars|score)$/, (f, int) => (int ? f.number.int({ min: 1, max: 5 }) : f.number.float({ min: 1, max: 5, fractionDigits: 1 }))],\n [/(percent|percentage|ratio)$/, (f, int) => (int ? f.number.int({ min: 0, max: 100 }) : f.number.float({ min: 0, max: 1, fractionDigits: 2 }))],\n [/^(page|pagenumber|currentpage)$/, (f) => f.number.int({ min: 1, max: 5 })],\n [/^(pagesize|perpage|limit|per)$/, (f) => f.helpers.arrayElement([10, 20, 25, 50])],\n [/^(year)$/, (f) => f.number.int({ min: 1990, max: 2026 })],\n [/(count|total|quantity|qty|stock|size|length|number)$/, (f) => f.number.int({ min: 0, max: 100 })],\n];\n\n/** A realistic number for this property name, or undefined when no rule applies. */\nexport function numberByName(f: Faker, name: string, integer: boolean): number | undefined {\n if (isIdName(name)) return f.number.int({ min: 1, max: 10_000 });\n if (integer && looksLikeDateTime(name)) return Math.floor(f.date.past({ years: 2 }).getTime() / 1000);\n const n = normalizeName(name);\n for (const [re, gen] of NUMBER_RULES) if (re.test(n)) return gen(f, integer);\n return undefined;\n}\n\nfunction fullName(f: Faker, r: RecordContext) {\n const p = personOf(f, r);\n return `${p.first} ${p.last}`;\n}\n\nfunction username(f: Faker, r: RecordContext) {\n const p = personOf(f, r);\n return f.internet.username({ firstName: p.first, lastName: p.last });\n}\n\nfunction email(f: Faker, r: RecordContext) {\n const p = personOf(f, r);\n return f.internet.email({ firstName: p.first, lastName: p.last }).toLowerCase();\n}\n\nfunction sentence(s: string) {\n return s.replace(/\\.$/, '');\n}\n"]}
@@ -0,0 +1,22 @@
1
+ import { type MockServer, type MockServerOptions } from './server.js';
2
+ export { availableLocales, generate, type GenerateOptions } from './generate.js';
3
+ export type { MockModel, MockResponse, MockRoute, Schema } from './model.js';
4
+ export { inferSchema, SchemaInferrer } from './infer.js';
5
+ export { loadSource } from './load.js';
6
+ export { loadOpenApi, SpecLoadError } from './openapi.js';
7
+ export { formatOpenApi, loadSamples, parseSampleName, toOpenApi } from './samples.js';
8
+ export { ResponseValidator, toJsonSchema, type ValidationResult } from './validate.js';
9
+ export { respond, type MockResult, type MockServer, type MockServerOptions, type RespondInput } from './server.js';
10
+ /**
11
+ * Create a mock server from a spec path, a sample folder, or an already-loaded model.
12
+ *
13
+ * ```ts
14
+ * const mock = await createMockServer({ spec: './openapi.yaml' });
15
+ * const url = await mock.listen(0);
16
+ * ```
17
+ */
18
+ export declare function createMockServer(options: Omit<MockServerOptions, 'model'> & ({
19
+ spec: string;
20
+ } | {
21
+ model: MockServerOptions['model'];
22
+ })): Promise<MockServer>;
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ import { loadSource } from './load.js';
2
+ import { createMockServer as createServer } from './server.js';
3
+ export { availableLocales, generate } from './generate.js';
4
+ export { inferSchema, SchemaInferrer } from './infer.js';
5
+ export { loadSource } from './load.js';
6
+ export { loadOpenApi, SpecLoadError } from './openapi.js';
7
+ export { formatOpenApi, loadSamples, parseSampleName, toOpenApi } from './samples.js';
8
+ export { ResponseValidator, toJsonSchema } from './validate.js';
9
+ export { respond } from './server.js';
10
+ /**
11
+ * Create a mock server from a spec path, a sample folder, or an already-loaded model.
12
+ *
13
+ * ```ts
14
+ * const mock = await createMockServer({ spec: './openapi.yaml' });
15
+ * const url = await mock.listen(0);
16
+ * ```
17
+ */
18
+ export async function createMockServer(options) {
19
+ const model = 'model' in options ? options.model : await loadSource(options.spec);
20
+ return createServer({ ...options, model });
21
+ }
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,gBAAgB,IAAI,YAAY,EAA2C,MAAM,aAAa,CAAC;AAExG,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAwB,MAAM,eAAe,CAAC;AAEjF,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACtF,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAyB,MAAM,eAAe,CAAC;AACvF,OAAO,EAAE,OAAO,EAA+E,MAAM,aAAa,CAAC;AAEnH;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,OAAsG;IAEtG,MAAM,KAAK,GAAG,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAClF,OAAO,YAAY,CAAC,EAAE,GAAG,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;AAC7C,CAAC","sourcesContent":["import { loadSource } from './load.js';\nimport { createMockServer as createServer, type MockServer, type MockServerOptions } from './server.js';\n\nexport { availableLocales, generate, type GenerateOptions } from './generate.js';\nexport type { MockModel, MockResponse, MockRoute, Schema } from './model.js';\nexport { inferSchema, SchemaInferrer } from './infer.js';\nexport { loadSource } from './load.js';\nexport { loadOpenApi, SpecLoadError } from './openapi.js';\nexport { formatOpenApi, loadSamples, parseSampleName, toOpenApi } from './samples.js';\nexport { ResponseValidator, toJsonSchema, type ValidationResult } from './validate.js';\nexport { respond, type MockResult, type MockServer, type MockServerOptions, type RespondInput } from './server.js';\n\n/**\n * Create a mock server from a spec path, a sample folder, or an already-loaded model.\n *\n * ```ts\n * const mock = await createMockServer({ spec: './openapi.yaml' });\n * const url = await mock.listen(0);\n * ```\n */\nexport async function createMockServer(\n options: Omit<MockServerOptions, 'model'> & ({ spec: string } | { model: MockServerOptions['model'] }),\n): Promise<MockServer> {\n const model = 'model' in options ? options.model : await loadSource(options.spec);\n return createServer({ ...options, model });\n}\n"]}
@@ -0,0 +1,10 @@
1
+ import type { Schema } from './model.js';
2
+ /** Collects samples for one schema position. Call `add` per sample, then `schema()`. */
3
+ export declare class SchemaInferrer {
4
+ private acc;
5
+ add(value: unknown): this;
6
+ get samples(): number;
7
+ schema(): Schema;
8
+ }
9
+ /** Infer a JSON Schema (OpenAPI 3.1 dialect) that every one of `samples` validates against. */
10
+ export declare function inferSchema(...samples: unknown[]): Schema;
package/dist/infer.js ADDED
@@ -0,0 +1,166 @@
1
+ // Schema inference works in two passes: `observe` folds every sample value into an
2
+ // accumulator of statistics, then `finalize` turns those statistics into a schema.
3
+ // Seeing all samples first is what lets us tell "optional" from "always present",
4
+ // "nullable" from "never null", and an enum from free text.
5
+ const MAX_TRACKED_VALUES = 50;
6
+ /** Keys whose values are free text even when the samples happen to repeat. */
7
+ const FREE_TEXT_KEY = /(name|title|description|summary|label|text|message|comment|bio|note|notes|body|content)$/i;
8
+ function emptyAcc() {
9
+ return {
10
+ seen: 0,
11
+ nulls: 0,
12
+ booleans: 0,
13
+ integers: 0,
14
+ floats: 0,
15
+ negative: false,
16
+ strings: 0,
17
+ stringValues: new Map(),
18
+ tooManyValues: false,
19
+ formats: new Map(),
20
+ arrays: 0,
21
+ objects: 0,
22
+ props: new Map(),
23
+ };
24
+ }
25
+ /** Collects samples for one schema position. Call `add` per sample, then `schema()`. */
26
+ export class SchemaInferrer {
27
+ acc = emptyAcc();
28
+ add(value) {
29
+ observe(this.acc, value);
30
+ return this;
31
+ }
32
+ get samples() {
33
+ return this.acc.seen;
34
+ }
35
+ schema() {
36
+ return finalize(this.acc);
37
+ }
38
+ }
39
+ /** Infer a JSON Schema (OpenAPI 3.1 dialect) that every one of `samples` validates against. */
40
+ export function inferSchema(...samples) {
41
+ const inferrer = new SchemaInferrer();
42
+ for (const s of samples)
43
+ inferrer.add(s);
44
+ return inferrer.schema();
45
+ }
46
+ function observe(acc, value) {
47
+ acc.seen++;
48
+ if (value === null || value === undefined) {
49
+ acc.nulls++;
50
+ }
51
+ else if (typeof value === 'boolean') {
52
+ acc.booleans++;
53
+ }
54
+ else if (typeof value === 'number') {
55
+ if (Number.isInteger(value))
56
+ acc.integers++;
57
+ else
58
+ acc.floats++;
59
+ if (value < 0)
60
+ acc.negative = true;
61
+ }
62
+ else if (typeof value === 'string') {
63
+ acc.strings++;
64
+ const format = detectFormat(value);
65
+ acc.formats.set(format, (acc.formats.get(format) ?? 0) + 1);
66
+ if (!acc.tooManyValues) {
67
+ acc.stringValues.set(value, (acc.stringValues.get(value) ?? 0) + 1);
68
+ if (acc.stringValues.size > MAX_TRACKED_VALUES) {
69
+ acc.tooManyValues = true;
70
+ acc.stringValues.clear();
71
+ }
72
+ }
73
+ }
74
+ else if (Array.isArray(value)) {
75
+ acc.arrays++;
76
+ acc.items ??= emptyAcc();
77
+ for (const item of value)
78
+ observe(acc.items, item);
79
+ }
80
+ else if (typeof value === 'object') {
81
+ acc.objects++;
82
+ for (const [key, v] of Object.entries(value)) {
83
+ let prop = acc.props.get(key);
84
+ if (!prop)
85
+ acc.props.set(key, (prop = emptyAcc()));
86
+ observe(prop, v);
87
+ }
88
+ }
89
+ }
90
+ function finalize(acc, key) {
91
+ const variants = [];
92
+ if (acc.objects)
93
+ variants.push(objectSchema(acc));
94
+ if (acc.arrays)
95
+ variants.push({ type: 'array', items: acc.items && acc.items.seen ? finalize(acc.items) : {} });
96
+ if (acc.strings)
97
+ variants.push(stringSchema(acc, key));
98
+ if (acc.integers || acc.floats) {
99
+ variants.push({ type: acc.floats ? 'number' : 'integer', ...(acc.negative ? {} : { minimum: 0 }) });
100
+ }
101
+ if (acc.booleans)
102
+ variants.push({ type: 'boolean' });
103
+ if (variants.length === 0)
104
+ return acc.nulls ? { type: 'null' } : {};
105
+ if (variants.length === 1) {
106
+ const only = variants[0];
107
+ if (acc.nulls) {
108
+ only.type = [only.type, 'null'];
109
+ if (only.enum)
110
+ only.enum = [...only.enum, null];
111
+ }
112
+ return only;
113
+ }
114
+ if (acc.nulls)
115
+ variants.push({ type: 'null' });
116
+ return { anyOf: variants };
117
+ }
118
+ function objectSchema(acc) {
119
+ const properties = {};
120
+ const required = [];
121
+ for (const [key, prop] of acc.props) {
122
+ properties[key] = finalize(prop, key);
123
+ if (prop.seen === acc.objects)
124
+ required.push(key);
125
+ }
126
+ return { type: 'object', properties, ...(required.length ? { required } : {}) };
127
+ }
128
+ function stringSchema(acc, key) {
129
+ const formats = [...acc.formats.keys()];
130
+ if (formats.length === 1 && formats[0])
131
+ return { type: 'string', format: formats[0] };
132
+ // A handful of short values repeated across many samples is an enum (`status`, `role`).
133
+ const values = [...acc.stringValues.keys()];
134
+ const looksLikeEnum = !acc.tooManyValues &&
135
+ !(key && FREE_TEXT_KEY.test(key)) &&
136
+ acc.strings >= 4 &&
137
+ values.length <= Math.min(8, acc.strings / 2) &&
138
+ values.every((v) => v.length > 0 && v.length <= 24 && !/^\d+$/.test(v) && !/\s{2,}/.test(v));
139
+ if (looksLikeEnum)
140
+ return { type: 'string', enum: values.sort() };
141
+ return { type: 'string' };
142
+ }
143
+ const DATE_TIME = /^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/;
144
+ const DATE = /^\d{4}-\d{2}-\d{2}$/;
145
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
146
+ const EMAIL = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
147
+ const URI = /^https?:\/\/[^\s]+$/;
148
+ function detectFormat(s) {
149
+ if (DATE_TIME.test(s) && isRealDate(s.slice(0, 10)))
150
+ return /^.{10}[Tt]/.test(s) ? 'date-time' : '';
151
+ if (DATE.test(s) && isRealDate(s))
152
+ return 'date';
153
+ if (UUID.test(s))
154
+ return 'uuid';
155
+ if (EMAIL.test(s))
156
+ return 'email';
157
+ if (URI.test(s) && URL.canParse(s))
158
+ return 'uri';
159
+ return '';
160
+ }
161
+ /** `2024-02-30` matches the regex but isn't a date; validators reject it, so we must too. */
162
+ function isRealDate(s) {
163
+ const d = new Date(`${s}T00:00:00Z`);
164
+ return !Number.isNaN(d.getTime()) && d.toISOString().startsWith(s);
165
+ }
166
+ //# sourceMappingURL=infer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"infer.js","sourceRoot":"","sources":["../src/infer.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Schema } from './model.js';\n\n// Schema inference works in two passes: `observe` folds every sample value into an\n// accumulator of statistics, then `finalize` turns those statistics into a schema.\n// Seeing all samples first is what lets us tell \"optional\" from \"always present\",\n// \"nullable\" from \"never null\", and an enum from free text.\n\nconst MAX_TRACKED_VALUES = 50;\n\n/** Keys whose values are free text even when the samples happen to repeat. */\nconst FREE_TEXT_KEY = /(name|title|description|summary|label|text|message|comment|bio|note|notes|body|content)$/i;\n\ninterface Acc {\n /** Times this position was observed, including nulls. */\n seen: number;\n nulls: number;\n booleans: number;\n integers: number;\n floats: number;\n negative: boolean;\n strings: number;\n stringValues: Map<string, number>;\n tooManyValues: boolean;\n /** Detected format per string ('' when none), with counts. */\n formats: Map<string, number>;\n arrays: number;\n items?: Acc;\n objects: number;\n props: Map<string, Acc>;\n}\n\nfunction emptyAcc(): Acc {\n return {\n seen: 0,\n nulls: 0,\n booleans: 0,\n integers: 0,\n floats: 0,\n negative: false,\n strings: 0,\n stringValues: new Map(),\n tooManyValues: false,\n formats: new Map(),\n arrays: 0,\n objects: 0,\n props: new Map(),\n };\n}\n\n/** Collects samples for one schema position. Call `add` per sample, then `schema()`. */\nexport class SchemaInferrer {\n private acc = emptyAcc();\n\n add(value: unknown): this {\n observe(this.acc, value);\n return this;\n }\n\n get samples(): number {\n return this.acc.seen;\n }\n\n schema(): Schema {\n return finalize(this.acc);\n }\n}\n\n/** Infer a JSON Schema (OpenAPI 3.1 dialect) that every one of `samples` validates against. */\nexport function inferSchema(...samples: unknown[]): Schema {\n const inferrer = new SchemaInferrer();\n for (const s of samples) inferrer.add(s);\n return inferrer.schema();\n}\n\nfunction observe(acc: Acc, value: unknown): void {\n acc.seen++;\n if (value === null || value === undefined) {\n acc.nulls++;\n } else if (typeof value === 'boolean') {\n acc.booleans++;\n } else if (typeof value === 'number') {\n if (Number.isInteger(value)) acc.integers++;\n else acc.floats++;\n if (value < 0) acc.negative = true;\n } else if (typeof value === 'string') {\n acc.strings++;\n const format = detectFormat(value);\n acc.formats.set(format, (acc.formats.get(format) ?? 0) + 1);\n if (!acc.tooManyValues) {\n acc.stringValues.set(value, (acc.stringValues.get(value) ?? 0) + 1);\n if (acc.stringValues.size > MAX_TRACKED_VALUES) {\n acc.tooManyValues = true;\n acc.stringValues.clear();\n }\n }\n } else if (Array.isArray(value)) {\n acc.arrays++;\n acc.items ??= emptyAcc();\n for (const item of value) observe(acc.items, item);\n } else if (typeof value === 'object') {\n acc.objects++;\n for (const [key, v] of Object.entries(value)) {\n let prop = acc.props.get(key);\n if (!prop) acc.props.set(key, (prop = emptyAcc()));\n observe(prop, v);\n }\n }\n}\n\nfunction finalize(acc: Acc, key?: string): Schema {\n const variants: Schema[] = [];\n if (acc.objects) variants.push(objectSchema(acc));\n if (acc.arrays) variants.push({ type: 'array', items: acc.items && acc.items.seen ? finalize(acc.items) : {} });\n if (acc.strings) variants.push(stringSchema(acc, key));\n if (acc.integers || acc.floats) {\n variants.push({ type: acc.floats ? 'number' : 'integer', ...(acc.negative ? {} : { minimum: 0 }) });\n }\n if (acc.booleans) variants.push({ type: 'boolean' });\n\n if (variants.length === 0) return acc.nulls ? { type: 'null' } : {};\n if (variants.length === 1) {\n const only = variants[0]!;\n if (acc.nulls) {\n only.type = [only.type, 'null'];\n if (only.enum) only.enum = [...only.enum, null];\n }\n return only;\n }\n if (acc.nulls) variants.push({ type: 'null' });\n return { anyOf: variants };\n}\n\nfunction objectSchema(acc: Acc): Schema {\n const properties: Record<string, Schema> = {};\n const required: string[] = [];\n for (const [key, prop] of acc.props) {\n properties[key] = finalize(prop, key);\n if (prop.seen === acc.objects) required.push(key);\n }\n return { type: 'object', properties, ...(required.length ? { required } : {}) };\n}\n\nfunction stringSchema(acc: Acc, key?: string): Schema {\n const formats = [...acc.formats.keys()];\n if (formats.length === 1 && formats[0]) return { type: 'string', format: formats[0] };\n\n // A handful of short values repeated across many samples is an enum (`status`, `role`).\n const values = [...acc.stringValues.keys()];\n const looksLikeEnum =\n !acc.tooManyValues &&\n !(key && FREE_TEXT_KEY.test(key)) &&\n acc.strings >= 4 &&\n values.length <= Math.min(8, acc.strings / 2) &&\n values.every((v) => v.length > 0 && v.length <= 24 && !/^\\d+$/.test(v) && !/\\s{2,}/.test(v));\n if (looksLikeEnum) return { type: 'string', enum: values.sort() };\n return { type: 'string' };\n}\n\nconst DATE_TIME = /^\\d{4}-\\d{2}-\\d{2}[Tt ]\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?([Zz]|[+-]\\d{2}:\\d{2})$/;\nconst DATE = /^\\d{4}-\\d{2}-\\d{2}$/;\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\nconst EMAIL = /^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$/;\nconst URI = /^https?:\\/\\/[^\\s]+$/;\n\nfunction detectFormat(s: string): string {\n if (DATE_TIME.test(s) && isRealDate(s.slice(0, 10))) return /^.{10}[Tt]/.test(s) ? 'date-time' : '';\n if (DATE.test(s) && isRealDate(s)) return 'date';\n if (UUID.test(s)) return 'uuid';\n if (EMAIL.test(s)) return 'email';\n if (URI.test(s) && URL.canParse(s)) return 'uri';\n return '';\n}\n\n/** `2024-02-30` matches the regex but isn't a date; validators reject it, so we must too. */\nfunction isRealDate(s: string): boolean {\n const d = new Date(`${s}T00:00:00Z`);\n return !Number.isNaN(d.getTime()) && d.toISOString().startsWith(s);\n}\n"]}
package/dist/load.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { MockModel } from './model.js';
2
+ /** Load either input: an OpenAPI file, or a folder of sample responses. */
3
+ export declare function loadSource(source: string): Promise<MockModel>;
package/dist/load.js ADDED
@@ -0,0 +1,15 @@
1
+ import fs from 'node:fs/promises';
2
+ import { loadOpenApi, SpecLoadError } from './openapi.js';
3
+ import { loadSamples } from './samples.js';
4
+ /** Load either input: an OpenAPI file, or a folder of sample responses. */
5
+ export async function loadSource(source) {
6
+ let stat;
7
+ try {
8
+ stat = await fs.stat(source);
9
+ }
10
+ catch {
11
+ throw new SpecLoadError('no such file or folder', source);
12
+ }
13
+ return stat.isDirectory() ? loadSamples(source) : loadOpenApi(source);
14
+ }
15
+ //# sourceMappingURL=load.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load.js","sourceRoot":"","sources":["../src/load.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAElC,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,2EAA2E;AAC3E,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,MAAc;IAC7C,IAAI,IAAI,CAAC;IACT,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,aAAa,CAAC,wBAAwB,EAAE,MAAM,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;AACxE,CAAC","sourcesContent":["import fs from 'node:fs/promises';\nimport type { MockModel } from './model.js';\nimport { loadOpenApi, SpecLoadError } from './openapi.js';\nimport { loadSamples } from './samples.js';\n\n/** Load either input: an OpenAPI file, or a folder of sample responses. */\nexport async function loadSource(source: string): Promise<MockModel> {\n let stat;\n try {\n stat = await fs.stat(source);\n } catch {\n throw new SpecLoadError('no such file or folder', source);\n }\n return stat.isDirectory() ? loadSamples(source) : loadOpenApi(source);\n}\n"]}
@@ -0,0 +1,34 @@
1
+ /** An OpenAPI / JSON Schema schema object, already dereferenced. May contain cycles. */
2
+ export type Schema = Record<string, any>;
3
+ export interface MockResponse {
4
+ status: number;
5
+ /** True when this came from the spec's `default` response rather than an explicit code. */
6
+ isDefault?: boolean;
7
+ schema?: Schema;
8
+ /** A concrete example from the spec; preferred over generated data. */
9
+ example?: unknown;
10
+ hasExample: boolean;
11
+ contentType?: string;
12
+ }
13
+ export interface MockRoute {
14
+ method: string;
15
+ /** Path template as written in the spec, e.g. `/users/{id}`. */
16
+ path: string;
17
+ pattern: RegExp;
18
+ paramNames: string[];
19
+ operationId?: string;
20
+ responses: MockResponse[];
21
+ }
22
+ export interface MockModel {
23
+ /** OpenAPI version the schemas are written in (`3.0.x` or `3.1.x`); sample folders infer 3.1. */
24
+ openapi: string;
25
+ title: string;
26
+ version: string;
27
+ /** Path prefix from `servers[0].url`, e.g. `/v1`; requests may include or omit it. */
28
+ basePath: string;
29
+ routes: MockRoute[];
30
+ /** Non-fatal problems found while loading (lenient mode). */
31
+ warnings: string[];
32
+ /** Files the model was built from, for hot reload. */
33
+ sources: string[];
34
+ }
package/dist/model.js ADDED
@@ -0,0 +1,4 @@
1
+ // The normalized model every input (OpenAPI spec, sample folder) is converted into.
2
+ // The generator and server only ever see this, never the original source.
3
+ export {};
4
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.js","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,0EAA0E","sourcesContent":["// The normalized model every input (OpenAPI spec, sample folder) is converted into.\n// The generator and server only ever see this, never the original source.\n\n/** An OpenAPI / JSON Schema schema object, already dereferenced. May contain cycles. */\nexport type Schema = Record<string, any>;\n\nexport interface MockResponse {\n status: number;\n /** True when this came from the spec's `default` response rather than an explicit code. */\n isDefault?: boolean;\n schema?: Schema;\n /** A concrete example from the spec; preferred over generated data. */\n example?: unknown;\n hasExample: boolean;\n contentType?: string;\n}\n\nexport interface MockRoute {\n method: string;\n /** Path template as written in the spec, e.g. `/users/{id}`. */\n path: string;\n pattern: RegExp;\n paramNames: string[];\n operationId?: string;\n responses: MockResponse[];\n}\n\nexport interface MockModel {\n /** OpenAPI version the schemas are written in (`3.0.x` or `3.1.x`); sample folders infer 3.1. */\n openapi: string;\n title: string;\n version: string;\n /** Path prefix from `servers[0].url`, e.g. `/v1`; requests may include or omit it. */\n basePath: string;\n routes: MockRoute[];\n /** Non-fatal problems found while loading (lenient mode). */\n warnings: string[];\n /** Files the model was built from, for hot reload. */\n sources: string[];\n}\n"]}
@@ -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
+ };