ModuleParser.js

// @flow
// @module ModuleParser

import * as types from './types'

const { 
  isString,
  isOfType,
  isPrimitive,
  isArray,
  isObject
} = types;

/**
 * The ModuleParser is a utility class designed to loop through and iterate 
 * on a directory and pull out of each .js file found, any classes or exports 
 * that extend from GQLBase or a child of GQLBase. 
 * 
 * @class ModuleParser
 */
export class ModuleParser {
  /**
   * The constructor 
   *
   * @constructor 
   * @method ⎆⠀constructor
   * @memberof ModuleParser
   * @inner
   * 
   * @param {string|mixed} moduleNameOrModule [description]
   */
  constuctor(moduleNameOrModule: string|mixed):ModuleParser {
    let moduleName, moduleContents;
    
    if (isString(moduleNameOrModule)) {
      try { moduleContents = require(moduleNameOrModule); } catch(ignore) { }
      if (!moduleContents) {
        throw new Error(`
          Unable to require()'${moduleNameOrModule}'). Please check this file 
          to make sure it should be part of the load process.
        `);
      }
    }
    else {
      moduleContents = moduleNameOrModule;
    }
    
    if (!moduleContents) {
      throw new Error(`
        Inspite of being passed ${moduleNameOrModule}, it was determined that
        the value passed in was null or falsey. Please provide a module name 
        or an Object that can be iterated over for potential GQLBase classes.
      `);
    }
    
    this.initWith(moduleContents);
  }
  
  /**
   * Given an object, typically the result of a `require()` or `import` 
   * command, iterate over its contents and find any `GQLBase` derived 
   * exports. Continually, and recursively, build this list of classes out 
   * so that we can add them to a `GQLExpressMiddleware`.
   * 
   * @method ⌾⠀initWith
   * @memberof ModuleParser
   * @inner
   * 
   * @param {mixed} contents the object to parse for properties extending 
   * from `GQLBase`
   * @param {Array<GQLBase>} gqlDefinitions the results, allowed as a second 
   * parameter during recursion as a means to save state between calls
   * @return {Set<mixed>} a unique set of values that are currently being 
   * iterated over. Passed in as a third parameter to save state between calls 
   * during recursion.
   */
  initWith(
    contents: mixed, 
    gqlDefinitions: Array<GQLBase> = [],
    stack: Set<mixed> = new Set()
  ): void {
    // In order to prevent infinite object recursion, we should add the 
    // object being iterated over to our Set. At each new recursive level 
    // add the item being iterated over to the set and only recurse into
    // if the item does not already exist in the stack itself.
    stack.add(contents)
    
    for (let key in contents) {
      let value = contents[key];
      
      if (isPrimitive(value)) { continue }
      
      if (extendsFrom(value, GQLBase)) {
        gqlDefinitions.push(value);
      }
      
      if ((isObject(value) || isArray(value)) && !stack.has(value)) {
        gqlDefinitions = this.initWith(value, gqlDefinitions, stack);
      }
    }

    // We remove the current iterable from our set as we leave this current 
    // recursive iteration.
    stack.delete(contents)
    
    return gqlDefinitions
  }
  
  /**
   * Returns the `constructor` name. If invoked as the context, or `this`, 
   * object of the `toString` method of `Object`'s `prototype`, the resulting 
   * value will be `[object ModuleParser]`
   * 
   * @method ⌾⠀[Symbol.toStringTag]
   * @memberof ModuleParser
   *
   * @return {string} the name of the class this is an instance of
   */
  [Symbol.toStringTag]() { return this.constructor.name }
  
  /**
   * Applies the same logic as {@link ModuleParser#[Symbol.toStringTag]} but on a
   * static scale. So, if you perform `Object.prototype.toString.call(ModuleParser)`
   * the result would be `[object ModuleParser]`.
   * 
   * @method ⌾⠀[Symbol.toStringTag]
   * @memberof ModuleParser
   * @static
   *
   * @return {string} the name of this class 
   */
  static [Symbol.toStringTag]() { return this.name }
}