UNPKG

@kubb/plugin-faker

Version:

Faker.js data generator plugin for Kubb, creating realistic mock data from OpenAPI specifications for development and testing.

454 lines (453 loc) 17 kB
import "./chunk--u3MIqq1.js"; import { createParser, findSchemaKeyword, isKeyword, schemaKeywords } from "@kubb/plugin-oas"; import { File, Function, FunctionParams } from "@kubb/react-fabric"; import { jsx, jsxs } from "@kubb/react-fabric/jsx-runtime"; //#region ../../internals/utils/src/string.ts /** * Strips a single matching pair of `"..."`, `'...'`, or `` `...` `` from both ends of `text`. * Returns the string unchanged when no balanced quote pair is found. * * @example * trimQuotes('"hello"') // 'hello' * trimQuotes('hello') // 'hello' */ function trimQuotes(text) { if (text.length >= 2) { const first = text[0]; const last = text[text.length - 1]; if (first === "\"" && last === "\"" || first === "'" && last === "'" || first === "`" && last === "`") return text.slice(1, -1); } return text; } /** * Escapes characters that are not allowed inside JS string literals. * Handles quotes, backslashes, and Unicode line terminators (U+2028 / U+2029). * * @see http://www.ecma-international.org/ecma-262/5.1/#sec-7.8.4 */ function jsStringEscape(input) { return `${input}`.replace(/["'\\\n\r\u2028\u2029]/g, (character) => { switch (character) { case "\"": case "'": case "\\": return `\\${character}`; case "\n": return "\\n"; case "\r": return "\\r"; case "\u2028": return "\\u2028"; case "\u2029": return "\\u2029"; default: return ""; } }); } //#endregion //#region ../../internals/utils/src/object.ts /** * Serializes a primitive value to a JSON string literal, stripping any surrounding quote characters first. * * @example * stringify('hello') // '"hello"' * stringify('"hello"') // '"hello"' */ function stringify(value) { if (value === void 0 || value === null) return "\"\""; return JSON.stringify(trimQuotes(value.toString())); } //#endregion //#region ../../internals/utils/src/regexp.ts /** * Converts a pattern string into a `new RegExp(...)` constructor call or a regex literal string. * Inline flags expressed as `^(?im)` prefixes are extracted and applied to the resulting expression. * Pass `null` as the second argument to emit a `/pattern/flags` literal instead. * * @example * toRegExpString('^(?im)foo') // → 'new RegExp("foo", "im")' * toRegExpString('^(?im)foo', null) // → '/foo/im' */ function toRegExpString(text, func = "RegExp") { const raw = trimQuotes(text); const match = raw.match(/^\^(\(\?([igmsuy]+)\))/i); const replacementTarget = match?.[1] ?? ""; const matchedFlags = match?.[2]; const cleaned = raw.replace(/^\\?\//, "").replace(/\\?\/$/, "").replace(replacementTarget, ""); const { source, flags } = new RegExp(cleaned, matchedFlags); if (func === null) return `/${source}/${flags}`; return `new ${func}(${JSON.stringify(source)}${flags ? `, ${JSON.stringify(flags)}` : ""})`; } //#endregion //#region src/parser.ts const fakerKeywordMapper = { any: () => "undefined", unknown: () => "undefined", void: () => "undefined", number: (min, max) => { if (max !== void 0 && min !== void 0) return `faker.number.float({ min: ${min}, max: ${max} })`; if (max !== void 0) return `faker.number.float({ max: ${max} })`; if (min !== void 0) return `faker.number.float({ min: ${min} })`; return "faker.number.float()"; }, integer: (min, max) => { if (max !== void 0 && min !== void 0) return `faker.number.int({ min: ${min}, max: ${max} })`; if (max !== void 0) return `faker.number.int({ max: ${max} })`; if (min !== void 0) return `faker.number.int({ min: ${min} })`; return "faker.number.int()"; }, bigint: () => "faker.number.bigInt()", string: (min, max) => { if (max !== void 0 && min !== void 0) return `faker.string.alpha({ length: { min: ${min}, max: ${max} } })`; if (max !== void 0) return `faker.string.alpha({ length: ${max} })`; if (min !== void 0) return `faker.string.alpha({ length: ${min} })`; return "faker.string.alpha()"; }, boolean: () => "faker.datatype.boolean()", undefined: () => "undefined", null: () => "null", array: (items = [], min, max) => { if (items.length > 1) return `faker.helpers.arrayElements([${items.join(", ")}])`; const item = items.at(0); if (min !== void 0 && max !== void 0) return `faker.helpers.multiple(() => (${item}), { count: { min: ${min}, max: ${max} }})`; if (min !== void 0) return `faker.helpers.multiple(() => (${item}), { count: ${min} })`; if (max !== void 0) return `faker.helpers.multiple(() => (${item}), { count: { min: 0, max: ${max} }})`; return `faker.helpers.multiple(() => (${item}))`; }, tuple: (items = []) => `[${items.join(", ")}]`, enum: (items = [], type = "any") => `faker.helpers.arrayElement<${type}>([${items.join(", ")}])`, union: (items = []) => `faker.helpers.arrayElement<any>([${items.join(", ")}])`, datetime: () => "faker.date.anytime().toISOString()", date: (type = "string", parser = "faker") => { if (type === "string") { if (parser !== "faker") return `${parser}(faker.date.anytime()).format("YYYY-MM-DD")`; return "faker.date.anytime().toISOString().substring(0, 10)"; } if (parser !== "faker") throw new Error(`type '${type}' and parser '${parser}' can not work together`); return "faker.date.anytime()"; }, time: (type = "string", parser = "faker") => { if (type === "string") { if (parser !== "faker") return `${parser}(faker.date.anytime()).format("HH:mm:ss")`; return "faker.date.anytime().toISOString().substring(11, 19)"; } if (parser !== "faker") throw new Error(`type '${type}' and parser '${parser}' can not work together`); return "faker.date.anytime()"; }, uuid: () => "faker.string.uuid()", url: () => "faker.internet.url()", and: (items = []) => { if (items.length === 0) return "{}"; if (items.length === 1) return items[0] ?? "{}"; return `{...${items.join(", ...")}}`; }, object: () => "object", ref: () => "ref", matches: (value = "", regexGenerator = "faker") => { if (regexGenerator === "randexp") return `${toRegExpString(value, "RandExp")}.gen()`; return `faker.helpers.fromRegExp("${value}")`; }, email: () => "faker.internet.email()", firstName: () => "faker.person.firstName()", lastName: () => "faker.person.lastName()", password: () => "faker.internet.password()", phone: () => "faker.phone.number()", blob: () => "faker.image.url() as unknown as Blob", default: void 0, describe: void 0, const: (value) => value ?? "", max: void 0, min: void 0, nullable: void 0, nullish: void 0, optional: void 0, readOnly: void 0, writeOnly: void 0, deprecated: void 0, example: void 0, schema: void 0, catchall: void 0, name: void 0, interface: void 0, exclusiveMaximum: void 0, exclusiveMinimum: void 0 }; /** * @link based on https://github.com/cellular/oazapfts/blob/7ba226ebb15374e8483cc53e7532f1663179a22c/src/codegen/generate.ts#L398 */ function schemaKeywordSorter(_a, b) { if (b.keyword === "null") return -1; return 0; } function joinItems(items) { switch (items.length) { case 0: return "undefined"; case 1: return items[0]; default: return fakerKeywordMapper.union(items); } } /** * Returns true if any schema in the list contains a $ref that points to a type other * than rootTypeName (i.e. an indirect / mutual ref rather than a direct self-ref). * Recurses through array.items, union/and args, tuple items, and nested object properties. */ function hasIndirectRef(schemas, rootTypeName) { return hasIndirectRefInner(schemas, rootTypeName, /* @__PURE__ */ new WeakSet()); } function hasIndirectRefInner(schemas, rootTypeName, visited) { return schemas.some((schema) => { if (!("args" in schema)) return false; if (visited.has(schema)) return false; visited.add(schema); switch (schema.keyword) { case schemaKeywords.ref: return schema.args?.name !== rootTypeName; case schemaKeywords.array: return hasIndirectRefInner(schema.args?.items ?? [], rootTypeName, visited); case schemaKeywords.union: case schemaKeywords.and: return Array.isArray(schema.args) && hasIndirectRefInner(schema.args, rootTypeName, visited); case schemaKeywords.tuple: return hasIndirectRefInner(Array.isArray(schema.args?.items) ? schema.args.items : schema.args?.items ? [schema.args.items] : [], rootTypeName, visited); case schemaKeywords.object: { const props = schema.args?.properties; if (!props) return false; return Object.values(props).some((propSchemas) => hasIndirectRefInner(propSchemas, rootTypeName, visited)); } } return false; }); } const parse = createParser({ mapper: fakerKeywordMapper, handlers: { union(tree, options) { const { current, schema, name, siblings } = tree; if (Array.isArray(current.args) && !current.args.length) return ""; return fakerKeywordMapper.union(current.args.map((it) => this.parse({ schema, parent: current, name, current: it, siblings }, { ...options, canOverride: false })).filter((x) => Boolean(x))); }, and(tree, options) { const { current, schema, siblings } = tree; return fakerKeywordMapper.and(current.args.map((it) => this.parse({ schema, parent: current, current: it, siblings }, { ...options, canOverride: false })).filter((x) => Boolean(x))); }, array(tree, options) { const { current, schema } = tree; return fakerKeywordMapper.array(current.args.items.map((it) => this.parse({ schema, parent: current, current: it, siblings: current.args.items }, { ...options, typeName: `NonNullable<${options.typeName}>[number]`, canOverride: false })).filter((x) => Boolean(x)), current.args.min, current.args.max); }, enum(tree, options) { const { current, parent, name } = tree; if (parent ? isKeyword(parent, schemaKeywords.tuple) : false) return fakerKeywordMapper.enum(current.args.items.map((schema) => { if (schema.format === "number") return schema.value; if (schema.format === "boolean") return schema.value; return stringify(schema.value); })); return fakerKeywordMapper.enum(current.args.items.map((schema) => { if (schema.format === "number") return schema.value; if (schema.format === "boolean") return schema.value; return stringify(schema.value); }), name ? options.typeName : void 0); }, ref(tree, options) { const { current, parent } = tree; if (!current.args?.name) throw new Error(`Name not defined for keyword ${current.keyword}`); if (options.rootTypeName && current.args.name === options.rootTypeName) return "undefined as any"; const isNestedInObjectOrAnd = parent && (isKeyword(parent, schemaKeywords.object) || isKeyword(parent, schemaKeywords.and)); if (options.canOverride && !isNestedInObjectOrAnd) return `${current.args.name}(data)`; return `${current.args.name}()`; }, object(tree, options) { const { current, schema } = tree; const filteredEntries = Object.entries(current.args?.properties || {}).filter((item) => { const schema = item[1]; return schema && typeof schema.map === "function"; }); const anyIndirectRef = filteredEntries.some(([, schemas]) => hasIndirectRef(schemas, options.rootTypeName)); const argsObject = filteredEntries.map(([name, schemas]) => { const mappedName = schemas.find((schema) => schema.keyword === schemaKeywords.name)?.args || name; if (options.mapper && Object.hasOwn(options.mapper, mappedName)) return `"${name}": ${options.mapper?.[mappedName]}`; const parsed = joinItems(schemas.sort(schemaKeywordSorter).map((it) => this.parse({ schema, name, parent: current, current: it, siblings: schemas }, { ...options, typeName: `NonNullable<${options.typeName}>[${JSON.stringify(name)}]`, canOverride: false })).filter((x) => Boolean(x))); if (anyIndirectRef && hasIndirectRef(schemas, options.rootTypeName)) return `get "${name}"() { return ${parsed} }`; return `"${name}": ${parsed}`; }).join(","); if (anyIndirectRef && options.canOverride) return `{${argsObject}${argsObject.length > 0 ? "," : ""}...(data || {})}`; return `{${argsObject}}`; }, tuple(tree, options) { const { current, schema, siblings } = tree; if (Array.isArray(current.args.items)) return fakerKeywordMapper.tuple(current.args.items.map((it) => this.parse({ schema, parent: current, current: it, siblings }, { ...options, canOverride: false })).filter((x) => Boolean(x))); return this.parse({ schema, parent: current, current: current.args.items, siblings }, { ...options, canOverride: false }); }, const(tree, _options) { const { current } = tree; if (current.args.format === "number" && current.args.name !== void 0) return fakerKeywordMapper.const(current.args.name?.toString()); return fakerKeywordMapper.const(stringify(current.args.value)); }, matches(tree, options) { const { current } = tree; if (current.args) return fakerKeywordMapper.matches(current.args, options.regexGenerator); }, null() { return fakerKeywordMapper.null(); }, undefined() { return fakerKeywordMapper.undefined(); }, any() { return fakerKeywordMapper.any(); }, string(tree, _options) { const { siblings } = tree; if (siblings) { const minSchema = findSchemaKeyword(siblings, "min"); const maxSchema = findSchemaKeyword(siblings, "max"); return fakerKeywordMapper.string(minSchema?.args, maxSchema?.args); } return fakerKeywordMapper.string(); }, number(tree, _options) { const { siblings } = tree; if (siblings) { const minSchema = findSchemaKeyword(siblings, "min"); const maxSchema = findSchemaKeyword(siblings, "max"); return fakerKeywordMapper.number(minSchema?.args, maxSchema?.args); } return fakerKeywordMapper.number(); }, integer(tree, _options) { const { siblings } = tree; if (siblings) { const minSchema = findSchemaKeyword(siblings, "min"); const maxSchema = findSchemaKeyword(siblings, "max"); return fakerKeywordMapper.integer(minSchema?.args, maxSchema?.args); } return fakerKeywordMapper.integer(); }, bigint(_tree, _options) { return fakerKeywordMapper.bigint(); }, datetime() { return fakerKeywordMapper.datetime(); }, date(tree, options) { const { current } = tree; return fakerKeywordMapper.date(current.args.type, options.dateParser); }, time(tree, options) { const { current } = tree; return fakerKeywordMapper.time(current.args.type, options.dateParser); } } }); //#endregion //#region src/components/Faker.tsx function Faker({ tree, description, name, typeName, seed, regexGenerator, canOverride, mapper, dateParser }) { const fakerText = joinItems(tree.map((schema, _index, siblings) => parse({ name, schema, parent: void 0, current: schema, siblings }, { typeName, rootTypeName: name, regexGenerator, mapper, canOverride, dateParser })).filter((x) => Boolean(x))); const isArray = fakerText.startsWith("faker.helpers.arrayElements") || fakerText.startsWith("faker.helpers.multiple"); const isRefToArray = tree.some((s) => isKeyword(s, schemaKeywords.schema) && s.args.type === "array"); const isObject = fakerText.startsWith("{"); const isTuple = fakerText.startsWith("faker.helpers.arrayElement"); const isSimpleString = name === "string"; const isSimpleInt = name === "integer"; const isSimpleFloat = name === "float"; let fakerTextWithOverride = fakerText; if (canOverride && isObject) if (fakerText.includes("...(data || {})")) fakerTextWithOverride = `{ ...${fakerText} }`; else fakerTextWithOverride = `{ ...${fakerText}, ...data || {} }`; if (canOverride && isTuple) fakerTextWithOverride = `data || ${fakerText}`; if (canOverride && isArray) fakerTextWithOverride = `[ ...${fakerText}, ...data || [] ]`; if (canOverride && isSimpleString) fakerTextWithOverride = "data ?? faker.string.alpha()"; if (canOverride && isSimpleInt) fakerTextWithOverride = "data ?? faker.number.int()"; if (canOverride && isSimpleFloat) fakerTextWithOverride = "data ?? faker.number.float()"; let type = `Partial<${typeName}>`; if (isArray) type = typeName; if (isRefToArray) type = typeName; if (isSimpleString) type = name; if (isSimpleInt || isSimpleFloat) type = "number"; const params = FunctionParams.factory({ data: { type, optional: true } }); let returnType = canOverride ? typeName : void 0; if (isSimpleString || isSimpleInt || isSimpleFloat) returnType = type; return /* @__PURE__ */ jsx(File.Source, { name, isExportable: true, isIndexable: true, children: /* @__PURE__ */ jsxs(Function, { export: true, name, JSDoc: { comments: [description ? `@description ${jsStringEscape(description)}` : void 0].filter(Boolean) }, params: canOverride ? params.toConstructor() : void 0, returnType, children: [ seed ? `faker.seed(${JSON.stringify(seed)})` : void 0, /* @__PURE__ */ jsx("br", {}), `return ${fakerTextWithOverride}` ] }) }); } //#endregion export { Faker as t }; //# sourceMappingURL=components-_oswvtq2.js.map