All files / src/plugins/optionsValidation validation.ts

100% Statements 48/48
100% Branches 26/26
100% Functions 9/9
100% Lines 46/46

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210                                                                                                                              4x             4x                 28x 28x                                         4x           53x 53x 288x   53x 280x   280x 280x 280x     280x 27x           27x 27x   27x 54x 25x     253x 250x 250x 250x 250x 250x       250x     305x 2135x 255x     305x 305x   50x 91x     50x   255x       305x     305x     250x 224x 26x 2x                       250x       53x                         4x       26x      
import { each, hasOwnProperty, keys, push, isEmptyObject } from 'support/utils';
import { type, isArray, isUndefined, isPlainObject, isString } from 'support/utils/types';
import { PlainObject, PartialOptions } from 'typings';
 
export type OptionsObjectType = Record<string, unknown>;
export type OptionsFunctionType = (this: unknown, ...args: unknown[]) => unknown;
export type OptionsTemplateType<T extends OptionsTemplateNativeTypes> = ExtractPropsKey<
  OptionsTemplateTypeMap,
  T
>;
export type OptionsTemplateTypes = keyof OptionsTemplateTypeMap;
export type OptionsTemplateNativeTypes = OptionsTemplateTypeMap[keyof OptionsTemplateTypeMap];
 
export type OptionsTemplateValue<T extends OptionsTemplateNativeTypes = string> = T extends string
  ? string extends T
    ? OptionsTemplateValueNonEnum<T>
    : string
  : OptionsTemplateValueNonEnum<T>;
 
export type OptionsTemplate<T> = {
  [P in keyof T]: T[P] extends OptionsObjectType
    ? OptionsTemplate<T[P]>
    : T[P] extends OptionsTemplateNativeTypes
    ? OptionsTemplateValue<T[P]>
    : never;
};
 
export type OptionsValidationResult<T> = [
  PartialOptions<T>, // validated
  Record<string, unknown> // foreign
];
 
type OptionsTemplateTypeMap = {
  __TPL_boolean_TYPE__: boolean;
  __TPL_number_TYPE__: number;
  __TPL_string_TYPE__: string;
  __TPL_array_TYPE__: Array<any> | ReadonlyArray<any>;
  __TPL_function_TYPE__: OptionsFunctionType;
  __TPL_null_TYPE__: null;
  __TPL_object_TYPE__: OptionsObjectType;
};
 
type OptionsTemplateValueNonEnum<T extends OptionsTemplateNativeTypes> =
  | OptionsTemplateType<T>
  | [OptionsTemplateType<T>, ...Array<OptionsTemplateTypes>];
 
type ExtractPropsKey<T, TProps extends T[keyof T]> = {
  [P in keyof T]: TProps extends T[P] ? P : never;
}[keyof T];
 
type OptionsTemplateTypesDictionary = {
  readonly boolean: OptionsTemplateType<boolean>;
  readonly number: OptionsTemplateType<number>;
  readonly string: OptionsTemplateType<string>;
  readonly array: OptionsTemplateType<Array<any>>;
  readonly object: OptionsTemplateType<OptionsObjectType>;
  readonly function: OptionsTemplateType<OptionsFunctionType>;
  readonly null: OptionsTemplateType<null>;
};
 
/**
 * A prefix and suffix tuple which serves as recognition pattern for template types.
 */
const templateTypePrefixSuffix: readonly [string, string] = ['__TPL_', '_TYPE__'];
 
/**
 * A object which serves as a mapping for "normal" types and template types.
 * Key   = normal type string
 * value = template type string
 */
const optionsTemplateTypes: OptionsTemplateTypesDictionary = [
  'boolean',
  'number',
  'string',
  'array',
  'object',
  'function',
  'null',
].reduce((result, item) => {
  result[item] = templateTypePrefixSuffix[0] + item + templateTypePrefixSuffix[1];
  return result;
}, {} as OptionsTemplateTypesDictionary);
 
/**
 * Validates the given options object according to the given template object and returns a object which looks like:
 * {
 *  foreign   : a object which consists of properties which aren't defined inside the template. (foreign properties)
 *  validated : a object which consists only of valid properties. (property name is inside the template and value has a correct type)
 * }
 * @param template The template according to which the options object shall be validated.
 * @param options The options object which shall be validated.
 * @param optionsDiff When provided the returned validated object will only have properties which are different to this objects properties.
 * Example (assume all properties are valid to the template):
 * Options object            : { a: 'a', b: 'b', c: 'c' }
 * optionsDiff object        : { a: 'a', b: 'b', c: undefined }
 * Returned validated object : { c: 'c' }
 * Because the value of the properties a and b didn't change, they aren't included in the returned object.
 * 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.
 * @param propPath The propertyPath which lead to this object. (used for error logging)
 */
