builder.js

/**
 * @module builder
*/

const uri = require('../src/_uri');

const PLACEHOLDER = '$$$'; // should be safe string, not occuring in url templates, and not breaking decomposition
const REPLACE = /\$\$\$/g;

class EncodedString {
    // represents encoded String
    // new Class to be able to make instanceOf
    // needed by encode function to know if encode or not

    constructor(value) {
        this.string = `${value}`; // template to get string from value
    }
}

const ENCODERS = [ // order is important!
    [ 'path', uri.encodeSegment ],
    [ 'query', uri.encodeQuery ],
    [ 'fragment', uri.encodeFragment ]
];

function encode(value, encoder) {
    return value instanceof EncodedString ? value.string : encoder(`${value}`); // template to get string from value
}

function build(encoders, strings, ...values) {
    const uriTemplate = strings.reduce((last, actual) => `${last}${PLACEHOLDER}${actual}`);

    const decomposed = uri.decomposeComponents(uriTemplate);

    encoders.forEach(([ key, encoder ]) => {
        const value = decomposed[key];
        if (!value) { return; }
        decomposed[key] = value.replace(REPLACE, () => encode(values.shift(), encoder));
    });

    const recomposed = uri.recomposeComponents(decomposed);

    if (REPLACE.test(recomposed)) {
        throw new Error(`Params outside ${encoders.map(([ key ]) => key).join('/')} are unsupported`);
    }
    return recomposed;
}

/**
 * ES6 template literal tag, used to build URLs safely (correctly encoded path/query/segment)
 * @memberof module:builder
 * @returns {string} builded URL
 *
 * @example
 * const { uriBuilder } = require('@gjax/uri');
 * const p1 = 'a/b?c', p2 = 'a#b', p3 = 'a b';
 * const url = uriBuilder`/foo/${p1}/bar/?x=${p2}#/baz/${p3}`;
 * // RESULT: /foo/a%2Fb%3Fc/bar/?x=a%23b#/baz/a%20b
 *
 */
function uriBuilder(strings, ...values) {
    return build(ENCODERS, strings, ...values);
}

/**
 * ES6 template literal tag, used to build URLs safely (correctly encoded path/query/segment)
 * Values in query are encoded using uri.encodeRqlValue
 * @memberof module:builder
 * @returns {string} builded URL
 *
 * @example
 * const { uriBuilder, uriBuilderRql } = require('@gjax/uri');
 * const p1 = 10, p2 = 'a)b';
 *
 * const url = uriBuilder`/foo/${p1}/bar/?eq(x,${p2})`
 * // RESULT: /foo/10/bar/?eq(x,a)b)'
 *
 * const url = uriBuilderRql`/foo/${p1}/bar/?eq(x,${p2})`
 * // RESULT: /foo/10/bar/?eq(x,a%29b)'
 *
 */
function uriBuilderRql(strings, ...values) {
    const encoders = ENCODERS.slice(0);
    encoders.splice(1, 1, [ 'query', uri.encodeRqlValue ]);
    return build(encoders, strings, ...values);
}

/**
 * Use to wrap value for uriBuilder to enforce no encoding for it
 * @param value
 * @memberof module:builder
 * @returns object representing wrapped value with encoded flag.
 *
 * @example
 * const { uriBuilder, raw } = require('@gjax/uri');
 * const p1 = 'a/b?c', query = 'name=John%20Doe&age=20';
 *
 * uriBuilder`/foo/${p1}/bar/?${query}`
 * // RESULT: /foo/a%2Fb%3Fc/bar/?name=John%2520Doe&age=20'
 *
 * uriBuilder`/foo/${p1}/bar/?${raw(query)}`
 * // RESULT: /foo/a%2Fb%3Fc/bar/?name=John%20Doe&age=20
 */
function raw(string) {
    return new EncodedString(string);
}

module.exports = {
    uriBuilder,
    uriBuilderRql,
    raw
};