Home Reference Source

src/kindergarten/Purpose.js

import {
  each,
  isEmpty,
  isFunction,
  isString
} from 'lodash';

import AllowedMethodsService from './utils/AllowedMethodsService';
import BaseObject from './BaseObject';
import Logger from './Logger';
import {
  isPerimeter,
  isSandbox
} from './utils';
import {
  ArgumentError,
  NoExposedMethodError,
  RestrictedMethodError
} from './errors';

/**
 * Definition of Purpose class.
 *
 * Purpose is a connection between Sandbox and Perimeter.
 * Whenever a Perimeter is added to a Sandbox new Purpose is created.
 * And all exposed methods from Perimeter and copied to Purpose.
 * Purpose should have as less methods as possible.
 * Purpose is used internally by Sandbox and shouldn't be used as standalone
 * object.
 */
export default class Purpose extends BaseObject {
  /**
   * Create new instance of purpose.
   */
  constructor(name, sandbox) {
    super();

    this._name = name;
    this._sandbox = sandbox;

    if (!isString(this._name)) {
      throw new ArgumentError(
        'Purpose must have a name.'
      );
    }

    if (!isSandbox(this._sandbox)) {
      throw new ArgumentError(
        'Purpose must have a sandbox.'
      );
    }
  }

  /**
   * Load perimeter & copy all exposed method into the purpose.
   * This method is used internally by Sandbox and it is not recommended to use
   * it externally.
   */
  _loadPerimeter(perimeter) {
    if (!isPerimeter(perimeter)) {
      throw new ArgumentError(
        'Cannot load perimeter. Is it an instance of perimeter?'
      );
    }

    const exposedMethods = perimeter.expose;
    const allowedMethodsService = new AllowedMethodsService(this, false);

    if (isEmpty(exposedMethods)) return;

    each(exposedMethods, (exposedMethod) => {
      if (allowedMethodsService.isRestricted(exposedMethod)) {
        throw new RestrictedMethodError(
          `Cannot create a method ${exposedMethods}. It is restricted.`
        );
      }

      if (isFunction(this[exposedMethod])) {
        Logger.warn(`Overriding already sandboxed method ${this._name}.${exposedMethod}.`);
      }

      if (!isFunction(perimeter[exposedMethod])) {
        throw new NoExposedMethodError(
          `The exposed method ${exposedMethod} is not defined on perimeter ${perimeter.purpose}.`
        );
      }

      // Call the method in context of perimeter and governed by a governess
      this[exposedMethod] = (...args) => perimeter.governed(
        perimeter[exposedMethod],
        args,
        perimeter
      );
    });
  }

  /**
   * Return true if perimeter is allowed to perform an action. It uses the
   * governess of the perimeter.
   */
  isAllowed(...args) {
    const perimeter = this._sandbox.getPerimeter(this._name);
    return perimeter.isAllowed(...args);
  }

  /**
   * Return true if perimeter is not allowed to perform an action. It uses the
   * governess of the perimeter.
   */
  isNotAllowed(...args) {
    return !this.isAllowed(...args);
  }
}