Source: url.js

import parse from 'url-parse';
import { getElements, getAttributeNS, find } from './utils';
import { OpenSearchParameter } from './parameter';


/**
 * @module opensearch/url
 */

/**
 * Class to parse a single URL of an OpenSearchDescription XML document and
 * to create HTTP requests for searches.
 * @property {string} type The mime-type for the content the URL is referring to
 * @property {string} url The URL template or base URL
 * @property {array} parameters The template/request parameters of the URL
 * @property {string} method The HTTP method
 * @property {string} enctype The encoding type
 * @property {Number} indexOffset the index offset of this URL
 * @property {Number} pageOffset the page offset of this URL
 */
export class OpenSearchUrl {
  /**
   * Create an OpenSearchUrl object
   * @param {string} type The mime-type for the content the URL is referring to
   * @param {string} url The URL template or base URL
   * @param {OpenSearchParameter[]} parameters The template/request parameters of the URL
   * @param {string} [method='GET'] The HTTP method
   * @param {string} [enctype='application/x-www-form-urlencoded'] The encoding type
   * @param {Number} [indexOffset=1] The index offset of this URL
   * @param {Number} [pageOffset=1] The page offset of this URL
   * @param {string[]} [relations=['results']] The relations of this URL.
   */
  constructor(type, url, parameters = [], method = 'GET',
    enctype = 'application/x-www-form-urlencoded',
    indexOffset = 1, pageOffset = 1, relations = ['results']) {
    this._type = type;
    this._url = url;
    this._method = method;
    this._enctype = enctype;
    this._indexOffset = indexOffset;
    this._pageOffset = pageOffset;
    this._relations = relations;

    this._parameters = parameters;
    this._parametersByName = {};
    this._parametersByType = {};
    parameters.forEach((param) => {
      this._parametersByType[param.type] = param;
      this._parametersByName[param.name] = param;
    });
  }

  /**
   * The mime-type for the content the URL is referring to
   * @readonly
   */
  get type() {
    return this._type;
  }

  /**
   * The URL template or base URL
   * @readonly
   */
  get url() {
    return this._url;
  }

  /**
   * The HTTP method
   * @readonly
   */
  get method() {
    return this._method;
  }

  /**
   * The encoding type
   * @readonly
   */
  get enctype() {
    return this._enctype;
  }

  /**
   * The index offset of this URL
   * @readonly
   */
  get indexOffset() {
    return this._indexOffset;
  }

  /**
   * The page offset of this URL
   * @readonly
   */
  get pageOffset() {
    return this._pageOffset;
  }

  /**
   * The page offset of this URL
   * @readonly
   */
  get relations() {
    return this._relations;
  }

  /**
   * The template/request parameters of the URL
   * @readonly
   */
  get parameters() {
    return this._parameters;
  }

  /**
   * Returns whether the URL has a template parameter of the given type
   * @param {string} type The parameter type to check
   * @returns {boolean} Whether the URL has a parameter of that type
   */
  hasParameter(type) {
    return Object.prototype.hasOwnProperty.call(this._parametersByType, type);
  }

  /**
   * Get the parameter of the specified type, if available
   * @param {string} type The parameter type to check
   * @returns {OpenSearchParameter} The parameter of the given type or null
   */
  getParameter(type) {
    return this._parametersByType[type];
  }

  /**
   * Checks whether this URL is compatible with the given parameters
   * @param {object} parameters An object mapping the name or type to the value
   * @returns {boolean} Whether or not the URL is compatible with the given parameters
   */
  isCompatible(parameters) {
    let compatible = true;
    Object.keys(parameters).forEach((key) => {
      if (!Object.prototype.hasOwnProperty.call(this._parametersByType, key)
          && !Object.prototype.hasOwnProperty.call(this._parametersByName, key)) {
        compatible = false;
      }
    });
    if (!compatible) {
      return false;
    }

    const missingMandatoryParameters = this.parameters.filter(
      parameter => parameter.mandatory
        && !Object.prototype.hasOwnProperty.call(parameters, parameter.name)
        && !Object.prototype.hasOwnProperty.call(parameters, parameter.type)
    );
    if (missingMandatoryParameters.length) {
      return false;
    }
    return true;
  }