const validateRecursive = <T extends PlainObject>(
  template: OptionsTemplate<T>,
  options: PartialOptions<T>,
  doWriteErrors?: boolean,
  propPath?: string
): OptionsValidationResult<T> => {
  const validatedOptions: PartialOptions<T> = {};
  const optionsCopy: PartialOptions<T> = { ...options };
  const props = keys(template).filter((prop) => hasOwnProperty(options, prop));
 
  each(props, (prop: Extract<keyof T, string>) => {
    const optionsValue: any = options[prop];
    const templateValue: PlainObject | string | OptionsTemplateTypes | Array<OptionsTemplateTypes> =
      template[prop];
    const templateIsComplex = isPlainObject(templateValue);
    const propPrefix = propPath ? `${propPath}.` : '';
 
    // if the template has a object as value, it means that the options are complex (verschachtelt)
    if (templateIsComplex && isPlainObject(optionsValue)) {
      const [validated, foreign] = validateRecursive(
        templateValue as T,
        optionsValue,
        doWriteErrors,
        propPrefix + prop
      );
      validatedOptions[prop] = validated as any;
      optionsCopy[prop] = foreign as any;
 
      each([optionsCopy, validatedOptions], (value) => {
        if (isEmptyObject(value[prop])) {
          delete value[prop];
        }
      });
    } else if (!templateIsComplex) {
      let isValid = false;
      const errorEnumStrings: Array<string> = [];
      const errorPossibleTypes: Array<string> = [];
      const optionsValueType = type(optionsValue);
      const templateValueArr: Array<string | OptionsTemplateTypes> = !isArray(templateValue)
        ? [templateValue as string | OptionsTemplateTypes]
        : (templateValue as Array<OptionsTemplateTypes>);
 
      each(templateValueArr, (currTemplateType) => {
        // if currType value isn't inside possibleTemplateTypes we assume its a enum string value
        let typeString: string | undefined;
        each(optionsTemplateTypes, (value: string, key: string) => {
          if (value === currTemplateType) {
            typeString = key;
          }
        });
        const isEnumString = isUndefined(typeString);
        if (isEnumString && isString(optionsValue)) {
          // split it into a array which contains all possible values for example: ["yes", "no", "maybe"]
          const enumStringSplit = currTemplateType.split(' ');
          isValid = !!enumStringSplit.find((possibility) => possibility === optionsValue);
 
          // build error message
          push(errorEnumStrings, enumStringSplit);
        } else {
          isValid = optionsTemplateTypes[optionsValueType] === currTemplateType;
        }
 
        // build error message
        push(errorPossibleTypes, isEnumString ? optionsTemplateTypes.string : typeString!);
 
        // continue if invalid, break if valid
        return !isValid;
      });
 
      if (isValid) {
        validatedOptions[prop] = optionsValue;
      } else if (doWriteErrors) {
        console.warn(
          `${
            `The option "${propPrefix}${prop}" wasn't set, because it doesn't accept the type [ ${optionsValueType.toUpperCase()} ] with the value of "${optionsValue}".\r\n` +
            `Accepted types are: [ ${errorPossibleTypes.join(', ').toUpperCase()} ].\r\n`
          }${
            errorEnumStrings.length > 0
              ? `\r\nValid strings are: [ ${errorEnumStrings.join(', ')} ].`
              : ''
          }`
        );
      }
 
      delete optionsCopy[prop];
    }
  });
 
  return [validatedOptions, optionsCopy]; // optionsCopy equals now to foreign options
};
 
/**
 * Validates the given options object according to the given template object and returns a tuple which looks like:
 * [
 *  validated : a object which consists only of valid properties. (property name is inside the template and value has a correct type)
 *  foreign   : a object which consists of properties which aren't defined inside the template. (foreign properties)
 * ]
 * @param template The template according to which the options object shall be validated.
 * @param options The options object which shall be validated.
 * @param doWriteErrors True if errors shall be logged into the console, false otherwise.
 */
const validateOptions = <T extends PlainObject>(
  template: OptionsTemplate<T>,
  options: PartialOptions<T>,
  doWriteErrors?: boolean
): OptionsValidationResult<T> => validateRecursive<T>(template, options, doWriteErrors);
 
export { validateOptions, optionsTemplateTypes };