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
JavaScript
// 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
};