Home Manual Reference Source Repository

src/utils/reflection.js

/*
 * BSD 3-Clause License
 *
 * Copyright (c) 2020, Mapcreator
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 *  Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 *  Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import Trait from '../traits/Trait';
import { fnv32b } from './hash';

/**
 * Tests if the parent is a parent of child
 * @param {Class|object} parent - Instance or Class
 * @param {Class|object} child - Instance or Class
 * @returns {boolean} - is parent a parent of child
 * @private
 * @example
 * class A {}
 * class B extends A {}
 * class C extends B {}
 *
 * isParentOf(A, C) // true
 * isParentOf(B, C) // true
 * isParentOf(C, C) // true
 * isParentOf(B, A) // false
 */
export function isParentOf (parent, child) {
  if (parent === null || child === null) {
    return false;
  }

  parent = typeof parent === 'function' ? parent : parent.constructor;
  child = typeof child === 'function' ? child : child.constructor;

  do {
    if (child.name === parent.name) {
      return true;
    }

    child = Object.getPrototypeOf(child);
  } while (child.name !== '');

  return false;
}


/**
 * Get the name of the value type
 * @param {*} value - Any value
 * @private
 * @returns {string} - Value type name
 */
export function getTypeName (value) {
  if (typeof value === 'undefined') {
    return 'undefined';
  }

  value = typeof value === 'function' ? value : value.constructor;

  return value.name;

}

/**
 * Helper class for mix
 * @see {@link mix}
 */
class Empty {
}

/**
 * Copy properties from source to target
 * @param {object} target - Object for the properties to be copied to
 * @param {object} source - Object containing properties to be copied
 */
function copyProps (target, source) {
  Object
    .getOwnPropertyNames(source)
    .concat(Object.getOwnPropertySymbols(source))
    .forEach(prop => {
      if (!prop.match(/^(?:constructor|initializer|prototype|arguments|caller|name|bind|call|apply|toString|length)$/)) {
        Object.defineProperty(target, prop, Object.getOwnPropertyDescriptor(source, prop));
      }
    });
}

/**
 * Mix traits into the target class
 * @param {Class} baseClass - Target base class for the traits to be applied to
 * @param {Class<Trait>} mixins - Traits to be applied
 * @returns {Class} - Constructor with any traits applied
 * @private
 */
export function mix (baseClass, ...mixins) {
  const cocktail = class _Cocktail extends (baseClass || Empty) {
    constructor (...args) {
      super(...args);

      mixins
        .map(mixin => mixin.prototype.initializer)
        .filter(initializer => typeof initializer === 'function')
        .forEach(initializer => initializer.call(this));
    }
  };


  for (const mixin of mixins) {
    if (!isParentOf(Trait, mixin)) {
      throw new TypeError(`Expected mixin to implement Trait for ${mixin.name}`);
    }

    copyProps(cocktail.prototype, mixin.prototype);
    copyProps(cocktail, mixin);
  }

  cocktail.__mixins = mixins;

  const hash = fnv32b(mixins.map(x => x.name).join(','));

  Object.defineProperty(cocktail, 'name', { value: `Cocktail_${hash}` });

  return cocktail;
}

/**
 * Checks if the target has a certain trait.
 *
 * @param {Class|Object} Subject - Instance or class
 * @param {Class<Trait>} TraitClass - Trait to check for
 * @returns {boolean} - If the subject has the trait
 * @private
 */
export function hasTrait (Subject, TraitClass) {
  Subject = typeof Subject === 'function' ? Subject : Subject.constructor;

  do {
    if (Array.isArray(Subject.__mixins) && Subject.__mixins.includes(TraitClass)) {
      return true;
    }

    Subject = Object.getPrototypeOf(Subject);
  } while (Subject.name !== '');

  return false;
}