collection/request.js

var util = require('../util'),
    _ = util.lodash,
    PropertyBase = require('./property-base').PropertyBase,
    Property = require('./property').Property,
    Url = require('./url').Url,
    ProxyConfig = require('./proxy-config').ProxyConfig,
    Certificate = require('./certificate').Certificate,
    HeaderList = require('./header-list').HeaderList,
    RequestBody = require('./request-body').RequestBody,
    RequestAuth = require('./request-auth').RequestAuth,

    Request,

    /**
     * Default request method
     *
     * @private
     * @const
     * @type {String}
     */
    DEFAULT_REQ_METHOD = 'GET',

    /**
     * Content length header name
     *
     * @private
     * @const
     * @type {String}
     */
    CONTENT_LENGTH = 'Content-Length',

    /**
     * Single space
     *
     * @private
     * @const
     * @type {String}
     */
    SP = ' ',

    /**
     * Carriage return + line feed
     *
     * @private
     * @const
     * @type {String}
     */
    CRLF = '\r\n',

    /**
     * HTTP version
     *
     * @private
     * @const
     * @type {String}
     */
    HTTP_X_X = 'HTTP/X.X',

    /**
     * @private
     * @type {Boolean}
     */
    supportsBuffer = (typeof Buffer !== undefined) && _.isFunction(Buffer.byteLength),

    /**
     * Source of request body size calculation.
     * Either computed from body or used Content-Length header value.
     *
     * @private
     * @const
     * @type {Object}
     */
    SIZE_SOURCE = {
        computed: 'COMPUTED',
        contentLength: 'CONTENT-LENGTH'
    };

/**
 * @typedef Request.definition
 * @property {String|Url} url The URL of the request. This can be a {@link Url.definition} or a string.
 * @property {String} method The request method, e.g: "GET" or "POST".
 * @property {Array<Header.definition>} header The headers that should be sent as a part of this request.
 * @property {RequestBody.definition} body The request body definition.
 * @property {RequestAuth.definition} auth The authentication/signing information for this request.
 * @property {ProxyConfig.definition} proxy The proxy information for this request.
 * @property {Certificate.definition} certificate The certificate information for this request.
 */
_.inherit((

    /**
     * A Postman HTTP request object.
     *
     * @constructor
     * @extends {Property}
     * @param {Request.definition} options -
     */
    Request = function PostmanRequest (options) {
        // this constructor is intended to inherit and as such the super constructor is required to be executed
        Request.super_.apply(this, arguments);

        // if the definition is a string, it implies that this is a get of URL
        (typeof options === 'string') && (options = {
            url: options
        });

        // Create the default properties
        _.assign(this, /** @lends Request.prototype */ {
            /**
             * @type {Url}
             */
            url: new Url(),

            /**
             * @type {HeaderList}
             */
            headers: new HeaderList(this, options && options.header),

            // Although a similar check is being done in the .update call below, this handles falsy options as well.
            /**
             * @type {String}
             * @todo: Clean this up
             */
            // the negated condition is required to keep DEFAULT_REQ_METHOD as a fallback
            method: _.has(options, 'method') && !_.isNil(options.method) ?
                String(options.method).toUpperCase() : DEFAULT_REQ_METHOD
        });

        this.update(options);
    }), Property);

