Home Reference Source Repository

src/index.js

import validate from './validate'
import { addQueryParams, mixinDynamicSegmentsValues } from './utils/url'

const identity = x => x

const defaultParams = {
  executor: null,
  url: null,
  method: null,
  headers: [],
  body: null,
  responseType: null,
  dynamicSegments: [],
  queryParams: [],
  bodyProcessor: identity,
  responseProcessor: identity
}

class Internals {
  constructor (params) {
    Object.entries(params)
      .map(([key, value]) =>
        Object.defineProperty(this, key, {
          get: () => value,
          enumerable: true,
          configurable: true
        }))
  }
}

/**
 * @typedef {[key: string, value: string]} Tuple
 */

 /**
  * @typedef {Object} HttpRequestParams
  * @property {string} url resource url
  * @property {string} method HTTP method,
  * @property {Tuple[]} headers array of HTTP headers key-value pairs
  * @property {Object} body request payload
  * @property {string} responseType response type
  * @property {Tuple[]} dynamicSegments key-value array of replacements for the provided url
  * @property {Tuple[]} queryParams key-value array of url params
  * @property {function(x: Object): Object} bodyProcessor map body function
  * @property {function(x: Object): Object} responseProcessor map response function
  * @property {function(url: string, method: string, headers: string[], responseType: string, body: Object): Promise} executor performs HTTP request and returns Promise,
  */

// executor: function(url: string, method: string, headers: string[], responseType: string, body: Object): Promise,
// bodyProcessor: function(x: Object): Object, responseProcessor: function(x: Object): Object

/**
 * Http request object.
 * Expose chainable API
 */
export default class Http {

  /**
   * @param {HttpRequestParams} params - HTTP request params
   */
  constructor (params = defaultParams) {
    const internals = new Internals(params)
    this.internals = () => internals
  }

  /**
   * Set the middleware that will perform the request
   * @param {function(url: string, method: string, headers: string[], responseType: string, body: Object): Promise} executor -
   * function which performs the request asynchroniously and returns Promise back
   * @returns {Object} Http object
   */
  executor (executor) {
    return new Http(Object.assign({}, this.internals(), { executor }))
  }

  /**
   * Adds URL information to HTTP request model
   * @param {string} url - URL
   * @returns {Object} Http object
   */
  url (url) {
    return new Http(Object.assign({}, this.internals(), { url }))
  }

  /**
   * Adds HTTP method information to request model
   * @param {string} method - HTTP method
   * @returns {Object} Http object
   */
  method (method) {
    return new Http(Object.assign({}, this.internals(), { method }))
  }

  /**
   * Adds header to request model
   * @param {string} header - valid header key
   * @param {string} value - valid header value
   * @returns {Object} Http object
   */
  header (header, value) {
    const headers = this.internals().headers.concat([[header, value]])
    return new Http(Object.assign({}, this.internals(), { headers }))
  }

  /**
   * Adds body to request model
   * @param {Object} body - request payload
   * @returns {Object} Http object
   */
  body (body) {
    return new Http(Object.assign({}, this.internals(), { body }))
  }

  /**
   * Sets response content type
   * Proper values could be obtained form XmlHttpRequest specification
   * https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest#Properties
   * @param {string} responseType - Proper values could be obtained form
   * XmlHttpRequest specification
   * https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest#Properties
   * @returns {Object} Http object
   */
  responseType (responseType) {
    return new Http(Object.assign({}, this.internals(), { responseType }))
  }

  /**
   * Adds dynamic segment value
   * @param {string} segment - segment key
   * @param {string} value - segment value
   * @returns {Object} Http object
   */
  segment (segment, value) {
    const dynamicSegments
      = this.internals().dynamicSegments.concat([[segment, value]])
    return new Http(Object.assign({}, this.internals(), { dynamicSegments }))
  }

  /**
   * Adds query string param
   * @param {string} name - param key
   * @param {string} value - param value
   * @returns {Object} Http object
   */
  query (name, value) {
    const queryParams = this.internals().queryParams.concat([[name, value]])
    return new Http(Object.assign({}, this.internals(), { queryParams }))
  }

  /**
   * Sets the function which gets the body object as a parameter
   * which result would be used as a request body
   * @param {func} bodyProcessor - f(x) => valid_http_body
   * @returns {Object} Http object
   */
  bodyProcessor (bodyProcessor) {
    return new Http(Object.assign({}, this.internals(), { bodyProcessor }))
  }

  /**
   * Sets the function which gets the response and produces another value
   * Useful for default HTTP error handling
   *
   * @example
   * response => {
   *  switch(response.code) {
   *    case "404":
   *      return { message: 'Resource not found' }
   *  }
   *}
   *
   * @param {func} responseProcessor - f(x) => y
   * @returns {Object} Http object
   */
  responseProcessor (responseProcessor) {
    return new Http(Object.assign({}, this.internals(), { responseProcessor }))
  }

  /**
   * Executes HTTP request
   * @returns {Object} - Promise
   */
  exec () {
    const {
      url, method, headers, responseType, dynamicSegments, queryParams, body,
      bodyProcessor, responseProcessor, executor
    } = this.internals()
    if (!executor) {
      throw new Error('executor was not set')
    }
    const errors = validate(url, method, headers, responseType)
    if (errors.length !== 0)
      throw new Error(errors.join('\n'))
    const urlWithDynamicSegments
      = mixinDynamicSegmentsValues(url, dynamicSegments)
    const fullUrl = addQueryParams(urlWithDynamicSegments, queryParams)
    return executor(
      fullUrl, method, headers, responseType, bodyProcessor(body)
    ).then(responseProcessor)
  }
}