  /**
   * Construct a {@link OpenSearchUrl} from a DOMNode
   * @param {DOMNode} node The DOM node from the OpenSearchDescription XML document
   * @returns {OpenSearchUrl} The constructed OpenSearchUrl object
   */
  static fromNode(node) {
    const parameterNodes = getElements(node, 'parameters', 'Parameter');
    const method = getAttributeNS(node, 'parameters', 'method');
    const enctype = getAttributeNS(node, 'parameters', 'enctype');
    const indexOffset = node.hasAttribute('indexOffset') ?
      parseInt(node.getAttribute('indexOffset'), 10) : 1;
    const pageOffset = node.hasAttribute('pageOffset') ?
      parseInt(node.getAttribute('pageOffset'), 10) : 1;
    const rel = node.getAttribute('rel');
    const relations = (!rel || rel === '') ? undefined : rel.split(' ');

    const parsed = parse(node.getAttribute('template'), true);
    const parametersFromTemplate = Object.keys(parsed.query)
      .map(name => OpenSearchParameter.fromKeyValuePair(name, parsed.query[name]))
      .filter(parameter => parameter);
    const parametersFromNode = parameterNodes.map(OpenSearchParameter.fromNode);

    const parametersNotInTemplate = parametersFromNode.filter(
      p1 => !find(parametersFromTemplate, p2 => p1.name === p2.name)
    ).map((param) => {
      // eslint-disable-next-line no-underscore-dangle, no-param-reassign
      param._mandatory = (typeof param.mandatory === 'undefined') ? true : param.mandatory;
      return param;
    });

    // merge parameters from node and template
    const parameters = parametersFromTemplate.map((p1) => {
      const p2 = find(parametersFromNode, p => p1.name === p.name);
      if (p2) {
        return p1.combined(p2);
      }
      return p1;
    }).concat(parametersNotInTemplate);

    return new OpenSearchUrl(
      node.getAttribute('type'), node.getAttribute('template'),
      parameters, method, enctype, indexOffset, pageOffset, relations
    );
  }

  /**
   * Construct a {@link OpenSearchUrl} from a template URL
   * @param {string} type The mime-type
   * @param {string} templateUrl The template URL string.
   * @param {string} [method='GET'] The HTTP method
   * @param {string} [enctype='application/x-www-form-urlencoded'] The encoding type
   * @returns {OpenSearchUrl} The constructed OpenSearchUrl object
   */
  static fromTemplateUrl(type, templateUrl, method = 'GET',
    enctype = 'application/x-www-form-urlencoded') {
    const parsed = parse(templateUrl, true);
    const parameters = Object.keys(parsed.query)
      .map(name => OpenSearchParameter.fromKeyValuePair(name, parsed.query[name]))
      .filter(parameter => parameter);
    return new OpenSearchUrl(type, templateUrl, parameters, method, enctype);
  }

  /**
   * Serialize the URL to a simple object.
   * @returns {object} The serialized URL
   */
  serialize() {
    return {
      type: this._type,
      url: this._url,
      method: this._method,
      enctype: this._enctype,
      indexOffset: this._indexOffset,
      pageOffset: this._pageOffset,
      relations: this._relations,
      parameters: this._parameters.map(parameter => parameter.serialize()),
    };
  }

  /**
   * Deserialize a parameter from a simple object.
   * @param {object} values The serialized URL
   * @returns {OpenSearchUrl} The deserialized URL
   */
  static deserialize(values) {
    return new OpenSearchUrl(
      values.type, values.url,
      values.parameters.map(parameterDesc => OpenSearchParameter.deserialize(parameterDesc)),
      values.method, values.enctype, values.indexOffset, values.pageOffset, values.relations
    );
  }
}