src/http/HttpTransport.js

import * as _ from "lamb";
import request from "superagent";

const defaultOptions = {
    authParams: [],
    baseURL: "",
    commonHeaders: {},
    transformer: _.identity,
    withCredentials: false
};

/**
 * Ensure that a string starts with a leading slash.
 * @private
 * @param {String} s
 * @returns {String}
 */
const ensureLeadingSlash = s => s.replace(/^([^/])/, "/$1");

/**
 * A very naive object cloner. Takes into account arrays of simple values and plain objects only.
 * @private
 * @function
 * @param {Array|Object} source
 * @returns {Array|Object}
 */
const naiveClone = _.adapter([
    _.casus(Array.isArray, _.drop(0)),
    _.casus(_.isType("Object"), obj => _.mapValues(obj, naiveClone)),
    _.identity
]);

/**
 * Simple HTTP transport wrapping [superagent]{@link http://visionmedia.github.io/superagent/}.
 * @memberof module:@cgnal/net/http
 * @since 0.0.1
 */
class HttpTransport {
    /**
     * @param {Object} options
     * @param {Array} [options.authParams=[]] Values to use in {@link http://visionmedia.github.io/superagent/#authentication|authentication}.
     * @param {String} [options.baseURL=""] The base URL of the HTTP transport.
     * @param {Object} [options.commonHeaders={}] The HTTP headers that will be used for all requests.
     * @param {Function} [options.transformer=x => x] A transformer function for the returned requests. Defaults to the {@link https://ascartabelli.github.io/lamb/module-lamb.html#identity|identity} function.
     * @param {Boolean} [options.withCredentials=false] Whether to use the {@link https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials|withCredentials} flag or not.
     */
    constructor (options = {}) {
        options = naiveClone({
            ...defaultOptions,
            ...options
        });

        this.authParams = options.authParams;
        this.baseURL = options.baseURL.replace(/\/$/, "");
        this.commonHeaders = options.commonHeaders;
        this.transformer = options.transformer;
        this.withCredentials = options.withCredentials;
    }

    /**
     * @private
     * @param {String} verb
     * @param {String} endpoint
     * @param {Object} headers
     * @param {Object} params
     * @param {FormData|Object} body
     * @returns {SuperAgentRequest}
     */
    _buildRequest (verb, endpoint, headers, params, body) {
        const req = request(
            verb,
            this.baseURL + ensureLeadingSlash(endpoint)
        ).set({
            ...this.commonHeaders,
            ...headers
        });

        req.query(params);

        if (body) {
            req.send(body);
        }

        if (this.withCredentials) {
            req.withCredentials();
        }

        if (this.authParams.length) {
            req.auth(...this.authParams);
        }

        return this.transformer(req);
    }

    /**
     * Adds (or replaces) a header with the given name and value.
     * @param {String} name
     * @param {String} value
     * @returns {Object} The common headers after the addition.
     */
    addHeader (name, value) {
        this.commonHeaders = _.setIn(this.commonHeaders, name, value);

        return this.commonHeaders;
    }

    /**
     * Builds a DELETE request.
     * @param {String} endpoint
     * @param {Object} [params={}]
     * @param {Object} [headers={}]
     * @returns {SuperAgentRequest}
     */
    delete (endpoint, params = {}, headers = {}) {
        return this._buildRequest("DELETE", endpoint, headers, params);
    }

    /**
     * Builds a GET request.
     * @param {String} endpoint
     * @param {Object} [params={}]
     * @param {Object} [headers={}]
     * @returns {SuperAgentRequest}
     */
    get (endpoint, params = {}, headers = {}) {
        return this._buildRequest("GET", endpoint, headers, params);
    }

    /**
     * Builds a HEAD request.
     * @param {String} endpoint
     * @param {Object} [params={}]
     * @param {Object} [headers={}]
     * @returns {SuperAgentRequest}
     */
    head (endpoint, params = {}, headers = {}) {
        return this._buildRequest("HEAD", endpoint, headers, params);
    }

    /**
     * Builds a PATCH request.
     * @param {String} endpoint
     * @param {Object} [params={}]
     * @param {FormData|Object} [body={}]
     * @param {Object} [headers={}]
     * @returns {SuperAgentRequest}
     */
    patch (endpoint, params = {}, body = {}, headers = {}) {
        return this._buildRequest("PATCH", endpoint, headers, params, body);
    }

    /**
     * Builds a POST request.
     * @param {String} endpoint
     * @param {Object} [params={}]
     * @param {FormData|Object} [body={}]
     * @param {Object} [headers={}]
     * @returns {SuperAgentRequest}
     */
    post (endpoint, params = {}, body = {}, headers = {}) {
        return this._buildRequest("POST", endpoint, headers, params, body);
    }

    /**
     * Builds a PUT request.
     * @param {String} endpoint
     * @param {Object} [params={}]
     * @param {FormData|Object} [body={}]
     * @param {Object} [headers={}]
     * @returns {SuperAgentRequest}
     */
    put (endpoint, params = {}, body = {}, headers = {}) {
        return this._buildRequest("PUT", endpoint, headers, params, body);
    }

    /**
     * Removes a HTTP header.
     * @param {String} name
     * @returns {Object} The common headers after the removal.
     */
    removeHeader (name) {
        this.commonHeaders = _.skipIn(this.commonHeaders, [name]);

        return this.commonHeaders;
    }
}

export default HttpTransport;