rodmax/factory-t

View on GitHub
src/factory.ts

Summary

Maintainability
A
3 hrs
Test Coverage
import { FieldFactoryByKey, FieldFactoryContext } from './common';

export class FactoryT<D extends object, O = unknown> {
    private itemsCount: number = 1;

    constructor(private fieldFactoryByKey: FieldFactoryByKey<D, O>) {}

    public item(partial: Partial<D> = {}, options?: O): D {
        const builtKeys: Array<keyof D> = [];
        const keysStack: Array<keyof D> = [];
        const obj = {} as D;

        const inject = <K extends keyof D>(k: K) => {
            if (!builtKeys.includes(k)) {
                makeField(k);
            }
            return obj[k];
        };
        const ctx: FieldFactoryContext<D, O> = {
            index: this.itemsCount,
            inject,
            options,
        };

        const makeField = <K extends keyof D>(k: K) => {
            if (builtKeys.includes(k)) {
                return;
            }
            if (keysStack.includes(k)) {
                keysStack.push(k);
                throw new Error(
                    `circular dependency cause between fields: ${keysStack.join('->')}`,
                );
            }
            keysStack.push(k);
            obj[k] = (partial as object).hasOwnProperty(k)
                ? (partial[k] as D[K])
                : this.fieldFactoryByKey[k](ctx);
            keysStack.pop();
            builtKeys.push(k);
        };

        for (const k of this.dataKeys()) {
            makeField(k);
        }

        this.itemsCount++;
        (Object.keys(partial) as Array<keyof D>).forEach((k) => {
            if (this.fieldFactoryByKey[k]) {
                return;
            }
            // we can get here, if three conditions occur:
            // 1. k - optional field
            // 2. field factory not provided for this field(TS allow this only for optional fields)
            // 3. partial[k] was passed to item() method (TS should check valid type of this value)
            // In this case we just assign this value to result object
            // Source issue #110
            obj[k] = partial[k] as D[keyof D];
        });

        return obj;
    }

    public list(
        inParams: (
            | Pick<PossibleListParams<D>, 'count'>
            | Pick<PossibleListParams<D>, 'partials'>
            | Pick<PossibleListParams<D>, 'count' | 'partials'>
        ) &
            Pick<PossibleListParams<D>, 'partial'>,
        options?: O,
    ): D[] {
        const params = inParams as PossibleListParams<D>;

        if (params.partials) {
            if (params.partials.length === 0) {
                throw new Error(
                    `${this.list.name}() assertion error: "partials" array must be not empty`,
                );
            }
            if (params.count && params.count < params.partials.length) {
                throw new Error(
                    `${this.list.name}() assertion error: "count" param should be greater then "partials.length"`,
                );
            }
        }

        const count = isFinite(params.count) ? params.count : params.partials.length;
        const partials = params.partials || [];
        const defaultPartial = params.partial ?? {};

        const items: D[] = [];

        for (let i = 0; i < count; i++) {
            const item = this.item(partials[i] || defaultPartial, options);
            items.push(item);
        }

        return items;
    }

    public resetCount(): void {
        this.itemsCount = 1;
    }

    private dataKeys(): Array<keyof D> {
        return Object.keys(this.fieldFactoryByKey) as Array<keyof D>;
    }
}

interface PossibleListParams<T> {
    count: number;
    partials: Array<Partial<T>>;
    partial?: Partial<T>;
}