Last active
January 8, 2025 17:03
-
-
Save JonCatmull/88bc8bbf96ea91592229dfc0aa27f416 to your computer and use it in GitHub Desktop.
Build mock data easily
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* A `CompositionFunction` creates or updates one specific property on | |
* your mock data object. It returns a new `Builder` instance with that | |
* updated property value. | |
* | |
* @example | |
* // Suppose we have a builder for a User object | |
* // The `.username('SteveRogers')` is a composition function call: | |
* const steve = UserBuilder.username('SteveRogers').build(); | |
*/ | |
export type CompositionFunction<T, K extends keyof T> = (value: T[K]) => Builder<T>; | |
/** | |
* `CompositionFunctions<T>` is a dictionary of composition functions | |
* (one per property in `T`). | |
* | |
* Example: If T = { id: number; username: string }, | |
* this type has `id(value: number): Builder<T>` and `username(value: string): Builder<T>`. | |
*/ | |
export type CompositionFunctions<T> = Required<{ | |
[P in keyof T]: CompositionFunction<T, P>; | |
}>; | |
/** | |
* A helper type for the `buildMany` function, which can create an array of objects | |
* either from an array of partial overrides or by a numeric count of duplicates. | |
*/ | |
export type BuildManyFunction<T> = (arg: Partial<T>[] | number) => T[]; | |
/** | |
* A `Builder<T>` includes: | |
* - One composition function per property in `T` | |
* - A `_obj` property containing the current base/default object | |
* - A `.build()` method returning a finalized, read-only object | |
* - A `.buildMany()` method to produce an array of objects in bulk | |
*/ | |
export type Builder<T> = CompositionFunctions<T> & { | |
/** | |
* Read-only property with the “current” object state. | |
*/ | |
_obj: Readonly<T>; | |
/** | |
* Builds a single object from the current default/base values | |
* plus any overridden properties set via composition functions. | |
* | |
* @returns A read-only copy of the final object. | |
*/ | |
build(): Readonly<T>; | |
/** | |
* Builds an array of objects. | |
* - Pass an array of partial overrides to merge with the default/base object | |
* - Pass a number N to create N identical copies of the default/base object | |
* | |
* @example | |
* const multipleUsers = UserBuilder.buildMany([ | |
* { username: 'BruceBanner' }, | |
* { username: 'NatashaRomanoff' } | |
* ]); | |
*/ | |
buildMany: BuildManyFunction<T>; | |
}; | |
/** | |
* `CreateBuilderFunction` is a function type that takes a default value `T` | |
* and returns a `Builder<T>`. | |
* | |
* @example | |
* // Usage: | |
* export const UserBuilder: Builder<User> = createBuilder<User>(myDefaultUser); | |
*/ | |
export type CreateBuilderFunction = <T>(defaultValue: T) => Builder<T>; | |
/** | |
* Creates a composition function for a specific property (keyName) on `obj`. | |
* When invoked, it sets that property to the provided `value` and returns a new `Builder`. | |
* | |
* @param builderFunction The top-level factory function (createBuilder). | |
* @param obj The current mock data object. | |
* @param keyName The property name to set (e.g., 'username'). | |
*/ | |
function createCompositionFunction<T, K extends keyof T>( | |
builderFunction: CreateBuilderFunction, | |
obj: T, | |
keyName: K | |
): CompositionFunction<T, K> { | |
return (value: T[K]) => builderFunction<T>({ ...obj, [keyName]: value }); | |
} | |
/** | |
* Dynamically creates a dictionary of composition functions for each property in `obj`. | |
* For every key in `obj`, we create a function that sets that key and returns a new builder. | |
* | |
* @param builderFunction The top-level factory function (createBuilder). | |
* @param obj The current mock data object. | |
* @returns A `CompositionFunctions<T>` object where each property is a function. | |
*/ | |
function createCompositionFunctions<T extends {}>( | |
builderFunction: CreateBuilderFunction, | |
obj: T | |
): CompositionFunctions<T> { | |
const compositionFuncs: Partial<CompositionFunctions<T>> = {}; | |
// We use (keyof T)[] to assure TS that every key is truly a key of T | |
for (const key of Object.keys(obj) as (keyof T)[]) { | |
compositionFuncs[key] = createCompositionFunction(builderFunction, obj, key); | |
} | |
return compositionFuncs as CompositionFunctions<T>; | |
} | |
/** | |
* Creates a `.buildMany()` function for a given object. It can either: | |
* - Take an array of partial overrides and return an array of merged objects | |
* - Take a number N and return an array of N identical copies of the object | |
* | |
* @param obj The current mock data object. | |
* @returns A function that implements `.buildMany()`. | |
*/ | |
function createBuildManyFunction<T>(obj: T): BuildManyFunction<T> { | |
return (arg: Partial<T>[] | number) => { | |
if (Array.isArray(arg)) { | |
return arg.map((partialObj) => ({ | |
...obj, | |
...partialObj, | |
})); | |
} | |
// If arg is a number, return an array with `arg` copies of the object | |
return Array(arg).fill({ ...obj }); | |
}; | |
} | |
/** | |
* The main builder factory. Given a default object of type `T`, | |
* it returns a `Builder<T>` with composition methods for each property, | |
* plus `.build()` and `.buildMany()`. | |
* | |
* @param defaultValue The base/default object for your mock data. | |
* | |
* @example | |
* // Example usage with the `User` interface: | |
* import { User } from './user.interface'; | |
* | |
* const defaultUser: User = { | |
* id: 3000, | |
* username: 'TonyStark', | |
* email: '[email protected]', | |
* roleIds: [999], | |
* roleId: '999', | |
* depotIds: ['STARK_TOWER'], | |
* sso: false, | |
* }; | |
* | |
* export const UserBuilder: Builder<User> = createBuilder<User>(defaultUser); | |
* | |
* // Then in tests: | |
* const singleUser = UserBuilder.username('SteveRogers').build(); | |
* const multipleUsers = UserBuilder.buildMany([ | |
* { username: 'BruceBanner' }, | |
* { username: 'NatashaRomanoff', email: '[email protected]' }, | |
* ]); | |
*/ | |
export const createBuilder: CreateBuilderFunction = <T>(defaultValue: T): Builder<T> => { | |
// Make a read-only copy of the default object | |
const _obj: Readonly<T> = defaultValue; | |
return { | |
_obj, | |
build: () => _obj, | |
buildMany: createBuildManyFunction(_obj), | |
...createCompositionFunctions(createBuilder, _obj), | |
}; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment