UNPKG

data-mock-builder

Version:

🧰 A fluent, type-safe mock object generator for test data creation, seeding, and API mocking.

319 lines (318 loc) • 10.9 kB
// src/builder.ts var _MockBuilder = class _MockBuilder { // Track the current field being defined /** * Creates a new MockBuilder instance. * @param options Optional. { deepCopy?: boolean; skipValidation?: boolean } */ constructor(options) { this.fields = []; this.repeatCount = 1; this.deepCopyEnabled = true; this.defaultSkipValidation = true; this.currentFieldName = null; if (options && typeof options.deepCopy === "boolean") { this.deepCopyEnabled = options.deepCopy; } if (options && typeof options.skipValidation === "boolean") { this.defaultSkipValidation = options.skipValidation; } } /** * Enable or disable deep copy of field values in build(). * By default, deep copy is enabled to prevent mutation between builds. * @param enabled true to enable deep copy, false to disable * @returns The builder instance for chaining. */ deepCopy(enabled) { this.deepCopyEnabled = enabled; return this; } field(name, value) { if (arguments.length === 2) { return this.addField(name, value); } this.currentFieldName = name; return { /** * Sets a string value or generator for the field. * If no value is provided, a random string is generated. * * @param val The string value or a factory function returning a string. * @returns The builder instance for chaining. */ string: (val = () => Math.random().toString(36).slice(2, 6)) => this.addField(name, val), /** * Sets a number value or generator for the field. * If no value is provided, a random integer is generated. * * @param val The number value or a factory function returning a number. * @returns The builder instance for chaining. */ number: (val = () => Math.floor(Math.random() * 1e3)) => this.addField(name, val), /** * Sets a boolean value or generator for the field. * If no value is provided, a random boolean is generated. * * @param val The boolean value or a factory function returning a boolean. * @returns The builder instance for chaining. */ boolean: (val = () => Math.random() > 0.5) => this.addField(name, val), /** * Sets an array value or generator for the field. * * @param val The array value or a factory function returning an array. * @returns The builder instance for chaining. */ array: (val) => this.addField(name, val), /** * Sets an object value or generator for the field. * * @param val The object value or a factory function returning an object. * @returns The builder instance for chaining. */ object: (val) => this.addField(name, val), /** * Sets an incrementing number for the field, starting from `start` and incrementing by `step`. * @param start The starting value for the increment (default: 1). * @param step The increment step (default: 1). * @returns The builder instance for chaining. */ increment: (start = 1, step = 1) => { let counter = start; return this.addField(name, () => { const val = counter; counter += step; return val; }); }, validator: (validator) => { if (!this.currentFieldName) { throw new Error("validator() must be called after defining a field"); } return this.addValidator(this.currentFieldName, validator); } }; } /** * Adds a validator to the last defined field. * * @param name The field name. * @param validator The validator function. * @returns The builder instance for chaining. * @private */ addValidator(name, validator) { const fieldIndex = this.fields.findIndex((field) => field.name === name); if (fieldIndex === -1) { throw new Error(`Cannot add validator: field '${name}' not found.`); } this.fields[fieldIndex].validator = validator; return this; } /** * Adds a field definition to the builder. * * @param name The field name. * @param val The value or value factory for the field. * @returns The builder instance for chaining. * @private */ addField(name, val) { const generator = typeof val === "function" ? (obj, index, options) => val(obj, index, options) : () => val; this.fields.push({ name, generator }); this.currentFieldName = name; return this; } /** * Sets the number of objects to generate when build() is called. * * @param n The number of objects to build. * @returns The builder instance for chaining. * * Example: * ``` * builder.repeat(5).build(); // returns an array of 5 objects * ``` */ repeat(n) { this.repeatCount = n; return this; } /** * Extends the builder with fields from a template object. * Each key-value pair in the template becomes a field. * * @param template An object whose properties are added as fields. * @returns The builder instance for chaining. * * Example: * ``` * builder.extend({ foo: "bar", count: 42 }); * ``` */ extend(template) { Object.entries(template).forEach(([key, value]) => { this.addField(key, value); }); return this; } /** * Defines a named preset template for reuse in multiple builders. * * Presets are global and can be used in any MockBuilder instance via the `preset` method. * A preset is simply a named object template that can be reused and extended. * * Example: * ``` * // Define a preset named "user" * MockBuilder.definePreset("user", { name: "Alice", age: 30 }); * * // Use the preset in a builder * const builder = new MockBuilder().preset("user"); * const result = builder.build(); * // result: { name: "Alice", age: 30 } * * // You can override fields after applying a preset * const builder2 = new MockBuilder().preset("user").field("age").number(40); * const result2 = builder2.build(); * // result2: { name: "Alice", age: 40 } * ``` * * @param name The name of the preset. * @param template The template object to associate with the preset. */ static definePreset(name, template) { this.presets[name] = template; } /** * Applies a preset template by name to the builder. * Throws an error if the preset does not exist. * * @param name The name of the preset to apply. * @returns The builder instance for chaining. * @throws Error if the preset is not found. * * Example: * ``` * builder.preset("user"); * ``` */ preset(name) { const preset = _MockBuilder.presets[name]; if (!preset) throw new Error(`Preset "${name}" not found.`); return this.extend(preset); } /** * Sets a custom validator for a field. * The validator function should return an object with { success: boolean, errorMsg?: string }. * * @param fieldName The name of the field to validate. * @param validator The validator function. * @returns The builder instance for chaining. * * Example: * ``` * builder * .field("age").number(25) * .validator("age", (value) => ({ * success: value >= 18, * errorMsg: value >= 18 ? undefined : "Age must be at least 18" * })) * .field("email").string("user@example.com") * .validator("email", (value) => ({ * success: /^.+@.+\..+$/.test(value), * errorMsg: /^.+@.+\..+$/.test(value) ? undefined : "Invalid email format" * })); * ``` */ validator(fieldName, validator) { return this.addValidator(fieldName, validator); } /** * Builds the mock object(s) according to the defined fields and repeat count. * If repeat() was not called or set to 1, returns a single object. * If repeat() was set to n > 1, returns an array of n objects. * * @param options Optional. Options object: * - deepCopy: boolean (overrides the builder's deep copy setting for this build) * - skipValidation: boolean (if true, skips field validation for required fields; default: true) * @returns The built object or array of objects, optionally cast to type T. * * Example: * ``` * interface User { id: number; name: string } * const user = builder.build<User>(); * const users = builder.repeat(2).build<User[]>(); * ``` */ build(options = {}) { function deepClone(value) { if (Array.isArray(value)) { return value.map(deepClone); } if (value && typeof value === "object" && !(value instanceof Date) && !(value instanceof RegExp) && !(value instanceof Function) && !(value instanceof Map) && !(value instanceof Set) && !(value instanceof WeakMap) && !(value instanceof WeakSet) && !(value instanceof Error) && !(value instanceof Promise)) { const result = {}; for (const key in value) { result[key] = deepClone(value[key]); } return result; } return value; } const useDeepCopy = typeof options.deepCopy === "boolean" ? options.deepCopy : this.deepCopyEnabled; const skipValidation = options.skipValidation === void 0 ? this.defaultSkipValidation : options.skipValidation; options.deepCopy = useDeepCopy; options.skipValidation = skipValidation; const createOne = (index) => { const obj = {}; const validationErrors = []; for (const { name, generator, validator } of this.fields) { const val = generator(obj, index, options); obj[name] = useDeepCopy ? deepClone(val) : val; if (!skipValidation && validator) { const validationResult = validator(obj[name], name); if (!validationResult.success) { validationErrors.push(validationResult.errorMsg || `Validation failed for field "${name}"`); } } } if (validationErrors.length > 0 && !skipValidation) { throw new Error(`Validation errors: ${validationErrors.join("\n")}`); } return obj; }; function validateFields(obj, typeKeys2) { for (const key of typeKeys2) { if (!(key in obj)) { throw new Error(`Missing field "${key}" in built object.`); } } } function getTypeKeys() { return Object.keys({}) || []; } const typeKeys = getTypeKeys(); if (this.repeatCount === 1) { const obj = createOne(); if (!skipValidation && typeKeys.length > 0) { validateFields(obj, typeKeys); } return obj; } const arr = Array.from({ length: this.repeatCount }, (_, index) => createOne(index)); if (!skipValidation && typeKeys.length > 0 && Array.isArray(arr)) { for (const obj of arr) { validateFields(obj, typeKeys); } } return arr; } }; _MockBuilder.presets = {}; var MockBuilder = _MockBuilder; export { MockBuilder };