UNPKG

interface-forge

Version:

Gracefully generate testing data using TypeScript and Faker.js

192 lines (191 loc) 8.28 kB
/* 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; }