_.assign(Request.prototype, /** @lends Request.prototype */ {

    /**
     * Updates the different properties of the request.
     *
     * @param {Request.definition} options -
     */
    update: function (options) {
        // Nothing to do
        if (!options) { return; }

        // The existing url is updated.
        _.has(options, 'url') && this.url.update(options.url);

        // The existing list of headers must be cleared before adding the given headers to it.
        options.header && this.headers.repopulate(options.header);

        // Only update the method if one is provided.
        _.has(options, 'method') && (this.method = _.isNil(options.method) ?
            DEFAULT_REQ_METHOD : String(options.method).toUpperCase());

        // The rest of the properties are not assumed to exist so we merge in the defined ones.
        _.mergeDefined(this, /** @lends Request.prototype */ {
            /**
             * @type {RequestBody|undefined}
             */
            body: _.createDefined(options, 'body', RequestBody),

            // auth is a special case, empty RequestAuth should not be created for falsy values
            // to allow inheritance from parent
            /**
             * @type {RequestAuth}
             */
            auth: options.auth ? new RequestAuth(options.auth) : undefined,

            /**
             * @type {ProxyConfig}
             */
            proxy: options.proxy && new ProxyConfig(options.proxy),

            /**
             * @type {Certificate|undefined}
             */
            certificate: options.certificate && new Certificate(options.certificate)
        });
    },

    /**
     * Sets authentication method for the request
     *
     * @param {?String|RequestAuth.definition} type -
     * @param {VariableList=} [options] -
     *
     * @note This function was previously (in v2 of SDK) used to clone request and populate headers. Now it is used to
     * only set auth information to request
     *
     * @note that ItemGroup#authorizeUsing depends on this function
     */
    authorizeUsing: function (type, options) {
        if (_.isObject(type) && _.isNil(options)) {
            options = _.omit(type, 'type');
            type = type.type;
        }

        // null = delete request
        if (type === null) {
            _.has(this, 'auth') && (delete this.auth);

            return;
        }

        if (!RequestAuth.isValidType(type)) {
            return;
        }

        // create a new authentication data
        if (!this.auth) {
            this.auth = new RequestAuth(null, this);
        }
        else {
            this.auth.clear(type);
        }

        this.auth.use(type, options);
    },

    /**
     * Returns an object where the key is a header name and value is the header value.
     *
     * @param {Object=} options -
     * @param {Boolean} options.ignoreCase When set to "true", will ensure that all the header keys are lower case.
     * @param {Boolean} options.enabled Only get the enabled headers
     * @param {Boolean} options.multiValue When set to "true", duplicate header values will be stored in an array
     * @param {Boolean} options.sanitizeKeys When set to "true", headers with falsy keys are removed
     * @returns {Object}
     * @note If multiple headers are present in the same collection with same name, but different case
     * (E.g "x-forward-port" and "X-Forward-Port", and `options.ignoreCase` is set to true,
     * the values will be stored in an array.
     */
    getHeaders: function getHeaders (options) {
        !options && (options = {});

        // @note: options.multiValue will not be respected since, Header._postman_propertyAllowsMultipleValues
        // gets higher precedence in PropertyLists.toObject.
        // @todo: sanitizeKeys for headers by default.
        return this.headers.toObject(options.enabled, !options.ignoreCase, options.multiValue, options.sanitizeKeys);
    },

    /**
     * Calls the given callback on each Header object contained within the request.
     *
     * @param {Function} callback -
     */
    forEachHeader: function forEachHeader (callback) {
        this.headers.all().forEach(function (header) {
            return callback(header, this);
        }, this);
    },

    /**
     * Adds a header to the PropertyList of headers.
     *
     * @param {Header| {key: String, value: String}} header Can be a {Header} object, or a raw header object.
     */
    addHeader: function (header) {
        this.headers.add(header);
    },

    /**
     * Removes a header from the request.
     *
     * @param {String|Header} toRemove A header object to remove, or a string containing the header key.
     * @param {Object} options -
     * @param {Boolean} options.ignoreCase If set to true, ignores case while removing the header.
     */
    removeHeader: function (toRemove, options) {
        toRemove = _.isString(toRemove) ? toRemove : toRemove.key;

        options = options || {};

        if (!toRemove) { // Nothing to remove :(
            return;
        }

        options.ignoreCase && (toRemove = toRemove.toLowerCase());

        this.headers.remove(function (header) {
            var key = options.ignoreCase ? header.key.toLowerCase() : header.key;

            return key === toRemove;
        });
    },

    /**
     * Updates or inserts the given header.
     *
     * @param {Object} header -
     */
    upsertHeader: function (header) {
        if (!(header && header.key)) { return; } // if no valid header is provided, do nothing

        var existing = this.headers.find({ key: header.key });

        if (!existing) {
            return this.headers.add(header);
        }

        existing.value = header.value;
    },

    /**
     * Add query parameters to the request.
     *
     * @todo: Rename this?
     * @param {Array<QueryParam>|String} params -
     */
    addQueryParams: function (params) {
        this.url.addQueryParams(params);
    },

    /**
     * Removes parameters passed in params.
     *
     * @param {String|Array} params -
     */
    removeQueryParams: function (params) {
        this.url.removeQueryParams(params);
    },

    /**
     * Get the request size by computing the headers and body or using the
     * actual content length header once the request is sent.
     *
     * @returns {Object}
     */
    size: function () {
        var contentLength = this.headers.get(CONTENT_LENGTH),
            requestTarget = this.url.getPathWithQuery(),
            bodyString,
            sizeInfo = {
                body: 0,
                header: 0,
                total: 0,
                source: SIZE_SOURCE.computed
            };

        // if 'Content-Length' header is present, we take body as declared by
        // the client(postman-request or user-defined). else we need to compute the same.
        if (contentLength && util.isNumeric(contentLength)) {
            sizeInfo.body = parseInt(contentLength, 10);
            sizeInfo.source = SIZE_SOURCE.contentLength;
        }
        // otherwise, if body is defined, we calculate the length of the body
        else if (this.body) {
            // @note body.toString() returns E for formdata or file mode
            bodyString = this.body.toString();
            sizeInfo.body = supportsBuffer ? Buffer.byteLength(bodyString) :
                /* istanbul ignore next */
                bodyString.length;
        }

        // https://tools.ietf.org/html/rfc7230#section-3
        // HTTP-message   = start-line (request-line / status-line)
        //                  *( header-field CRLF )
        //                  CRLF
        //                  [ message-body ]
        // request-line = method SP request-target SP HTTP-version CRLF
        sizeInfo.header = (this.method + SP + requestTarget + SP + HTTP_X_X + CRLF + CRLF).length +
            this.headers.contentSize();

        // compute the approximate total body size by adding size of header and body
        sizeInfo.total = (sizeInfo.body || 0) + (sizeInfo.header);

        return sizeInfo;
    },

    /**
     * Converts the Request to a plain JavaScript object, which is also how the request is
     * represented in a collection file.
     *
     * @returns {{url: (*|String), method: *, header: (undefined|*), body: *, auth: *, certificate: *}}
     */
    toJSON: function () {
        var obj = PropertyBase.toJSON(this);

        // remove header array if blank
        if (_.isArray(obj.header) && !obj.header.length) {
            delete obj.header;
        }

        return obj;
    },

    /**
     * Creates a clone of this request
     *
     * @returns {Request}
     */
    clone: function () {
        return new Request(this.toJSON());
    }
});

_.assign(Request, /** @lends Request */ {
    /**
     * Defines the name of this property for internal use.
     *
     * @private
     * @readOnly
     * @type {String}
     */
    _postman_propertyName: 'Request',

    /**
     * Check whether an object is an instance of {@link ItemGroup}.
     *
     * @param {*} obj -
     * @returns {Boolean}
     */
    isRequest: function (obj) {
        return Boolean(obj) && ((obj instanceof Request) ||
            _.inSuperChain(obj.constructor, '_postman_propertyName', Request._postman_propertyName));
    }
});

module.exports = {
    Request
};