interface-forge
Version:
Gracefully generate testing data using TypeScript and Faker.js
192 lines (191 loc) • 8.28 kB
JavaScript
/* eslint-disable unicorn/no-new-array */
import { en, Faker } from '@faker-js/faker';
import { isIterator, isRecord } from '@tool-belt/type-predicates';
/*
* A reference to a function that returns a value of type `T`.
* */
class Ref {
args;
handler;
constructor({ args, handler }) {
this.handler = handler;
this.args = args;
}
callHandler() {
return this.handler(...this.args);
}
}
/**
* Builds a single object based on the factory's schema.
* This method generates an instance by applying the factory function and optionally merging
* the provided `kwargs` with the generated values. It's useful for creating a single, customized
* instance where specific properties can be overridden or added.
* @param kwargs Optional. An object containing properties to override or add to the generated instance.
* Each property in `kwargs` will replace or add to the properties generated by the factory function.
* @returns An instance of type `T`, generated and optionally modified according to the `kwargs` parameter.
*/
export class Factory extends Faker {
factory;
constructor(factory, options) {
super({
locale: options?.locale ?? en,
randomizer: options?.randomizer,
});
this.factory = factory;
}
/**
* Generates a batch of objects based on the factory's schema.
* This method allows for the creation of multiple instances at once, optionally applying specific
* properties to each instance through the `kwargs` parameter. It supports both individual property
* overrides for each instance in the batch or a single set of overrides applied to all instances.
* @param size The number of instances to generate in the batch.
* @param kwargs Optional. An object or an array of objects containing properties to override in the
* generated instances. If an array is provided, each object in the array corresponds
* to the overrides for the instance at the same index in the batch. If a single object
* is provided, its properties are applied to all instances in the batch.
* @returns An array of instances generated by the factory, with each instance optionally modified
* according to the `kwargs` parameter.
*/
batch = (size, kwargs) => {
if (kwargs) {
const generator = this.iterate(Array.isArray(kwargs) ? kwargs : [kwargs]);
return new Array(size)
.fill(null)
.map((_, i) => this.generate(i, generator.next().value));
}
return new Array(size).fill(null).map((_, i) => this.generate(i));
};
/**
* Builds a single object based on the factory's schema.
* This method generates an instance by applying the factory function and optionally merging
* the provided `kwargs` with the generated values. It's useful for creating a single, customized
* instance where specific properties can be overridden or added.
* @param kwargs Optional. An object containing properties to override or add to the generated instance.
* Each property in `kwargs` will replace or add to the properties generated by the factory function.
* @returns An instance of type `T`, generated and optionally modified according to the `kwargs` parameter.
*/
build = (kwargs) => {
return this.generate(0, kwargs);
};
/**
* Cycles through the values of an iterable indefinitely.
* This method creates a generator that iterates over each element of the provided iterable.
* Once it reaches the end of the iterable, it starts over from the beginning, allowing for infinite iteration.
* @template T The type of elements in the iterable.
* @param iterable An iterable object containing the elements to cycle through.
* @returns A Generator that yields elements from the iterable indefinitely.
*/
iterate(iterable) {
const values = iterableToArray(iterable);
return (function* () {
let counter = 0;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
while (true) {
const value = values[counter];
if (counter === values.length - 1) {
counter = 0;
}
else {
counter++;
}
yield value;
}
})();
}
/**
* Samples values randomly from an iterable.
* This method creates a generator that yields random values from the provided iterable.
* Each iteration randomly selects an element from the iterable, ensuring that the same value
* is not yielded consecutively unless the iterable contains a single element. This method
* allows for an infinite sequence of random values from the iterable, suitable for scenarios
* where random sampling with replacement is needed, but with a constraint to prevent immediate
* repetition of the same value.
* @template T The type of elements in the iterable.
* @param iterable An iterable object containing the elements to sample from.
* @returns A Generator that yields random elements from the iterable indefinitely, ensuring no immediate repetitions.
*/
sample(iterable) {
const values = iterableToArray(iterable);
return (function* () {
let lastValue = null;
let newValue = null;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
while (true) {
if (values.length <= 1) {
yield values[0];
}
lastValue = newValue;
while (newValue === lastValue) {
newValue =
values[Math.floor(Math.random() * values.length)];
}
yield newValue;
}
})();
}
/**
* Creates a reference to a function that can be used within the factory.
* This method allows for the encapsulation of a function and its arguments, enabling deferred execution.
* @template R The return type of the function.
* @template C The type of the function, constrained to functions that return `R`.
* @param handler The function to be encapsulated.
* @param args The arguments to be passed to the function upon invocation.
* @returns A `Ref` instance encapsulating the function and its arguments, allowing for deferred execution.
*/
use(handler, ...args) {
// @ts-expect-error, any and never clash
return new Ref({ args, handler });
}
generate(iteration, kwargs) {
const defaults = this.factory(this, iteration);
if (kwargs) {
return merge(this.parseValue(defaults), this.parseValue(kwargs));
}
return this.parseValue(defaults);
}
parseValue(value) {
if (value instanceof Ref) {
return value.callHandler();
}
if (isIterator(value)) {
return value.next().value;
}
if (isRecord(value)) {
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, this.parseValue(v)]));
}
return value;
}
}
/**
*
* @param iterable The iterable to be converted to an array.
* @returns An array containing the values of the iterable.
*/
function iterableToArray(iterable) {
const values = [];
for (const value of iterable) {
values.push(value);
}
return values;
}
/**
*
* @param target The target object to merge into
* @param {...any} sources The source objects to merge
* @returns T
*/
function merge(target, ...sources) {
const output = { ...target };
for (const source of sources.filter(Boolean)) {
for (const [key, value] of Object.entries(source)) {
const existingValue = Reflect.get(output, key);
if (isRecord(value) && isRecord(existingValue)) {
Reflect.set(output, key, merge(existingValue, value));
}
else {
Reflect.set(output, key, value);
}
}
}
return output;
}