context.js

import autobind from 'autobind-decorator';
import Promise from 'bluebird';
import isCallable from 'is-callable';
import DependencyError from './error';
import Provider from './provider';

/**
 * @class
 */
class Context {
  static _repo = {};

  /**
   * Finds a context instance with the given name and return it.
   * Returns `undefined` if not found.
   *
   * @param {string} name The context's name.
   * @returns {(Context|undefined)} The context instance.
   */
  static get(name) {
    return Context._repo[name];
  }

  /**
   * Removes a context instance with the given name from the repository.
   *
   * @param {string} name The context's name.
   */
  static delete(name) {
    delete Context._repo[name];
  }

  /**
   * Creates new context instance.
   *
   * @param {string} name The context's name. This can be later used to fetch the instance from the repository.
   *                      See {@link Context.get}, and {@link Context.delete}
   */
  constructor(name) {
    this.name = name;

    if (name) {
      Context._repo[name] = this;
    }
  }

  _providers = {};

  /**
   * Creates and returns a new provider instance.
   * The instance will be used as a provider for the dependency with the `key`.
   * That is, when resolving dependency with the `key`, the returned provider will be used.
   * See {@link Provider} API to customize provision settings.
   *
   * @param {string} key The key to register the provider.
   * @returns {Provider} The new provider instance.
   *
   * @example
   * context.provide('logger').as(console.log);
   */
  @autobind
  provide(key) {
    const provider = new Provider(this);
    this._providers[key] = provider;
    return provider;
  }

  /**
   * Finds and returns the depedency with the given `key`.
   *
   * @param {string} key The dependency's key.
   * @returns {Promise<any>} The resolved dependency.
   * @throws {DependencyError} Throws when no valid provider has been registered for the `key`.
   *
   * @example
   * context.resolve('logger').then(log => log('Hello world'));
   */
  @autobind
  resolve(key) {
    const provider = this._providers[key];

    if (provider === undefined) {
      return Promise.reject(new DependencyError(`Cannot find provider for: ${key}`));
    } else {
      return Promise.resolve(provider.get());
    }
  }

  /**
   * Resolves any dependencies that match the given condition.
   *
   * @param {Condition} [cond] The condition to match.
   * @return {Promise<any[]>} The dependencies matching the condition.
   *
   * @example
   * context.resolveAll(['log', 'debug']).then(([log, debug]) => {
   *   debug('Print greeting message');
   *   log('Hello world');
   * });
   */
  @autobind
  resolveAll(cond) {
    if (Array.isArray(cond)) {
      return Promise.resolve(cond)
        .map(c => this.resolveAll(c))
        .reduce((acc, item) => [...acc, ...item], [])
        .filter(values => !!values);
    }

    if (cond instanceof RegExp) {
      return Promise.resolve(Object.keys(this._providers))
        .filter(key => cond.test(key))
        .map(key => this.resolve(key))
        .filter(values => !!values);
    }

    if (isCallable(cond)) {
      return Promise.resolve(Object.keys(this._providers))
        .filter(cond)
        .map(key => this.resolve(key))
        .filter(values => !!values);
    }

    if (cond === undefined || cond === true) {
      return Promise.resolve(Object.keys(this._providers))
        .map(key => this.resolve(key))
        .filter(values => !!values);
    }

    if (cond === null || cond === false) {
      return Promise.resolve([]);
    }

    return this.resolve(cond)
      .then(value => [value])
      .filter(values => !!values);
  }

  /**
   * Wraps function to automatically resolve dependencies and inject into arguments.
   *
   * @param {Condition} cond
   * @param {Function} consumer
   *
   * @example
   * const greet = context.using(['log', 'debug'], (log, debug, name) => {
   *   debug('Print greeting message');
   *   log(`Hello ${name}`);
   * });
   *
   * greet('world');
   */
  @autobind
  using(cond, consumer) {
    return (...ownArgs) => this.resolveAll(cond).then(values => consumer(...values, ...ownArgs));
  }
}

export default Context;