All files / src/core/options validation.ts

100% Statements 49/49
100% Branches 36/36
100% Functions 8/8
100% Lines 47/47

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                        3x         3x           3x   21x 21x                                             3x             65x 65x 372x   65x 372x 372x 372x 372x 372x     372x 33x 33x 33x   33x 66x 33x     339x 336x 336x 336x 336x 336x       336x   417x 417x   64x 121x     64x   353x       417x     417x     336x 310x 310x 256x   26x 2x               336x       65x                                             3x                             32x                            
import { each, indexOf } from 'core/utils/arrays';
import { type, isArray, isUndefined, isEmptyObject, isPlainObject, isString } from 'core/utils/types';
import {
  PlainObject,
  OptionsTemplate,
  OptionsTemplateTypes,
  OptionsTemplateType,
  OptionsValidated,
  Func,
  OptionsValidatedResult,
} from 'core/typings';
 
const { stringify } = JSON;
 
/**
 * 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 options The options object which shall be validated.
 * @param template The template according to which the options object 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 = function <T extends PlainObject>(
  options: T,
  template: OptionsTemplate<Required<T>>,
  optionsDiff: OptionsValidated<T>,
  doWriteErrors?: boolean,
  propPath?: string,
): OptionsValidatedResult<T> {
  const validatedOptions: OptionsValidated<T> = {};
  const optionsCopy: T = { ...options };
  const props = Object.keys(template).filter((prop) => options.hasOwnProperty(prop));
 
  each(props, (prop: Extract<keyof T, string>) => {
    const optionsDiffValue: any = isUndefined(optionsDiff[prop]) ? {} : optionsDiff[prop];
    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 validatedResult = validateRecursive(optionsValue, templateValue as PlainObject, optionsDiffValue, doWriteErrors, propPrefix + prop);
      validatedOptions[prop] = validatedResult.validated;
      optionsCopy[prop] = validatedResult.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
        const isEnumString = indexOf(Object.values(optionsTemplateTypes), currTemplateType) < 0;
        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
          errorEnumStrings.push(...enumStringSplit);
        } else {
          isValid = optionsTemplateTypes[optionsValueType] === currTemplateType;
        }
 
        // build error message
        errorPossibleTypes.push(isEnumString ? optionsTemplateTypes.string : currTemplateType);
 
        // continue if invalid, break if valid
        return !isValid;
      });
 
      if (isValid) {
        const doStringifyComparison = isArray(optionsValue) || isPlainObject(optionsValue);
        if (doStringifyComparison ? stringify(optionsValue) !== stringify(optionsDiffValue) : optionsValue !== optionsDiffValue) {
          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 {
    foreign: optionsCopy,
    validated: validatedOptions,
  };
};
 
/**
 * 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 options The options object which shall be validated.
 * @param template The template according to which the options object 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.
 */
const validate = function <T extends PlainObject>(
  options: T,
  template: OptionsTemplate<Required<T>>,
  optionsDiff?: OptionsValidated<T>,
  doWriteErrors?: boolean,
): OptionsValidatedResult<T> {
  /*
    if (!isEmptyObject(foreign) && doWriteErrors)
        console.warn(`The following options are discarded due to invalidity:\r\n ${window.JSON.stringify(foreign, null, 2)}`);
 
    //add values, which aren't specified in the template, to the finished validated object to prevent them from being discarded
    if (keepForeignProps) {
        Object.assign(result.validated, foreign);
    }
    */
  return validateRecursive(options, template, optionsDiff || {}, doWriteErrors || false);
};
 
export { validate, optionsTemplateTypes };
 
type OptionsTemplateTypesDictionary = {
  readonly boolean: OptionsTemplateType<boolean>;
  readonly number: OptionsTemplateType<number>;
  readonly string: OptionsTemplateType<string>;
  readonly array: OptionsTemplateType<Array<any>>;
  readonly object: OptionsTemplateType<object>; // eslint-disable-line @typescript-eslint/ban-types
  readonly function: OptionsTemplateType<Func>;
  readonly null: OptionsTemplateType<null>;
};