// @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 }
}