Show:
"use strict";

import { ok as assert } from "assert";
import includeAll from "include-all";
import inflect from "inflect";

const LOOKUP_MAP = {
  module: __dirname
};

/**
 * @class Registry
 * @constructor
 */
export default class Registry {
  constructor() {
    this._registry = new Map();
  }

  /**
   * Inject an object into another object
   *
   * @method inject
   * @param {Object} context the object to inject onto
   * @param {String} lookup name by which to look search for the injection in the registry
   * @param {String} propertyName optional property name of the newly injected object
   * @returns {Object} context
   *
   * @example
   * ```javascript
   * registry.inject(object, "service:store");
   * // object.store
   *
   * registry.inject(object, "service:model-manager", "modelManager");
   * // object.modelManager
   * ```
   */
  inject(context, lookup, propertyName) {
    const hasBeenInjected = this._registry.has(lookup);

    assert(hasBeenInjected, `Attempted to inject unknown object '${lookup}'`);

    const [prop] = lookup.split(":");
    const property = propertyName || prop;
    let obj;

    if (Object.prototype.hasOwnProperty.call(context, property)) { return; }

    try {
      obj = this.lookup(lookup);
      assert(obj, `Attempted to inject unknown object '${lookup}'`);
      Object.defineProperty(context, property, {
        enumerable: false,
        configurable: false,
        get() { return obj; },
        set() {}
      });
    } catch (err) {
      throw err;
    }

    /* eslint-disable consistent-return */
    return context;
    /* eslint-enable consistent-return */
  }

  /**
   * Find an object in the registry. If the object isn't found in the registry,
   * lookup will attempt to find it by requiring it in. If the require fails
   * the lookup fails
   *
   * @method lookup
   * @param {String} name colon delimited lookup string "service:foo"
   * @returns {Object}
   *
   * @example
   * ```javascript
   * registry.lookup("service:foo");
   * ```
   */
  lookup(name) {
    const [moduleLookup, moduleName] = name.split(":");
    let obj = this._registry.get(name);

    if (obj) { return obj.instance; }

    try {
      obj = this._loadModule(moduleLookup, moduleName);
      assert(obj, `Attempted to lookup unknown ${moduleLookup} '${moduleName}'`);
    } catch (err) {
      throw err;
    }

    this.register(name, obj);

    return obj;
  }

  /**
   * Register an object in the registry by name. If the name exists and it was
   * registered with the { singleton: true } option, an error will be thrown.
   *
   * @method register
   * @param {String} name the name by which to register the object
   * @param {Object} Obj the object to store in the registry
   * @param {Object} options register options
   * @param {Boolean} options.instantiate instantiate the object when registering it
   * @param {Boolean} options.singleton only allow one registration of this name/object
   * @returns {Object} Obj
   *
   * @example
   * ```javascript
   * registry.register("service:foo", { foo: "bar" });
   * ```
   */
  register(name, Obj, options = {}) {
    const { instantiate, singleton } = options;
    let obj = this._registry.get(name);

    if (obj && obj.singleton) {
      throw new Error("Cannot re-register singleton object");
    }

    obj = {};

    if (instantiate) {
      obj.instance = new Obj(this);
      obj.singleton = singleton;

      this._registry.set(name, obj);

      return obj;
    }

    obj.instance = Obj;
    obj.singleton = singleton;

    this._registry.set(name, obj);

    return obj;
  }

  /**
   * Get the lookup directory for internal modules
   * @method _getLookupDirectory
   * @private
   * @param {String} lookup string name of object we're looking for (e.g. 'module')
   * @returns {String} directory
   */
  _getLookupDirectory(lookup) {
    return LOOKUP_MAP[lookup];
  }

  /**
   * Attempts to load modules by requiring them in locally. Lookup directory is
   * determined by the type of object we're loading (e.g. 'module' => ./) and the
   * name of the module (e.g. 'model-manager') which is underscored
   *
   * @method _loadModule
   * @private
   * @param {String} lookup string name of object we're looking for (e.g. 'module')
   * @param {String} name string module name
   * @returns {Object} required module
   */
  _loadModule(lookup, name) {
    let modules;

    try {
      modules = includeAll({ dirname: this._getLookupDirectory(lookup) });
    } catch (err) {
      throw new Error(`Attempted to lookup unknown ${lookup} '${name}'`);
    }

    // TODO: this will need to be configurable somehow (or better regex)
    const key = `${inflect.underscore(name)}.js`;

    return modules[key];
  }
}