Skip to content

Commit

Permalink
feat: added ModelModifier
Browse files Browse the repository at this point in the history
  • Loading branch information
dereekb committed Jun 6, 2022
1 parent e852230 commit 118bde7
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 16 deletions.
1 change: 1 addition & 0 deletions packages/util/src/lib/model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './model';
export * from './model.copy';
export * from './model.conversion';
export * from './model.conversion.field';
export * from './model.modify';
99 changes: 99 additions & 0 deletions packages/util/src/lib/model/model.conversion.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { build } from './../value/build';
import { countPOJOKeys, KeyValueTypleValueFilter } from '../object';
import { modelFieldMapFunction, makeModelMapFunctions, modelFieldConversions } from './model.conversion';
import { copyField } from './model.conversion.field';
import { modifyModelMapFunctions } from './model.modify';

interface TestConversionModel {
name: string;
Expand Down Expand Up @@ -147,3 +148,101 @@ describe('modelFieldMapFunction()', () => {
});
});
});

describe('modifyModelMapFunctions()', () => {
it('should wrap the modify function', () => {
const result = modifyModelMapFunctions({
mapFunctions,
modifiers: [
{
modifyData: () => undefined,
modifyModel: () => undefined
}
]
});

expect(result).toBeDefined();
expect(result.from).toBeDefined();
expect(result.to).toBeDefined();
});

describe('function', () => {
describe('copy=false', () => {
it('should not copy the input model or data when applying modifiers.', () => {
const modifyDataName = '0';
const modifyModelName = '1';

const modifyMapFunctions = modifyModelMapFunctions({
mapFunctions,
modifiers: [
{
modifyData: (x) => (x.name = modifyDataName),
modifyModel: (x) => (x.name = modifyModelName)
}
],
copy: false // do not modify a copy
});

const inputModel = {
...defaultTestModel
};

expect(inputModel.name).toBe(defaultTestModel.name);

const data = modifyMapFunctions.to(inputModel);
expect(inputModel.name).toBe(modifyModelName);
expect(data).toBeDefined();
expect(data.name).toBe(modifyModelName);

const model = modifyMapFunctions.from(data);
expect(model).toBeDefined();
expect(data.name).toBe(modifyDataName);
expect(model.name).toBe(modifyDataName);
});
});

describe('conversions', () => {
let calledModifyData = false;
let calledModifyModel = false;

const modifyMapFunctions = modifyModelMapFunctions({
mapFunctions,
modifiers: [
{
modifyData: () => (calledModifyData = !calledModifyData),
modifyModel: () => (calledModifyModel = !calledModifyModel)
}
]
});

beforeEach(() => {
calledModifyData = false;
calledModifyModel = false;
});

it('should call the modifyData function of all input modifiers when converting a defined value', () => {
const data = modifyMapFunctions.to(defaultTestModel);
expect(data).toBeDefined();
expect(calledModifyModel).toBe(true);
expect(calledModifyData).toBe(false);

const model = modifyMapFunctions.from(data);
expect(model).toBeDefined();
expect(calledModifyModel).toBe(true);
expect(calledModifyData).toBe(true);
});

it('should not call all the modifyData function of all input modifiers when converting an undefined value', () => {
const data = modifyMapFunctions.to(undefined);
expect(data).toBeDefined();
expect(calledModifyModel).toBe(false);
expect(calledModifyData).toBe(false);

const model = modifyMapFunctions.from(undefined);
expect(model).toBeDefined();
expect(calledModifyModel).toBe(false);
expect(calledModifyData).toBe(false);
});
});
});
});
16 changes: 12 additions & 4 deletions packages/util/src/lib/model/model.conversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,18 @@ export function modelFieldConversions<V extends object, D extends object>(config
}

export type ModelFieldMapFunctions<I = unknown, O = unknown> = {
from: ModelFieldMapFromFunction<I, O>;
to: ModelFieldMapToFunction<I, O>;
readonly from: ModelFieldMapFromFunction<I, O>;
readonly to: ModelFieldMapToFunction<I, O>;
};

