lib/utils/exception.js

/**
 * Check if the argument is of a registered exception type.
 * @param {mdn:Object} object - the object to check
 * @return {mdn:Boolean} whether the object is a registered exception
 */
export default function exception(object) {
  return object == null ? false : registry.has(object[tokenAccessor]);
}

/**
 * The registry of every registered type of exception.
 * The keys are tokens that uniquely identifies a type of exception
 * and the values are the constructors used to instanciate them
 */
const registry = new Map();

/**
 * A symbol to access the token property of the created exceptions.
 */
const tokenAccessor = Symbol("token property name");


/**
 * A helper function to efficiently use property descriptors in
 * {@link mdn:Object.defineProperty}.
 */
function propertyDescriptor(value) {
  propertyDescriptor.d.value = value;
  return propertyDescriptor.d;
}

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

/**
 * A default constructor for an exception
 */
function Exception(value) {
  this.name = this.constructor.name;
  this.value = value;
}

const depthAccessor = Symbol("depth property name");
const incDepthAccessor = Symbol("increment depth property name");

const properties = {
  token: {
    get: function() {
      return this[tokenAccessor];
    },
    configurable: false,
    enumerable: false
  },
  onIt: {
    value: function(handler) {
      return handler.call(undefined, this);
    },
    writable: false,
    configurable: false,
    enumerable: false
  },
  dispatch: {
    value: function(handlers) {
      if(!registry.has(this[tokenAccessor])) {
        throw new Error("Not a recognized exception");
      }
      let handler = handlers[this[tokenAccessor]];
      if(handler == null) {
        handler = handlers.default;
        if(handler == null) {
          throw this;
        }
      }
      return this.onIt(handler);
    },
    writable: false,
    configurable: false,
    enumerable: false
  }
};

const messageProperty = {
  get: function() {
    return `${this.name} raised`;
  },
  configurable: true,
  enumerable: true
};

const incDepthProperty = {
  value: function() {
    this[depthAccessor]++;
  },
  configurable: false,
  enumerable: true,
  writable: false
};

Object.defineProperties(Exception.prototype, properties);
Object.defineProperty(Exception.prototype, "message", messageProperty);
Object.defineProperty(Exception.prototype, incDepthAccessor, incDepthProperty);

/**
 * Registers a new type of exception.
 * @param {mdn:String} name - The name of the new type
 * @param {Class} superClass - An optional constructor that will be extended
 * @return {Symbol} The token of the newly created type of exception
 */
exception.register = (name, superClass=Exception) => {
  const newExceptionClass = class extends superClass {
    constructor(...args) {
      super(...args);
      this.name = name;
      this[depthAccessor] = 0;
    }
  };
  const token = Symbol(name);
  if(superClass !== Exception) {
    Object.defineProperties(newExceptionClass.prototype, properties);
    // if(!("message" in newExceptionClass.prototype)) {
    //   Object.defineProperty(newExceptionClass.prototype, "message", messageProperty);
    // }
    if(!(incDepthAccessor in newExceptionClass.prototype)) {
      Object.defineProperty(newExceptionClass.prototype, incDepthAccessor, incDepthProperty);
    }
  }
  Object.defineProperty(newExceptionClass.prototype, tokenAccessor, propertyDescriptor(token));
  registry.set(token, newExceptionClass);
  return token;  
};


/**
 * Creates an exception of a given type
 * @param {Symbol} The token
 * @param ...args The arguments to pass to the exception constructor
 * @return {Exception} The newly created exception
 * @throws Error When the supplied token does not match a registered type of 
 * exception
 */
exception.create = (token, ...args) => {
  const exceptionClass = registry.get(token);
  if(exceptionClass === undefined) {
    throw new Error("Not a recognized exception");
  }
  return new exceptionClass(...args);
};

/**
 * A collection of exceptions
 */
