Code improvements for options and cache

This commit is contained in:
Rene
2020-12-07 14:50:54 +01:00
parent 1435ee119b
commit bdb2babc83
16 changed files with 285 additions and 240 deletions
+1
View File
@@ -39,6 +39,7 @@ module.exports = {
'no-empty': ['error', { allowEmptyCatch: true }],
'no-cond-assign': ['error', 'except-parens'],
camelcase: ['error', { allow: ['^__', '^UNSAFE_'] }],
'prefer-destructuring': 'off',
'consistent-return': 'off',
'import/prefer-default-export': 'off',
'import/no-extraneous-dependencies': 'off',
+1
View File
@@ -9,6 +9,7 @@ const testServerLoaderPath = path.resolve(__dirname, './config/jest-test-server.
// https://jestjs.io/docs/en/configuration.html
const base = {
cache: false,
clearMocks: true,
collectCoverage: true,
coverageDirectory: './.coverage',
@@ -1,10 +1,9 @@
import {
optionsTemplateTypes as oTypes,
transform,
OptionsTemplate,
transformOptions,
OptionsTemplateValue,
OptionsAndOptionsTemplateValue,
OptionsAndOptionsTemplate,
OptionsWithOptionsTemplateValue,
OptionsWithOptionsTemplate,
Func,
} from 'support/options';
import { ResizeBehavior, OverflowBehavior, VisibilityBehavior, AutoHideBehavior, Options } from 'options';
@@ -13,9 +12,9 @@ const classNameAllowedValues: OptionsTemplateValue<string | null> = [oTypes.stri
const numberAllowedValues: OptionsTemplateValue<number> = oTypes.number;
const booleanNullAllowedValues: OptionsTemplateValue<boolean | null> = [oTypes.boolean, oTypes.null];
const stringArrayNullAllowedValues: OptionsTemplateValue<string | Array<string> | null> = [oTypes.string, oTypes.array, oTypes.null];
const booleanTrueTemplate: OptionsAndOptionsTemplateValue<boolean> = [true, oTypes.boolean];
const booleanFalseTemplate: OptionsAndOptionsTemplateValue<boolean> = [false, oTypes.boolean];
const callbackTemplate: OptionsAndOptionsTemplateValue<Func | null> = [null, [oTypes.function, oTypes.null]];
const booleanTrueTemplate: OptionsWithOptionsTemplateValue<boolean> = [true, oTypes.boolean];
const booleanFalseTemplate: OptionsWithOptionsTemplateValue<boolean> = [false, oTypes.boolean];
const callbackTemplate: OptionsWithOptionsTemplateValue<Func | null> = [null, [oTypes.function, oTypes.null]];
const resizeAllowedValues: OptionsTemplateValue<ResizeBehavior> = 'none both horizontal vertical';
const overflowBehaviorAllowedValues: OptionsTemplateValue<OverflowBehavior> = 'visible-hidden visible-scroll scroll hidden';
const scrollbarsVisibilityAllowedValues: OptionsTemplateValue<VisibilityBehavior> = 'visible hidden auto';
@@ -36,7 +35,7 @@ const scrollbarsAutoHideAllowedValues: OptionsTemplateValue<AutoHideBehavior> =
* Property "a" has a default value of 'default' and it can be a string or null
* Property "b" has a default value of 250 and it can be number
*/
const defaultOptionsWithTemplate: OptionsAndOptionsTemplate<Required<Options>> = {
const defaultOptionsWithTemplate: OptionsWithOptionsTemplate<Required<Options>> = {
className: ['os-theme-dark', classNameAllowedValues], // null || string
resize: ['none', resizeAllowedValues], // none || both || horizontal || vertical || n || b || h || v
sizeAutoCapable: booleanTrueTemplate, // true || false
@@ -84,5 +83,4 @@ const defaultOptionsWithTemplate: OptionsAndOptionsTemplate<Required<Options>> =
},
};
export const optionsTemplate: OptionsTemplate<Required<Options>> = transform(defaultOptionsWithTemplate, true);
export const defaultOptions: Options = transform(defaultOptionsWithTemplate);
export const { _template: optionsTemplate, _options: defaultOptions } = transformOptions(defaultOptionsWithTemplate);
@@ -1,4 +1,4 @@
import { validate, assignDeep } from 'support';
import { validateOptions, assignDeep } from 'support';
import { Options, optionsTemplate } from 'options';
import { TargetElement } from 'overlayscrollbars';
import { Environment } from 'environment';
@@ -32,11 +32,11 @@ export class OverlayScrollbars {
#instanceVars: OverlayScrollbarsInstanceVars = {
_setOptions(newOptions: Options): Options {
const { _currentOptions } = this;
const { validated } = validate(newOptions, optionsTemplate, _currentOptions, true);
const { _validated } = validateOptions(newOptions, optionsTemplate, _currentOptions, true);
this._currentOptions = assignDeep({}, _currentOptions, validated);
this._currentOptions = assignDeep({}, _currentOptions, _validated);
return validated;
return _validated;
},
};
@@ -10,6 +10,11 @@ import {
TRBL,
equalTRBL,
createCache,
optionsTemplateTypes as oTypes,
transformOptions,
validateOptions,
OptionsTemplateValue,
assignDeep,
} from 'support';
import { Lifecycle } from 'overlayscrollbars/lifecycles';
import { getEnvironment, Environment } from 'environment';
@@ -18,17 +23,25 @@ import { createTrinsicObserver } from 'overlayscrollbars/observers/TrinsicObserv
export type OverflowBehavior = 'hidden' | 'scroll' | 'visible-hidden' | 'visible-scroll';
export interface StructureLifecycleOptions {
_paddingAbsolute?: boolean;
_overflowBehavior?: {
x: OverflowBehavior;
y: OverflowBehavior;
paddingAbsolute?: boolean;
overflowBehavior?: {
x?: OverflowBehavior;
y?: OverflowBehavior;
};
}
interface StructureLifecycleCache {
padding: TRBL;
}
const overflowBehaviorAllowedValues: OptionsTemplateValue<OverflowBehavior> = 'visible-hidden visible-scroll scroll hidden';
const { _template: optionsTemplate, _options: defaultOptions } = transformOptions<Required<StructureLifecycleOptions>>({
paddingAbsolute: [false, oTypes.boolean],
overflowBehavior: {
x: ['scroll', overflowBehaviorAllowedValues],
y: ['scroll', overflowBehaviorAllowedValues],
},
});
const classNameHost = 'os-host';
const classNameViewport = 'os-viewport';
const classNameContent = 'os-content';
@@ -37,7 +50,13 @@ const classNameViewportScrollbarStyling = `${classNameViewport}-scrollbar-styled
const cssMarginEnd = cssProperty('margin-inline-end');
const cssBorderEnd = cssProperty('border-inline-end');
export const createStructureLifecycle = (target: HTMLElement, options?: StructureLifecycleOptions): Lifecycle<StructureLifecycleOptions> => {
export const createStructureLifecycle = (target: HTMLElement, initialOptions?: StructureLifecycleOptions): Lifecycle<StructureLifecycleOptions> => {
const options: Required<StructureLifecycleOptions> = assignDeep(
{},
defaultOptions,
validateOptions<StructureLifecycleOptions>(initialOptions || {}, optionsTemplate)._validated
);
const destructFns: (() => any)[] = [];
const env: Environment = getEnvironment();
const scrollbarsOverlaid = env._nativeScrollbarIsOverlaid;
@@ -69,9 +88,13 @@ export const createStructureLifecycle = (target: HTMLElement, options?: Structur
destructFns.push(createTrinsicObserver(target, onTrinsicChanged));
return {
_options() {
_options(newOptions?: StructureLifecycleOptions) {
// eslint-disable-next-line
console.log('_options');
console.log('_options', newOptions);
},
_update(force?: boolean) {
// eslint-disable-next-line
console.log('_options', force);
},
_destruct() {
runEach(destructFns);
@@ -2,5 +2,6 @@ import { PlainObject } from 'typings';
export interface Lifecycle<T = PlainObject> {
_options(options?: T): void;
_update(force?: boolean): void;
_destruct(): void;
}
+5 -3
View File
@@ -19,7 +19,7 @@ export type CacheUpdateFunction<T, P extends keyof T> = (current?: T[P], previou
export type CacheEqualFunction<T, P extends keyof T> = (a?: T[P], b?: T[P]) => boolean;
export type CacheChanged<T> = {
[P in keyof T]: boolean;
[P in keyof T]?: T[P];
};
export type CacheUpdateInfo<T> = {
@@ -42,7 +42,7 @@ export type CacheUpdateInfo<T> = {
* If no equal function is passed a shallow comparison is carried out between the values.
*
* @returns A function which can be called with wither one ar an array of properties which shall be updated. Optionally it can be called with the force param.
* This function returns a object which contains all cache properties as booleans which indicate whether the corresponding cache values really changed or not.
* This function returns a object which contains all changed cache properties, if a property isn't in this object it means that it didn't change.
*/
export const createCache = <T>(cacheUpdateInfo: CacheUpdateInfo<T>): ((propsToUpdate?: PropsToUpdate<T>, force?: boolean) => CacheChanged<T>) => {
const cache: Cache<T> = {} as T;
@@ -64,7 +64,9 @@ export const createCache = <T>(cacheUpdateInfo: CacheUpdateInfo<T>): ((propsToUp
const result: CacheChanged<T> = {} as CacheChanged<T>;
each(allProps, (prop: keyof T) => {
result[prop] = !!(cache[prop]._changed || force);
if (cache[prop]._changed || force) {
result[prop] = cache[prop]._current;
}
cache[prop]._changed = false;
});
@@ -20,16 +20,16 @@ export type OptionsTemplate<T extends Required<T>> = {
: never;
};
export type OptionsValidatedResult<T> = {
readonly foreign: PlainObject;
readonly validated: T;
readonly _foreign: PlainObject;
readonly _validated: T;
};
// Options With Options Template Typings:
export type OptionsAndOptionsTemplateValue<T extends OptionsTemplateNativeTypes> = [T, OptionsTemplateValue<T>];
export type OptionsAndOptionsTemplate<T extends Required<T>> = {
export type OptionsWithOptionsTemplateValue<T extends OptionsTemplateNativeTypes> = [T, OptionsTemplateValue<T>];
export type OptionsWithOptionsTemplate<T extends Required<T>> = {
[P in keyof T]: PlainObject extends T[P]
? OptionsAndOptionsTemplate<Required<T[P]>>
? OptionsWithOptionsTemplate<Required<T[P]>>
: T[P] extends OptionsTemplateNativeTypes
? OptionsAndOptionsTemplateValue<T[P]>
? OptionsWithOptionsTemplateValue<T[P]>
: never;
};
type OptionsTemplateTypeMap = {
@@ -1,32 +1,37 @@
import { OptionsTemplate, OptionsAndOptionsTemplate, OptionsTemplateTypes } from 'support/options';
import { OptionsTemplate, OptionsWithOptionsTemplate, OptionsTemplateTypes } from 'support/options';
import { PlainObject } from 'typings';
import { isArray, isObject } from 'support/utils/types';
import { isArray } from 'support/utils/types';
import { each, keys } from 'support/utils';
export interface OptionsWithOptionsTemplateTransformation<T extends Required<T>> {
_template: OptionsTemplate<T>;
_options: T;
}
/**
* Transforms the given OptionsAndOptionsTemplate<T> object to its corresponding generic (T) Object or its corresponding Template object.
* @param optionsWithOptionsTemplate The OptionsAndOptionsTemplate<T> object which shall be converted.
* @param toTemplate True if the given OptionsAndOptionsTemplate<T> shall be converted to its corresponding Template object.
* Transforms the given OptionsWithOptionsTemplate<T> object to its corresponding generic (T) Object or its corresponding Template object.
* @param optionsWithOptionsTemplate The OptionsWithOptionsTemplate<T> object which shall be converted.
* @param toTemplate True if the given OptionsWithOptionsTemplate<T> shall be converted to its corresponding Template object.
*/
export function transform<T extends Required<T>>(optionsWithOptionsTemplate: OptionsAndOptionsTemplate<T>): T;
export function transform<T extends Required<T>>(
optionsWithOptionsTemplate: OptionsAndOptionsTemplate<T>,
toTemplate: true | void
): OptionsTemplate<T>;
export function transform<T extends Required<T>>(
optionsWithOptionsTemplate: OptionsAndOptionsTemplate<T>,
toTemplate?: true | void
): OptionsTemplate<T> | T {
const result: any = {};
export function transformOptions<T extends Required<T>>(
optionsWithOptionsTemplate: OptionsWithOptionsTemplate<T>
): OptionsWithOptionsTemplateTransformation<T> {
const result: any = {
_template: {},
_options: {},
};
each(keys(optionsWithOptionsTemplate), (key: Extract<keyof T, string>) => {
const val: PlainObject | OptionsTemplateTypes | Array<OptionsTemplateTypes> = optionsWithOptionsTemplate[key];
/* istanbul ignore else */
if (isArray(val)) {
result[key] = val[toTemplate ? 1 : 0];
} else if (isObject(val)) {
result[key] = transform(val as OptionsAndOptionsTemplate<typeof val>, toTemplate);
result._template[key] = val[1];
result._options[key] = val[0];
} else {
// if (isObject(val))
const tmpResult = transformOptions(val as OptionsWithOptionsTemplate<typeof val>);
result._template[key] = tmpResult._template;
result._options[key] = tmpResult._options;
}
});
@@ -61,8 +61,8 @@ const validateRecursive = <T extends PlainObject>(
// if the template has a object as value, it means that the options are complex (verschachtelt)
if (templateIsComplex && isPlainObject(optionsValue)) {
const validatedResult = validateRecursive(optionsValue, templateValue as PlainObject, optionsDiffValue, doWriteErrors, propPrefix + prop);
validatedOptions[prop] = validatedResult.validated;
optionsCopy[prop] = validatedResult.foreign as any;
validatedOptions[prop] = validatedResult._validated;
optionsCopy[prop] = validatedResult._foreign as any;
each([optionsCopy, validatedOptions], (value) => {
if (isEmptyObject(value[prop])) {
@@ -118,8 +118,8 @@ const validateRecursive = <T extends PlainObject>(
});
return {
foreign: optionsCopy,
validated: validatedOptions,
_foreign: optionsCopy,
_validated: validatedOptions,
};
};
@@ -140,7 +140,7 @@ const validateRecursive = <T extends PlainObject>(
* Without the optionsDiff object the returned validated object would be: { a: 'a', b: 'b', c: 'c' }
* @param doWriteErrors True if errors shall be logged into the console, false otherwise.
*/
const validate = <T extends PlainObject>(
const validateOptions = <T extends PlainObject>(
options: T,
template: OptionsTemplate<Required<T>>,
optionsDiff?: T,
@@ -158,7 +158,7 @@ const validate = <T extends PlainObject>(
return validateRecursive<T>(options, template, optionsDiff || ({} as T), doWriteErrors || false);
};
export { validate, optionsTemplateTypes };
export { validateOptions, optionsTemplateTypes };
type OptionsTemplateTypesDictionary = {
readonly boolean: OptionsTemplateType<boolean>;
@@ -1,9 +1,9 @@
import { validate } from 'support/options';
import { validateOptions } from 'support/options';
import { defaultOptions, optionsTemplate } from 'options';
describe('options', () => {
test('default options matching the options template', () => {
const { validated } = validate(defaultOptions, optionsTemplate);
expect(validated).toEqual(defaultOptions);
const { _validated } = validateOptions(defaultOptions, optionsTemplate);
expect(_validated).toEqual(defaultOptions);
});
});
@@ -15,45 +15,51 @@ const createUpdater = <T>(updaterReturn: (i: number) => T) => {
describe('cache', () => {
describe('createCache', () => {
test('creates and updates simple cache', () => {
interface Test {
number: number;
boolean: boolean;
string: string;
object: {};
}
const [updateNumberFn, updateNumber] = createUpdater<number>((i) => i);
const [updateBooleanFn, updateBoolean] = createUpdater<boolean>((i) => !!(i % 2));
const [updateStringFn, updateString] = createUpdater<string>((i) => `${i}`);
const [updateObjFn, updateObj] = createUpdater<object>((i) => ({ [i]: i }));
const updateCache = createCache({
const updateCache = createCache<Test>({
number: updateNumber,
boolean: updateBoolean,
string: updateString,
object: updateObj,
});
expect(updateCache('number').number).toBe(true);
expect(updateCache('number').number).toBe(1);
expect(updateNumberFn).toHaveBeenCalledTimes(1);
expect(updateNumberFn).toHaveBeenCalledWith(undefined, undefined);
expect(updateCache('number').number).toBe(true);
expect(updateCache('number').number).toBe(2);
expect(updateNumberFn).toHaveBeenCalledTimes(2);
expect(updateNumberFn).toHaveBeenCalledWith(1, undefined);
expect(updateCache('number').number).toBe(true);
expect(updateCache('number').number).toBe(3);
expect(updateNumberFn).toHaveBeenCalledTimes(3);
expect(updateNumberFn).toHaveBeenCalledWith(2, 1);
let { string, boolean, object, number } = updateCache('number');
expect(string).toBe(false);
expect(boolean).toBe(false);
expect(object).toBe(false);
expect(number).toBe(true);
expect(string).toBe(undefined);
expect(boolean).toBe(undefined);
expect(object).toBe(undefined);
expect(number).toBe(4);
expect(updateBooleanFn).not.toHaveBeenCalled();
expect(updateStringFn).not.toHaveBeenCalled();
expect(updateObjFn).not.toHaveBeenCalled();
({ string, boolean, object, number } = updateCache(['string', 'boolean', 'object']));
expect(string).toBe(true);
expect(boolean).toBe(true);
expect(object).toBe(true);
expect(number).toBe(false);
expect(string).toBe('1');
expect(boolean).toBe(!!(1 % 2));
expect(object).toEqual({ 1: 1 });
expect(number).toBe(undefined);
expect(updateBooleanFn).toHaveBeenCalledTimes(1);
expect(updateBooleanFn).toHaveBeenCalledWith(undefined, undefined);
@@ -86,10 +92,10 @@ describe('cache', () => {
updateCache(['string', 'boolean', 'object']);
({ string, boolean, object, number } = updateCache());
expect(string).toBe(true);
expect(boolean).toBe(true);
expect(object).toBe(true);
expect(number).toBe(true);
expect(string).toBe('5');
expect(boolean).toBe(!!(5 % 2));
expect(object).toEqual({ 5: 5 });
expect(number).toBe(5);
expect(updateBooleanFn).toHaveBeenCalledTimes(5);
expect(updateStringFn).toHaveBeenCalledTimes(5);
@@ -103,14 +109,17 @@ describe('cache', () => {
number: updateNumber,
});
expect(updateCache('number').number).toBe(true);
expect(updateCache('number').number).toBe(0);
expect(updateNumberFn).toHaveBeenCalledWith(undefined, undefined);
expect(updateCache('number').number).toBe(false);
expect(updateCache('number').number).toBe(undefined);
expect(updateNumberFn).toHaveBeenCalledWith(0, undefined);
expect(updateCache('number').number).toBe(false);
expect(updateCache('number').number).toBe(undefined);
expect(updateNumberFn).toHaveBeenCalledWith(0, 0);
const changed = updateCache('number');
expect(Object.prototype.hasOwnProperty.call(changed, 'changed')).toBe(false);
});
test('doesnt update if nothing changes with non primitives', () => {
@@ -127,26 +136,29 @@ describe('cache', () => {
],
});
expect(updateCache('constObj').constObj).toBe(true);
expect(updateCache('constObj').constObj).toBe(constObj);
expect(updateConstObjFn).toHaveBeenCalledWith(undefined, undefined);
expect(updateCache('constObj').constObj).toBe(false);
expect(updateCache('constObj').constObj).toBe(undefined);
expect(updateConstObjFn).toHaveBeenCalledWith(constObj, undefined);
expect(updateCache('constObj').constObj).toBe(false);
expect(updateCache('constObj').constObj).toBe(undefined);
expect(updateConstObjFn).toHaveBeenCalledWith(constObj, constObj);
expect(Object.prototype.hasOwnProperty.call(updateCache('constObj'), 'constObj')).toBe(false);
expect(updateCache('similarObj').similarObj).toBe(true);
expect(updateCache('similarObj').similarObj).toEqual(constObj);
expect(updateSimilarObjFn).toHaveBeenCalledWith(undefined, undefined);
expect(updateCache('similarObj').similarObj).toBe(true);
expect(updateCache('similarObj').similarObj).toEqual(constObj);
expect(updateSimilarObjFn).toHaveBeenCalledWith(constObj, undefined);
expect(updateCache('similarObj').similarObj).toBe(true);
expect(updateCache('similarObj').similarObj).toEqual(constObj);
expect(updateSimilarObjFn).toHaveBeenCalledWith(constObj, constObj);
expect(Object.prototype.hasOwnProperty.call(updateCache('similarObj'), 'similarObj')).toBe(true);
expect(updateCache('comparisonObj').comparisonObj).toBe(true);
expect(updateCache('comparisonObj').comparisonObj).toEqual(constObj);
expect(updateComparisonObjFn).toHaveBeenCalledWith(undefined, undefined);
expect(updateCache('comparisonObj').comparisonObj).toBe(false);
expect(updateCache('comparisonObj').comparisonObj).toBe(undefined);
expect(updateComparisonObjFn).toHaveBeenCalledWith(constObj, undefined);
expect(updateCache('comparisonObj').comparisonObj).toBe(false);
expect(updateCache('comparisonObj').comparisonObj).toBe(undefined);
expect(updateComparisonObjFn).toHaveBeenCalledWith(constObj, constObj);
expect(Object.prototype.hasOwnProperty.call(updateCache('comparisonObj'), 'comparisonObj')).toBe(false);
});
test('updates definitely with force', () => {
@@ -155,13 +167,13 @@ describe('cache', () => {
number: updateNumber,
});
expect(updateCache('number', true).number).toBe(true);
expect(updateCache('number', true).number).toBe(0);
expect(updateNumberFn).toHaveBeenCalledWith(undefined, undefined);
expect(updateCache('number', true).number).toBe(true);
expect(updateCache('number', true).number).toBe(0);
expect(updateNumberFn).toHaveBeenCalledWith(0, undefined);
expect(updateCache('number', true).number).toBe(true);
expect(updateCache('number', true).number).toBe(0);
expect(updateNumberFn).toHaveBeenCalledWith(0, 0);
});
@@ -173,19 +185,21 @@ describe('cache', () => {
number: [updateNumber, () => true],
});
expect(updateCache('string').string).toBe(true);
expect(updateCache('string').string).toBe('hi');
expect(updateStringFn).toHaveBeenCalledWith(undefined, undefined);
expect(updateCache('string').string).toBe(true);
expect(updateCache('string').string).toBe('hi');
expect(updateStringFn).toHaveBeenCalledWith('hi', undefined);
expect(updateCache('string').string).toBe(true);
expect(updateCache('string').string).toBe('hi');
expect(updateStringFn).toHaveBeenCalledWith('hi', 'hi');
expect(Object.prototype.hasOwnProperty.call(updateCache('string'), 'string')).toBe(true);
expect(updateCache('number').number).toBe(false);
expect(updateCache('number').number).toBe(undefined);
expect(updateNumberFn).toHaveBeenCalledWith(undefined, undefined);
expect(updateCache('number').number).toBe(false);
expect(updateCache('number').number).toBe(undefined);
expect(updateNumberFn).toHaveBeenCalledWith(1, undefined);
expect(updateCache('number').number).toBe(false);
expect(updateCache('number').number).toBe(undefined);
expect(updateNumberFn).toHaveBeenCalledWith(2, 1);
expect(Object.prototype.hasOwnProperty.call(updateCache('number'), 'number')).toBe(false);
});
});
});
@@ -1,5 +1,5 @@
import { PlainObject } from 'typings';
import { optionsTemplateTypes as oTypes, transform, OptionsTemplate, OptionsAndOptionsTemplate } from 'support/options';
import { optionsTemplateTypes as oTypes, transformOptions, OptionsTemplate, OptionsWithOptionsTemplate } from 'support/options';
type TestOptionsObj = { propA: 'propA'; null: null };
type TestOptionsEnum = 'A' | 'B' | 'C';
@@ -51,7 +51,7 @@ const optionsTemplate: OptionsTemplate<Required<TestOptions>> = {
func: oTypes.function,
};
const optionsAndOptionsTemplate: OptionsAndOptionsTemplate<Required<TestOptions>> = {
const TestOptionsWithOptionsTemplate: OptionsWithOptionsTemplate<Required<TestOptions>> = {
str: [options.str, optionsTemplate.str],
strArrNull: [options.strArrNull, optionsTemplate.strArrNull],
nullbool: [options.nullbool, optionsTemplate.nullbool],
@@ -68,10 +68,10 @@ const optionsAndOptionsTemplate: OptionsAndOptionsTemplate<Required<TestOptions>
describe('options and options template object transformation', () => {
test('transforms correctly into options object', () => {
expect(transform(optionsAndOptionsTemplate)).toEqual(options);
expect(transformOptions(TestOptionsWithOptionsTemplate)._options).toEqual(options);
});
test('transforms correctly into template object', () => {
expect(transform(optionsAndOptionsTemplate, true)).toEqual(optionsTemplate);
expect(transformOptions(TestOptionsWithOptionsTemplate)._template).toEqual(optionsTemplate);
});
});
@@ -1,4 +1,4 @@
import { validate, optionsTemplateTypes as oTypes, OptionsTemplate } from 'support/options';
import { validateOptions, optionsTemplateTypes as oTypes, OptionsTemplate } from 'support/options';
import { assignDeep, isEmptyObject } from 'support/utils';
type TestOptionsObj = { propA: 'propA'; null: null };
@@ -56,41 +56,41 @@ describe('options validation', () => {
foreignDeep: { a: 'A', b: 'B' },
};
const modifiedOptions = assignDeep({}, options, { nested: foreignObj }, foreignObj);
const result = validate(modifiedOptions, template);
const { validated } = result;
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(validated).toEqual(options);
expect(_validated).toEqual(options);
});
test('passed objects arent mutated', () => {
const clonedOptions = assignDeep({}, options);
validate(clonedOptions, template, clonedOptions);
validateOptions(clonedOptions, template, clonedOptions);
expect(clonedOptions).toEqual(options);
});
test('passed object isnt returned object', () => {
const clonedOptions = assignDeep({}, options);
const result = validate(clonedOptions, template);
const result = validateOptions(clonedOptions, template);
expect(result.validated).not.toBe(clonedOptions);
expect(result._validated).not.toBe(clonedOptions);
});
});
describe('foreign property return', () => {
test('return no foreign property', () => {
const result = validate(options, template);
const result = validateOptions(options, template);
expect(isEmptyObject(result.foreign)).toBe(true);
expect(isEmptyObject(result._foreign)).toBe(true);
});
test('return signle non-object foreign property', () => {
const foreignObj = { foreignProp: 'foreign' };
const modifiedOptions = assignDeep({}, options, foreignObj);
const result = validate(modifiedOptions, template);
const { foreign } = result;
const result = validateOptions(modifiedOptions, template);
const { _foreign } = result;
expect(foreign).toEqual(foreignObj);
expect(_foreign).toEqual(foreignObj);
});
test('return complex foreign properties', () => {
@@ -99,10 +99,10 @@ describe('options validation', () => {
foreignDeep: { a: 'A', b: 'B' },
};
const modifiedOptions = assignDeep({}, options, foreignObj);
const result = validate(modifiedOptions, template);
const { foreign } = result;
const result = validateOptions(modifiedOptions, template);
const { _foreign } = result;
expect(foreign).toEqual(foreignObj);
expect(_foreign).toEqual(foreignObj);
});
test('return nested complex foreign properties', () => {
@@ -111,24 +111,24 @@ describe('options validation', () => {
foreignDeep: { a: 'A', b: 'B' },
};
const modifiedOptions = assignDeep({}, options, { nested: foreignObj }, foreignObj);
const result = validate(modifiedOptions, template);
const { foreign } = result;
const result = validateOptions(modifiedOptions, template);
const { _foreign } = result;
expect(foreign.nested).toEqual(foreignObj);
delete foreign.nested;
expect(foreign).toEqual(foreignObj);
expect(_foreign.nested).toEqual(foreignObj);
delete _foreign.nested;
expect(_foreign).toEqual(foreignObj);
});
});
describe('diff property return', () => {
test('one value changed', () => {
const modifiedOptions = assignDeep({}, options, { str: 'newvaluetest' });
const result = validate(modifiedOptions, template, options);
const { validated } = result;
const result = validateOptions(modifiedOptions, template, options);
const { _validated } = result;
expect(validated.str).toBe('newvaluetest');
delete validated.str;
expect(isEmptyObject(validated)).toBe(true);
expect(_validated.str).toBe('newvaluetest');
delete _validated.str;
expect(isEmptyObject(_validated)).toBe(true);
});
test('multiple values changed', () => {
@@ -136,42 +136,42 @@ describe('options validation', () => {
str: 'newvaluetest',
nullbool: null,
});
const result = validate(modifiedOptions, template, options);
const { validated } = result;
const result = validateOptions(modifiedOptions, template, options);
const { _validated } = result;
expect(validated.str).toBe('newvaluetest');
expect(validated.nullbool).toBe(null);
delete validated.str;
delete validated.nullbool;
expect(isEmptyObject(validated)).toBe(true);
expect(_validated.str).toBe('newvaluetest');
expect(_validated.nullbool).toBe(null);
delete _validated.str;
delete _validated.nullbool;
expect(isEmptyObject(_validated)).toBe(true);
});
test('one nested value changed', () => {
const modifiedOptions = assignDeep({}, options, { nested: { num: -1293 } });
const result = validate(modifiedOptions, template, options);
const { validated } = result;
const result = validateOptions(modifiedOptions, template, options);
const { _validated } = result;
expect(validated.nested?.num).toBe(-1293);
delete validated.nested?.num;
expect(isEmptyObject(validated.nested)).toBe(true);
delete validated.nested;
expect(isEmptyObject(validated)).toBe(true);
expect(_validated.nested?.num).toBe(-1293);
delete _validated.nested?.num;
expect(isEmptyObject(_validated.nested)).toBe(true);
delete _validated.nested;
expect(isEmptyObject(_validated)).toBe(true);
});
test('multiple nested values changed', () => {
const modifiedOptions = assignDeep({}, options, {
nested: { num: -1293, abc: 'C' },
});
const result = validate(modifiedOptions, template, options);
const { validated } = result;
const result = validateOptions(modifiedOptions, template, options);
const { _validated } = result;
expect(validated.nested?.num).toBe(-1293);
expect(validated.nested?.abc).toBe('C');
delete validated.nested?.num;
delete validated.nested?.abc;
expect(isEmptyObject(validated.nested)).toBe(true);
delete validated.nested;
expect(isEmptyObject(validated)).toBe(true);
expect(_validated.nested?.num).toBe(-1293);
expect(_validated.nested?.abc).toBe('C');
delete _validated.nested?.num;
delete _validated.nested?.abc;
expect(isEmptyObject(_validated.nested)).toBe(true);
delete _validated.nested;
expect(isEmptyObject(_validated)).toBe(true);
});
test('various values changed', () => {
@@ -182,22 +182,22 @@ describe('options validation', () => {
abc: 'C',
nested: { num: -1293, abc: 'C' },
});
const result = validate(modifiedOptions, template, options);
const { validated } = result;
const result = validateOptions(modifiedOptions, template, options);
const { _validated } = result;
expect(validated.str).toBe('newstrvalue');
expect(validated.func).toBe(newFunc);
expect(validated.abc).toBe('C');
delete validated.str;
delete validated.func;
delete validated.abc;
expect(validated.nested?.num).toBe(-1293);
expect(validated.nested?.abc).toBe('C');
delete validated.nested?.num;
delete validated.nested?.abc;
expect(isEmptyObject(validated.nested)).toBe(true);
delete validated.nested;
expect(isEmptyObject(validated)).toBe(true);
expect(_validated.str).toBe('newstrvalue');
expect(_validated.func).toBe(newFunc);
expect(_validated.abc).toBe('C');
delete _validated.str;
delete _validated.func;
delete _validated.abc;
expect(_validated.nested?.num).toBe(-1293);
expect(_validated.nested?.abc).toBe('C');
delete _validated.nested?.num;
delete _validated.nested?.abc;
expect(isEmptyObject(_validated.nested)).toBe(true);
delete _validated.nested;
expect(isEmptyObject(_validated)).toBe(true);
});
test('various values changed with foreign properties', () => {
@@ -218,40 +218,40 @@ describe('options validation', () => {
foreignObj,
{ nested: foreignObj }
);
const result = validate(modifiedOptions, template, options);
const { validated } = result;
const result = validateOptions(modifiedOptions, template, options);
const { _validated } = result;
expect(validated.str).toBe('newstrvalue');
expect(validated.func).toBe(newFunc);
expect(validated.abc).toBe('C');
delete validated.str;
delete validated.func;
delete validated.abc;
expect(validated.nested?.num).toBe(-1293);
expect(validated.nested?.abc).toBe('C');
delete validated.nested?.num;
delete validated.nested?.abc;
expect(isEmptyObject(validated.nested)).toBe(true);
delete validated.nested;
expect(isEmptyObject(validated)).toBe(true);
expect(_validated.str).toBe('newstrvalue');
expect(_validated.func).toBe(newFunc);
expect(_validated.abc).toBe('C');
delete _validated.str;
delete _validated.func;
delete _validated.abc;
expect(_validated.nested?.num).toBe(-1293);
expect(_validated.nested?.abc).toBe('C');
delete _validated.nested?.num;
delete _validated.nested?.abc;
expect(isEmptyObject(_validated.nested)).toBe(true);
delete _validated.nested;
expect(isEmptyObject(_validated)).toBe(true);
});
});
describe('value validity', () => {
test('single value doesnt match template', () => {
const modifiedOptions = assignDeep({}, options, { str: 1 });
const result = validate(modifiedOptions, template);
const { validated } = result;
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(validated).not.toHaveProperty('str');
expect(_validated).not.toHaveProperty('str');
});
test('single enum value doesnt match template', () => {
const modifiedOptions = assignDeep({}, options, { abc: 'testval' });
const result = validate(modifiedOptions, template);
const { validated } = result;
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(validated).not.toHaveProperty('abc');
expect(_validated).not.toHaveProperty('abc');
});
test('multiple values dont match template', () => {
@@ -260,51 +260,51 @@ describe('options validation', () => {
abc: 'testval',
nullbool: 'string',
});
const result = validate(modifiedOptions, template);
const { validated } = result;
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(validated).not.toHaveProperty('str');
expect(validated).not.toHaveProperty('abc');
expect(validated).not.toHaveProperty('nullbool');
expect(_validated).not.toHaveProperty('str');
expect(_validated).not.toHaveProperty('abc');
expect(_validated).not.toHaveProperty('nullbool');
});
test('single nested value dont match template', () => {
const modifiedOptions = assignDeep({}, options, { nested: { num: 'hi' } });
const result = validate(modifiedOptions, template);
const { validated } = result;
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(validated.nested).not.toHaveProperty('num');
expect(_validated.nested).not.toHaveProperty('num');
});
test('single nested enum value dont match template', () => {
const modifiedOptions = assignDeep({}, options, {
nested: { abc: 'testabc' },
});
const result = validate(modifiedOptions, template);
const { validated } = result;
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(validated.nested).not.toHaveProperty('abc');
expect(_validated.nested).not.toHaveProperty('abc');
});
test('multiple nested values dont match template', () => {
const modifiedOptions = assignDeep({}, options, {
nested: { num: 'hi', abc: 'testabc' },
});
const result = validate(modifiedOptions, template);
const { validated } = result;
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(validated.nested).not.toHaveProperty('num');
expect(validated.nested).not.toHaveProperty('abc');
expect(_validated.nested).not.toHaveProperty('num');
expect(_validated.nested).not.toHaveProperty('abc');
});
test('all nested values dont match template', () => {
const modifiedOptions = assignDeep({}, options, {
nested: { num: 'hi', abc: 'testabc', switch: 1 },
});
const result = validate(modifiedOptions, template);
const { validated } = result;
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(validated).not.toHaveProperty('nested');
expect(_validated).not.toHaveProperty('nested');
});
test('all nested values dont match template with foreign property', () => {
@@ -316,10 +316,10 @@ describe('options validation', () => {
switch: 1,
},
});
const result = validate(modifiedOptions, template);
const { validated } = result;
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(validated).not.toHaveProperty('nested');
expect(_validated).not.toHaveProperty('nested');
});
test('various values dont match template', () => {
@@ -329,13 +329,13 @@ describe('options validation', () => {
abc: 'testest',
func: {},
});
const result = validate(modifiedOptions, template);
const { validated } = result;
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(validated.nested).not.toHaveProperty('switch');
expect(validated).not.toHaveProperty('obj');
expect(validated).not.toHaveProperty('abc');
expect(validated).not.toHaveProperty('func');
expect(_validated.nested).not.toHaveProperty('switch');
expect(_validated).not.toHaveProperty('obj');
expect(_validated).not.toHaveProperty('abc');
expect(_validated).not.toHaveProperty('func');
});
test('various values dont match template with foreign properties', () => {
@@ -355,42 +355,42 @@ describe('options validation', () => {
foreignObj,
{ nested: foreignObj }
);
const result = validate(modifiedOptions, template);
const { validated, foreign } = result;
const result = validateOptions(modifiedOptions, template);
const { _validated, _foreign } = result;
expect(foreign.nested).toEqual(foreignObj);
delete foreign.nested;
expect(foreign).toEqual(foreignObj);
expect(_foreign.nested).toEqual(foreignObj);
delete _foreign.nested;
expect(_foreign).toEqual(foreignObj);
expect(validated.nested).not.toHaveProperty('switch');
expect(validated).not.toHaveProperty('obj');
expect(validated).not.toHaveProperty('abc');
expect(validated).not.toHaveProperty('func');
expect(_validated.nested).not.toHaveProperty('switch');
expect(_validated).not.toHaveProperty('obj');
expect(_validated).not.toHaveProperty('abc');
expect(_validated).not.toHaveProperty('func');
});
test('nested object is string', () => {
const modifiedOptions = assignDeep({}, options, { nested: 'string' });
const result = validate(modifiedOptions, template);
const { validated } = result;
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(validated).not.toHaveProperty('nested');
expect(_validated).not.toHaveProperty('nested');
});
test('nested object is null', () => {
const modifiedOptions = assignDeep({}, options, { nested: null });
const result = validate(modifiedOptions, template);
const { validated } = result;
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(validated).not.toHaveProperty('nested');
expect(_validated).not.toHaveProperty('nested');
});
test('nested object is undefined', () => {
const modifiedOptions = assignDeep({}, options);
modifiedOptions.nested = undefined;
const result = validate(modifiedOptions, template);
const { validated } = result;
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(validated).not.toHaveProperty('nested');
expect(_validated).not.toHaveProperty('nested');
});
});
@@ -399,7 +399,7 @@ describe('options validation', () => {
const { warn } = console;
console.warn = jest.fn();
validate(options, template, {}, true);
validateOptions(options, template, {}, true);
expect(console.warn).not.toBeCalled();
console.warn = warn;
@@ -410,7 +410,7 @@ describe('options validation', () => {
console.warn = jest.fn();
const modifiedOptions = assignDeep({}, options, { str: 1 });
validate(modifiedOptions, template, {}, false);
validateOptions(modifiedOptions, template, {}, false);
expect(console.warn).not.toBeCalled();
console.warn = warn;
@@ -421,15 +421,15 @@ describe('options validation', () => {
console.warn = jest.fn();
// str must be string
validate(assignDeep({}, options, { str: 1 }), template, {}, true);
validateOptions(assignDeep({}, options, { str: 1 }), template, {}, true);
expect(console.warn).toBeCalledTimes(1);
// abc must be A | B | C
validate(assignDeep({}, options, { abc: 'some string' }), template, {}, true);
validateOptions(assignDeep({}, options, { abc: 'some string' }), template, {}, true);
expect(console.warn).toBeCalledTimes(2);
// everthing OK
validate(assignDeep({}, options, { abc: 'C' }), template, {}, true);
validateOptions(assignDeep({}, options, { abc: 'C' }), template, {}, true);
expect(console.warn).toBeCalledTimes(2);
console.warn = warn;
@@ -13,9 +13,9 @@ export declare type OptionsValidatedResult<T> = {
readonly foreign: PlainObject;
readonly validated: T;
};
export declare type OptionsAndOptionsTemplateValue<T extends OptionsTemplateNativeTypes> = [T, OptionsTemplateValue<T>];
export declare type OptionsAndOptionsTemplate<T extends Required<T>> = {
[P in keyof T]: PlainObject extends T[P] ? OptionsAndOptionsTemplate<Required<T[P]>> : T[P] extends OptionsTemplateNativeTypes ? OptionsAndOptionsTemplateValue<T[P]> : never;
export declare type OptionsWithOptionsTemplateValue<T extends OptionsTemplateNativeTypes> = [T, OptionsTemplateValue<T>];
export declare type OptionsWithOptionsTemplate<T extends Required<T>> = {
[P in keyof T]: PlainObject extends T[P] ? OptionsWithOptionsTemplate<Required<T[P]>> : T[P] extends OptionsTemplateNativeTypes ? OptionsWithOptionsTemplateValue<T[P]> : never;
};
declare type OptionsTemplateTypeMap = {
__TPL_boolean_TYPE__: boolean;
@@ -1,3 +1,3 @@
import { OptionsTemplate, OptionsAndOptionsTemplate } from 'support/options';
export declare function transform<T extends Required<T>>(optionsWithOptionsTemplate: OptionsAndOptionsTemplate<T>): T;
export declare function transform<T extends Required<T>>(optionsWithOptionsTemplate: OptionsAndOptionsTemplate<T>, toTemplate: true | void): OptionsTemplate<T>;
import { OptionsTemplate, OptionsWithOptionsTemplate } from 'support/options';
export declare function transform<T extends Required<T>>(optionsWithOptionsTemplate: OptionsWithOptionsTemplate<T>): T;
export declare function transform<T extends Required<T>>(optionsWithOptionsTemplate: OptionsWithOptionsTemplate<T>, toTemplate: true | void): OptionsTemplate<T>;