export type ModelFieldMapFunctionsConfig<I = unknown, O = unknown> = {
from: ModelFieldMapFromConfig<I, O>;
to: ModelFieldMapToConfig<I, O>;
readonly from: ModelFieldMapFromConfig<I, O>;
readonly to: ModelFieldMapToConfig<I, O>;
};

export type ModelFieldMapFunctionsWithDefaultsConfig<I = unknown, O = unknown> = {
readonly from: ModelFieldMapFromWithDefaultConfig<I, O>;
readonly to: ModelFieldMapToWithDefaultConfig<I, O>;
};

export function modelFieldMapFunctions<I = unknown, O = unknown>(config: ModelFieldMapFunctionsConfig<I, O>): ModelFieldMapFunctions<I, O> {
Expand Down Expand Up @@ -153,6 +158,9 @@ export type ModelFieldMapConfig<I, O> = XOR<ModelFieldMapMaybeTooConfig<I, O>, M
export type ModelFieldMapFromConfig<I = unknown, O = unknown> = ModelFieldMapConfig<O, I>;
export type ModelFieldMapToConfig<I = unknown, O = unknown> = ModelFieldMapConfig<I, O>;

export type ModelFieldMapFromWithDefaultConfig<I = unknown, O = unknown> = ModelFieldMapMaybeWithDefaultConfig<O, I>;
export type ModelFieldMapToWithDefaultConfig<I = unknown, O = unknown> = ModelFieldMapMaybeWithDefaultConfig<I, O>;

export type ModelFieldMapFunction<I = unknown, O = unknown> = MapFunction<Maybe<I>, O>;
export type ModelFieldMapFromFunction<I, O> = ModelFieldMapFunction<O, I>;
export type ModelFieldMapToFunction<I, O> = ModelFieldMapFunction<I, O>;
Expand Down
90 changes: 90 additions & 0 deletions packages/util/src/lib/model/model.modify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { MaybeMap } from '@dereekb/util';
import { ArrayOrValue, asArray } from '../array/array';
import { filterMaybeValues } from '../array/array.value';
import { Maybe } from '../value/maybe';
import { maybeMergeModifiers, ModifierFunction } from '../value/modifier';
import { ModelConversionOptions, ModelMapFunction, ModelMapFunctions } from './model.conversion';

export type ModelInputDataModifier<D extends object> = {
modifyData: ModifierFunction<D>;
};

export type ModelInputModelModifier<V extends object> = {
modifyModel: ModifierFunction<V>;
};

export type ModelModifier<V extends object, D extends object> = ModelInputModelModifier<V> & ModelInputDataModifier<D>;
export type PartialModelModifier<V extends object, D extends object> = Partial<MaybeMap<ModelModifier<V, D>>>;

export function maybeMergeModelModifiers<V extends object, D extends object>(input: ArrayOrValue<PartialModelModifier<V, D>>): PartialModelModifier<V, D> {
const modifiers = asArray(input);
const allModifyData = filterMaybeValues(modifiers.map((x) => x.modifyData));
const allModifyModel = filterMaybeValues(modifiers.map((x) => x.modifyModel));
const modifyData = maybeMergeModifiers(allModifyData);
const modifyModel = maybeMergeModifiers(allModifyModel);

return {
modifyData,
modifyModel
};
}

export interface ModifyModelMapFunctionsConfig<V extends object, D extends object> {
readonly mapFunctions: ModelMapFunctions<V, D>;
/**
* Partial model modifiers to use.
*/
readonly modifiers: ArrayOrValue<PartialModelModifier<V, D>>;
/**
* Provides a default value for both copyModel and copyData.
*/
readonly copy?: boolean;
/**
* Whether or not to copy the input model before applying modifiers.
*
* Defaults to true.
*/
readonly copyModel?: boolean;
/**
* Whether or not to copy the input data before applying modifiers.
*
* Defaults to true.
*/
readonly copyData?: boolean;
}

export function modifyModelMapFunctions<V extends object, D extends object>(config: ModifyModelMapFunctionsConfig<V, D>): ModelMapFunctions<V, D> {
const { copy, copyModel = copy, copyData = copy, mapFunctions, modifiers } = config;
const { from, to } = mapFunctions;
const { modifyData, modifyModel } = maybeMergeModelModifiers(modifiers);

const modifyFrom = modifyModelMapFunction(from, modifyData, copyData);
const modifyTo = modifyModelMapFunction(to, modifyModel, copyModel);

return {
from: modifyFrom,
to: modifyTo
};
}

/**
* Merges a ModifierFunction with a ModelMapFunction
*
* @param mapFn
* @param modifyModel
* @param copy
* @returns
*/
export function modifyModelMapFunction<I extends object, O extends object>(mapFn: ModelMapFunction<I, O>, modifyModel: Maybe<ModifierFunction<I>>, copy = true): ModelMapFunction<I, O> {
return modifyModel
? (input: Maybe<I>, target?: Maybe<Partial<O>>, options?: Maybe<ModelConversionOptions<I>>) => {
const inputToMap = copy && input != null ? { ...input } : input;

if (inputToMap != null) {
modifyModel(inputToMap);
}

return mapFn(inputToMap, target, options);
}
: mapFn;
}
65 changes: 53 additions & 12 deletions packages/util/src/lib/value/modifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,21 @@ export type ModifierKey = string;
*/
export type ModifierFunction<T> = (input: T) => void;

/**
* Retains a reference to a ModifierFunction
*/
export interface ModifierFunctionRef<T> {
readonly modify: ModifierFunction<T>;
}

/**
* A modifier that has a key and modify function.
*/
export interface Modifier<T> {
export interface Modifier<T> extends ModifierFunctionRef<T> {
/**
* Modifier key.
*/
readonly key: ModifierKey;

/**
*
*/
readonly modify: ModifierFunction<T>;
}

/**
Expand All @@ -40,6 +42,8 @@ export function modifier<T>(key: string, modify: ModifierFunction<T>): Modifier<
};
}

export const NOOP_MODIFIER: ModifierFunction<any> = () => undefined;

/**
* Map of Modifiers keyed by the modifier key.
*/
Expand Down Expand Up @@ -79,7 +83,7 @@ export function removeModifiers<T>(modifiers: ArrayOrValue<Modifier<T>>, map: Ma
}

export function modifierMapToFunction<T>(map: Maybe<ModifierMap<T>>): ModifierFunction<T> {
return maybeModifierMapToFunction(map) ?? (() => undefined);
return maybeModifierMapToFunction(map) ?? NOOP_MODIFIER;
}

/**
Expand All @@ -89,9 +93,46 @@ export function modifierMapToFunction<T>(map: Maybe<ModifierMap<T>>): ModifierFu
* @returns
*/
export function maybeModifierMapToFunction<T>(map: Maybe<ModifierMap<T>>): Maybe<ModifierFunction<T>> {
const fns: ModifierFunction<T>[] = [];
map?.forEach((x) => fns.push(x.modify));
return (input) => {
fns.forEach((fn) => fn(input));
};
let fn: Maybe<ModifierFunction<T>>;

if (map != null) {
const fns: ModifierFunction<T>[] = [];
map.forEach((x) => fns.push(x.modify));
fn = (input) => fns.forEach((fn) => fn(input));
}

return fn;
}

/**
* Merges all modifiers into a single function.
*
* @param map
* @returns
*/
export function mergeModifiers<T>(modifiers: ModifierFunction<T>[]): ModifierFunction<T> {
return maybeMergeModifiers(modifiers) ?? NOOP_MODIFIER;
}

/**
* Merges all modifiers into a single function. If not modifier functions are input, returns
*
* @param map
* @returns
*/
export function maybeMergeModifiers<T>(modifiers: Maybe<ModifierFunction<T>[]>): Maybe<ModifierFunction<T>> {
let result: Maybe<ModifierFunction<T>> = undefined;

if (modifiers != null) {
switch (modifiers.length) {
case 1:
result = modifiers[0];
break;
default:
result = (input) => (modifiers as ModifierFunction<T>[]).forEach((fn) => fn(input));
break;
}
}

return result;
}

0 comments on commit 118bde7

Please sign in to comment.