class ObjectExceptions {
  constructor(descriptor, definition, exceptions, output) {
    this.value = descriptor;
    this.definition = definition;
    this.exceptions = exceptions;
    this.output = output;
    this[incDepthAccessor]();
    this[depthAccessor] = 0;
  }
  get message() {
    let m = `${this.name}: exceptions on the following properties: `;
    for(let prop of Object.keys(this.exceptions)) {
      m += "\n";
      m += "  ".repeat(this.exceptions[prop][depthAccessor]);
      m += `${prop}: ${this.exceptions[prop].message}`;
    }
    return m;
  }
  onProperties(objectCallbacks) {
    const results = {};
    let callback, e;
    for(let prop of Object.keys(this.exceptions)) {
      callback = objectCallbacks[prop];
      e = this.exceptions[prop];
      if(callback == null) {
        throw e;
      }
      results[prop] = callback.call(undefined, e);
    }
    return results;
  }
  onEachProperty(callback) {
    const results = {};
    let e;
    for(let prop of Object.keys(this.exceptions)) {
      e = this.exceptions[prop];
      results[prop] = callback.call(undefined, e, prop);
    }
    return results;
  }
}

Object.defineProperty(ObjectExceptions.prototype, incDepthAccessor, propertyDescriptor(function() {
  this[depthAccessor]++;
  for(let prop of Object.keys(this.exceptions)) {
    this.exceptions[prop][incDepthAccessor]();
  }
}));

/**
 * A collection of exceptions
 */
class ArrayExceptions {
  constructor(value, definition, exceptions, output) {
    this.value = value;
    this.definition = definition;
    this.output = output;
    this.exceptions = exceptions;
    this[incDepthAccessor]();
    this[depthAccessor]--;
  }
  get message() {
    let m = `${this.name}: exceptions on the following indexes: `;
    this.exceptions.forEach((err, i) => {
      if(err != null) {
        m +="\n";
        m += "  ".repeat(err[depthAccessor]);
        m += `index ${i}: ${err.message}`;
      }
    });
    return m;
  }
  onEach(callback) {
    const results = [];
    for(let err, i = 0; i < this.exceptions.length; i++) {
      err = this.exceptions[i];
      if(err !== undefined) {
        results[i] = callback.call(undefined, err, i);
      }
    }
    return results;
  }
}

Object.defineProperty(ArrayExceptions.prototype, incDepthAccessor, propertyDescriptor(function() {
  this[depthAccessor]++;
  for(let e of this.exceptions) {
    if(e !== undefined) {
      e[incDepthAccessor]();
    }
  }
}));

class NotMember {
  constructor(value, collection) {
    this.value = value;
    this.collection = collection;
  }
  get message() {
    return `${this.name}: not a member of the supplied collection`;
  }
}

class WrongType {
  constructor(value, type) {
    this.value = value;
    this.type = type;
  }
  get message() {
    return `${this.name}: should be of type ${this.type}`;
  }
}

class WrongClass {
  constructor(value, classConstructor) {
    this.value = value;
    this.classConstructor = classConstructor;
  }
  get message() {
    return `${this.name}: not an instance of the given class/constructor`;
  }
}

class MissingValue {
  constructor(value, allowNull=false) {
    this.value = value;
    this.allowNull = allowNull;
  }
  get message() {
    return `${this.name}: undefined ${this.allowNull ? "" : "or null"},\
      but variable is ${this.value}`;
  }
}

class UnknownPropertyName {
  constructor(value, definition, propertyName) {
    this.value = value;
    this.definition = definition;
    this.propertyName = propertyName;
  }
  get message() {
    return `${this.name}: the property ${this.propertyName} has not been defined`;
  }
}

exception.array = exception.register("ArrayExceptions", ArrayExceptions);
exception.object = exception.register("ObjectExceptions", ObjectExceptions);
exception.missingValue = exception.register("MissingValue", MissingValue);
exception.notMember = exception.register("NotMember", NotMember);
exception.wrongClass = exception.register("WrongClass", WrongClass);
exception.wrongType = exception.register("WrongType", WrongType);
exception.unknownProp = exception.register("UnknownPropertyName", UnknownPropertyName);