restocat/restocat

View on GitHub
lib/router/Route.js

Summary

Maintainability
A
45 mins
Test Coverage
const {pathToRegexp} = require('path-to-regexp');
const httpErrors = require('../httpErrors');

class Route {
  constructor(locator, descriptionCollection, handleName, endpoint) {
    this._locator = locator;
    this._collectionsLoader = this._locator.resolve('collectionsLoader');
    this._events = this._locator.resolve('events');
    this._definedRoutes = locator.resolve('definedRoutes');

    this.init(descriptionCollection, handleName, endpoint);
  }

  init(descriptionCollection, handleName, endpoint) {
    const parts = endpoint.split(' ');

    if (parts.length < 2) {
      throw new httpErrors.InternalServerError(`Invalid endpoint '${endpoint}'. Endpoint should be contain method and route (ex. 'get /some/:id')`);
    }

    const method = parts[0].toLowerCase();
    let path = [this._definedRoutes[descriptionCollection.name], parts[1]].join('/').replace(/(\/)\/+/g, '$1');

    if (path[path.length - 1] === '/' && path.length !== 1) {
      path = path.slice(0, -1);
    }

    this.descriptor = descriptionCollection;
    this.collectionName = descriptionCollection.name;
    this.handleName = handleName;
    this.method = method;
    this.path = path;
    this.keys = [];
    this.regexp = pathToRegexp(this.path, this.keys);

    if (this.path === '/') {
      this.fast_slash = true;
    }
  }

  async handle(context) {
    this._events.emit('debug', `Call handle ${this.handleName} in ${this.collectionName}`);

    const constructor = this.descriptor.constructor;

    if (typeof constructor !== 'function') {
      throw new httpErrors.InternalServerError(`Constructor for collection ${this.collectionName} must be a function`);
    }

    constructor.prototype.$context = context;

    const instance = this._getInstance(constructor, context);

    if (instance instanceof Error) {
      throw new httpErrors.InternalServerError(instance);
    }

    if (!instance[this.handleName]) {
      const error = `Not found handler '${this.handleName}' in collection's logic file '${this.collectionName}'`;

      this._events.emit('error', error);

      throw new httpErrors.InternalServerError(error);
    }

    instance.$context = context;

    return await instance[this.handleName]();
  }

  _getInstance(constructor, context) {
    try {
      return new constructor(context.locator);
    } catch (error) {
      if (!(error instanceof Error)) {
        return new Error(error);
      }

      return error;
    }
  }

  match(path) {
    if (!path) {
      return false;
    }

    if (path === '/' && this.fast_slash) {
      return Object.create(null);
    }

    const results = this.regexp.exec(path);

    if (!results) {
      return false;
    }

    const params = Object.create(null);
    const keys = this.keys;

    for (let i = 1; i < results.length; i++) {
      const key = keys[i - 1];
      const prop = key.name;
      const value = decode_param(results[i]);

      if (value !== undefined || !(hasOwnProperty.call(params, prop))) {
        params[prop] = value;
      }
    }

    return params;
  }

  /**
   * Сreate a string representation of current route
   *
   * @returns {String} representation of current route
   */
  toString() {
    return `${this.method} ${this.path} | ${this.collectionName}.${this.handleName}`;
  }
}

/**
 * Decode param value.
 *
 * @param {string} value part of uri
 * @return {string} Decoded string
 * @private
 */
function decode_param(value) {
  if (typeof value !== 'string' || value.length === 0) {
    return value;
  }

  try {
    return decodeURIComponent(value);
  } catch (err) {
    if (err instanceof URIError) {
      err.message = `Failed to decode param '${value}'`;
      err.status = 400;
    }

    throw err;
  }
}

module.exports = Route;