lib/utils/check.js

/**
 * @module lib/utils/descriptor
 * @requires lib/utils/exception
 */

import exception from "./exception";

/**
 * Creates a new check builder
 */
function check() {
  return new Check();
}

export { check as default, exception };

const queueAccessor = Symbol("queue property name");

const identityBypass = (x) => x;
identityBypass.bypass = true;

const emptyArrayDescriptor = () => {
  emptyArrayDescriptor.d.value = [];
  return emptyArrayDescriptor.d;
};

emptyArrayDescriptor.d = {
  enumerable: false,
  writable: false,
  configurable: false
};

class Check {
  constructor() {
    Object.defineProperty(this, queueAccessor, emptyArrayDescriptor());
  }

  optional() {
    this[queueAccessor].push(identityBypass);
    return this;
  };

  defaults(defaultValue) {
    const checkDefaults = () => {
      return defaultValue;
    };
    checkDefaults.bypass = true;
    this[queueAccessor].push(checkDefaults);
    return this;
  };
  /**
   * @return {Any|CheckError<notMember>}
   */
  inMap(map) {
    const checkInMap = (value) => {
      if(map.has(value)) {
        return value;
      }
      return exception.create(exception.notMember, value, map);
    };
    this[queueAccessor].push(checkInMap);
    return this;
  };

  /**
   * @return {Any|CheckError<notMember>}
   */
  inSet(map) {
    const checkInSet = (value) => {
      if(map.has(value)) {
        return value;
      }
      return exception.create(exception.notMember, value, map);
    };
    this[queueAccessor].push(checkInSet);
    return this;
  };
  /**
   * @return {Any|CheckError<notMember>}
   */
  inArray(array) {
    const checkInArray = (value) => {
      if(array.includes(value)) {
        return value;
      }
      return exception.create(exception.notMember, value, array);
    };
    this[queueAccessor].push(checkInArray);
    return this;
  };

  /**
   * @return {Any|CheckError<wrongType>}
   */
  ofType(type) {
    const checkOfType = (value) => {
      if(typeof value === type) {
        return value;
      }
      return exception.create(exception.wrongType, value, type);
    };
    this[queueAccessor].push(checkOfType);
    return this;
  };

  /**
   * @return {Any|CheckError<wrongClass>}
   */
  instanceOf(constructor) {
    const checkInstanceOf = (value) => {
      if(value instanceof constructor) {
        return value;
      }
      return exception.create(exception.wrongClass, value, constructor);
    };
    this[queueAccessor].push(checkInstanceOf);
    return this;
  };

  /**
   * @return {Any|CheckError<array>|CheckError<notIterable>}
   */
  iterable(check) {
    const checkIterable = (value, ...parents) => {
      value = value || [];
      const exceptions = [];
      const output = [];
      if(value[Symbol.iterator] == null) {
        return exception.create(exception.wrongType, value, "iterable");
      }
      if(checkIterable.check === undefined) {
        return value;
      }
      let i = 0;
      for(let v of value) {
        const c = checkIterable.check.resolve(v,  output, ...parents);
        if(exception(c)) {
          exceptions[i] = c;
        } else {
          output[i] = c;
        }
        i++;
      }
      if(exceptions.length > 0) {
        exceptions.length = i;
        output.length = i;
        return exception.create(exception.array, value, checkIterable.check, exceptions, output);
      }
      return output;
    };
    checkIterable.iterable = true;
    checkIterable.check = check;
    this[queueAccessor].push(checkIterable);
    return this;
  };
  /**
   * @return {Any|CheckError<object>}
   */
  object(definition, fireUnknown=false, order) {
    definition = definition || {};
    order = order || [];
    const checkObject = (value, ...parents) => {
      value = value || {};
      const output = {};
      const exceptions = {};
      let c;
      order = order.concat(
        Object.keys(checkObject.definition).filter(
          (prop) => !order.includes(prop)));
      for(let prop of order) {
        if(checkObject.definition[prop] === undefined) {
          continue;
        }
        c = checkObject.definition[prop].resolve(value[prop], output, ...parents);
        if(exception(c)) {
          exceptions[prop] = c;
        } else {
          output[prop] = c;
        }
      }
      for(let prop of Object.keys(value)) {
        if(checkObject.definition[prop] === undefined) {
          if(checkObject.fireUnknown) {
            exceptions[prop] = exception.create(exception.unknownProp, value, checkObject.definition, prop);
          } else {
            output[prop] = value[prop];
          }
        }
      }
      if(Object.keys(exceptions).length > 0) {
        return exception.create(exception.object, value, checkObject.definition, exceptions, output);
      }
      return output;
    };
    checkObject.object = true;
    checkObject.definition = definition;
    checkObject.order = order;
    checkObject.fireUnkown = fireUnknown;
    this[queueAccessor].push(checkObject);
    return this;
  };

  /**
   * @return {Any|CheckError<missingValue>}
   */
  mandatory() {
    const checkMandatory = (value) => {
      if(value == null) {
        return exception.create(exception.missingValue, value);
      }
      return value;
    };
    this[queueAccessor].push(checkMandatory);
    return this;
  };

  compose(check) {
    const queue = this[queueAccessor];
    for(let f of check[queueAccessor]) {
      if(f.object === true) {
        this.object(Object.assign(f.definition), f.fireUnknown, f.order);
      } else if (f.iterable === true) {
        this.iterable((new Check()).compose(f.check));
      } else {
        queue.push(f);
      }
    }
    return this;
  };
  
  // TODO merge orders
  deepCompose(append) {
    const queue = this[queueAccessor];
    const appendQueue = append[queueAccessor];
    if(appendQueue.length <= 0) {
      return this;
    }
    if(queue.length <= 0) {
      this.compose(append);
      return this;
    }
    const tip = queue[queue.length - 1];
    const toCompose = appendQueue[0];
    if(tip.object === true && toCompose.object === true) {
      const newDef = Object.assign({}, tip.definition);
      for(let prop of Object.keys(toCompose.definition)) {
        if(newDef[prop] === undefined) {
          newDef[prop] = check().compose(toCompose.definition[prop]);
        } else {
          newDef[prop] = check().compose(newDef[prop]).deepCompose(toCompose.definition[prop]);
        }
      }
      tip.definition = newDef;
      return this;
    }
    if(tip.iterable === true && toCompose.iterable === true) {
      tip.check = check().compose(tip.check).deepCompose(toCompose.check);
      return this;
    }
    return this.compose(append);
  }

  add(f) {
    this[queueAccessor].push(f);
    return this;
  };

  resolve(value, ...parents) {
    for(let f of this[queueAccessor]) {
      if(exception(value)) {
        return value;
      }
      if(f.bypass && value == null) {
        return f.call(undefined, value, ...parents);
      }
      value = f.call(undefined, value, ...parents);
    }
    return value;
  };

  hold() {
    return this.resolve.bind(this);
  };
}