base/api.js

const base = require("./base");
const compat = require("./compat");
require("./ripe");
const ripe = base.ripe;
const fetch = compat.fetch;
const XMLHttpRequest = compat.XMLHttpRequest;

/**
 * @class
 * @classdesc The API class to be instantiated. Implements all the API interfaces.
 * @param {Object} options The options to be used to configure the API client.
 * @returns {Ripe} The newly created RipeAPI object.
 */
ripe.RipeAPI = function(options = {}) {
    options.cached = typeof options.cached === "undefined" ? false : options.cached;
    options.noBundles = typeof options.noBundles === "undefined" ? true : options.noBundles;
    return new ripe.Ripe(options);
};

/**
 * Runs a simple "ping" operation to validate the connection with the
 * Core server-side.
 *
 * @param {Object} options An object of options to configure the request.
 * @param {Function} callback Function with the result of the request.
 * @returns {XMLHttpRequest} The XMLHttpRequest instance of the API request.
 */
ripe.Ripe.prototype.ping = function(options, callback) {
    callback = typeof options === "function" ? options : callback;
    options = typeof options === "function" || options === undefined ? {} : options;
    const url = `${this.url}ping`;
    options = Object.assign(options, {
        url: url,
        method: "GET"
    });
    options = this._build(options);
    return this._cacheURL(options.url, options, callback);
};

ripe.Ripe.prototype.pingP = function(options) {
    return new Promise((resolve, reject) => {
        this.ping(options, (result, isValid, request) => {
            isValid ? resolve(result) : reject(new ripe.RemoteError(request, null, result));
        });
    });
};

/**
 * Retrieves summary information about the Core server-side
 * (such as version, description and others).
 *
 * @param {Object} options An object of options to configure the request.
 * @param {Function} callback Function with the result of the request.
 * @returns {XMLHttpRequest} The XMLHttpRequest instance of the API request.
 */
ripe.Ripe.prototype.info = function(options, callback) {
    callback = typeof options === "function" ? options : callback;
    options = typeof options === "function" || options === undefined ? {} : options;
    const url = `${this.url}info`;
    options = Object.assign(options, {
        url: url,
        method: "GET"
    });
    options = this._build(options);
    return this._cacheURL(options.url, options, callback);
};

/**
 * Retrieves summary information about the Core server-side
 * (such as version, description and others).
 *
 * @param {Object} options An object of options to configure the request.
 * @returns {Promise} Summary information of the RIPE server.
 */
ripe.Ripe.prototype.infoP = function(options) {
    return new Promise((resolve, reject) => {
        this.info(options, (result, isValid, request) => {
            isValid ? resolve(result) : reject(new ripe.RemoteError(request, null, result));
        });
    });
};

/**
 * Runs the GeoIP resolution process so that it's possible to uncover
 * more geographical information about the current user.
 *
 * @param {String} address The optional address to be used in case the address
 * of the IP request is not desired.
 * @param {Object} options An object of options to configure the request.
 * @param {Function} callback Function with the result of the request.
 * @returns {XMLHttpRequest} The XMLHttpRequest instance of the API request.
 */
ripe.Ripe.prototype.geoResolve = function(address, options, callback) {
    callback = typeof options === "function" ? options : callback;
    options = typeof options === "function" || options === undefined ? {} : options;
    const url = `${this.url}geo_resolve`;
    options = Object.assign(options, {
        url: url,
        method: "GET",
        params: {
            address: address
        }
    });
    options = this._build(options);
    return this._cacheURL(options.url, options, callback);
};

ripe.Ripe.prototype.geoResolveP = function(address, options) {
    return new Promise((resolve, reject) => {
        this.geoResolve(address, options, (result, isValid, request) => {
            isValid ? resolve(result) : reject(new ripe.RemoteError(request, null, result));
        });
    });
};

/**
 * Retrieves the complete set of session elements to be used, such as:
 * the 'sid ', 'session_id', 'username ', 'name', 'email' and 'tokens'.
 *
 * @param {String} username The username to authenticate.
 * @param {String} password The username's password.
 * @param {Object} options An object of options to configure the request.
 * @param {Function} callback Function with the result of the request.
 * @returns {XMLHttpRequest} The XMLHttpRequest instance of the API request.
 */
ripe.Ripe.prototype.signin = function(username, password, options, callback) {
    callback = typeof options === "function" ? options : callback;
    options = typeof options === "function" || options === undefined ? {} : options;
    const url = `${this.url}signin`;
    options = Object.assign(options, {
        url: url,
        method: "POST",
        params: {
            username: username,
            password: password
        }
    });
    options = this._build(options);
    return this._cacheURL(options.url, options, callback);
};

ripe.Ripe.prototype.signinP = function(username, password, options) {
    return new Promise((resolve, reject) => {
        this.signin(username, password, options, (result, isValid, request) => {
            isValid ? resolve(result) : reject(new ripe.RemoteError(request, null, result));
        });
    });
};

/**
 * Retrieves the complete set of session elements to be used, such as:
 * the 'sid ', 'session_id', 'username ', 'name', 'email' and 'tokens'.
 *
 * This strategy uses the admin back-end for authentication.
 *
 * @param {String} username The username to authenticate.
 * @param {String} password The username's password.
 * @param {Object} options An object of options to configure the request.
 * @param {Function} callback Function with the result of the request.
 * @returns {XMLHttpRequest} The XMLHttpRequest instance of the API request.
 */
ripe.Ripe.prototype.signinAdmin = function(username, password, options, callback) {
    callback = typeof options === "function" ? options : callback;
    options = typeof options === "function" || options === undefined ? {} : options;
    this.username = username;
    this.password = password;
    const url = `${this.url}signin_admin`;
    options = Object.assign(options, {
        url: url,
        method: "POST",
        params: {
            username: username,
            password: password
        }
    });
    options = this._build(options);
    return this._cacheURL(options.url, options, callback);
};

ripe.Ripe.prototype.signinAdminP = function(username, password, options) {
    return new Promise((resolve, reject) => {
        this.signinAdmin(username, password, options, (result, isValid, request) => {
            isValid ? resolve(result) : reject(new ripe.RemoteError(request, null, result));
        });
    });
};

/**
 * Retrieves the complete set of session elements to be used, such as:
 * the 'sid ', 'session_id', 'username ', 'name', 'email'.
 *
 * @param {String} token The authentication token.
 * @param {Object} options An object of options to configure the request.
 * @param {Function} callback Function with the result of the request.
 * @returns {XMLHttpRequest} The XMLHttpRequest instance of the API request.
 */
ripe.Ripe.prototype.signinPid = function(token, options, callback) {
    callback = typeof options === "function" ? options : callback;
    options = typeof options === "function" || options === undefined ? {} : options;
    this.pid = token;
    const url = `${this.url}signin_pid`;
    options = Object.assign(options, {
        url: url,
        method: "POST",
        params: {
            token: token
        }
    });
    options = this._build(options);
    return this._cacheURL(options.url, options, callback);
};

ripe.Ripe.prototype.signinPidP = function(token, options) {
    return new Promise((resolve, reject) => {
        this.signinPid(token, options, (result, isValid, request) => {
            isValid ? resolve(result) : reject(new ripe.RemoteError(request, null, result));
        });
    });
};

/**
 * Retrieves the price for current customization.
 *
 * @param {Object} options An Object containing customization information that
 * can be used to override the current customization, allowing to set the
 * 'brand', 'model' and 'parts'.
 * @param {Function} callback Function with the result of the request.
 * @returns {XMLHttpRequest} The XMLHttpRequest instance of the API request.
 */
ripe.Ripe.prototype.getPrice = function(options, callback) {
    callback = typeof options === "function" ? options : callback;
    options = typeof options === "function" || options === undefined ? {} : options;
    options = this._getPriceOptions(options);
    options = this._build(options);
    return this._cacheURL(options.url, options, callback);
};

ripe.Ripe.prototype.getPriceP = function(options) {
    return new Promise((resolve, reject) => {
        this.getPrice(options, (result, isValid, request) => {
            isValid ? resolve(result) : reject(new ripe.RemoteError(request, null, result));
        });
    });
};

/**
 * Retrieves the price for a set of customizations.
 *
 * @param {Object} options An Object containing customization information that
 * can be used not only to override the current customization, allowing to set
 * the brand', 'model', but also to provide the config list to fetch the price
 * for.
 * @param {Function} callback Function with the result of the request.
 * @returns {XMLHttpRequest} The XMLHttpRequest instance of the API request.
 */
ripe.Ripe.prototype.getPrices = function(options, callback) {
    callback = typeof options === "function" ? options : callback;
    options = typeof options === "function" || options === undefined ? {} : options;
    options = this._getPricesOptions(options);
    options = this._build(options);
    return this._cacheURL(options.url, options, callback);
};

ripe.Ripe.prototype.getPricesP = function(options) {
    return new Promise((resolve, reject) => {
        this.getPrices(options, (result, isValid, request) => {
            isValid ? resolve(result) : reject(new ripe.RemoteError(request, null, result));
        });
    });
};

/**
 * Returns the video of a model's customization.
 *
 * @param {Object} options An object containing the information required
 * to get a video for a model, more specifically `brand`, `model`, `name`
 * of the video and `p` containing the model's customization.
 * @param {Function} callback Function with the result of the request.
 * @returns {XMLHttpRequest} The XMLHttpRequest instance of the API request.
 */
ripe.Ripe.prototype.getVideo = function(options, callback) {
    callback = typeof options === "function" ? options : callback;
    options = typeof options === "function" || options === undefined ? {} : options;
    options = this._getVideoOptions(options);
    options = this._build(options);
    return this._cacheURL(options.url, options, callback);
};

/**
 * Returns the video of a model's customization.
 *
 * @param {Object} options An object containing the information required
 * to get a video for a model, more specifically `brand`, `model`, `name`
 * of the video and `p` containing the model's customization.
 * @param {Function} callback Function with the result of the request.
 * @returns {Promise} The URL path to the video.
 */
ripe.Ripe.prototype.getVideoP = function(options) {
    return new Promise((resolve, reject) => {
        this.getVideo(options, (result, isValid, request) => {
            isValid ? resolve(result) : reject(new ripe.RemoteError(request, null, result));
        });
    });
};

/**
 * Returns the video thumbnail image of a model's customization.
 *
 * @param {Object} options An object containing the information required
 * to get the thumbnail of a video for a model, more specifically `brand`,
 * `model`, `name` of the video and `p` containing the model's customization.
 * @param {Function} callback Function with the result of the request.
 * @returns {XMLHttpRequest} The XMLHttpRequest instance of the API request.
 */
ripe.Ripe.prototype.getVideoThumbnail = function(options, callback) {
    callback = typeof options === "function" ? options : callback;
    options = typeof options === "function" || options === undefined ? {} : options;
    options = this._getVideoThumbnailOptions(options);
    options = this._build(options);
    return this._cacheURL(options.url, options, callback);
};

/**
 * Returns the video thumbnail image of a model's customization.
 *
 * @param {Object} options An object containing the information required
 * to get a video for a model, more specifically `brand`, `model`, `name`
 * of the video and `p` containing the model's customization.
 * @param {Function} callback Function with the result of the request.
 * @returns {Promise} The URL path to the video thumbnail image.
 */
ripe.Ripe.prototype.getVideoThumbnailP = function(options) {
    return new Promise((resolve, reject) => {
        this.getVideoThumbnail(options, (result, isValid, request) => {
            isValid ? resolve(result) : reject(new ripe.RemoteError(request, null, result));
        });
    });
};

/**
 * @ignore
 */
ripe.Ripe.prototype._cacheURL = function(url, options, callback) {
    // runs the defaulting operation on the provided options
    // optional parameter (ensures valid object there)
    callback = typeof options === "function" ? options : callback;
    options = typeof options === "function" || options === undefined ? {} : options;

    // builds the (base) key value for the provided value
    // from options or used the default one
    const key = options.key || "default";

    // creates the full key by adding the base key to the
    // URL value (including query string), this is unique
    // assuming no request payload
    const query = this._buildQuery(options.params || {});
    const fullKey = key + ":" + url + ":" + query;

    // determines the number of retries left for the request operation
    // this is going to be used in case there's an auth related error
    let retries = options.retries;
    retries = typeof retries === "undefined" ? 1 : retries;

    // determines if the current request should be cached, obeys
    // some of the basic rules for that behaviour
    let cached = options.cached;
    cached = typeof cached === "undefined" ? this.options.cached : cached;
    cached = typeof cached === "undefined" ? true : cached;
    cached = cached && !options.force && ["GET"].indexOf(options.method || "GET") !== -1;

    // determines the correct callback to be called once an auth
    // related problem occurs, fallsback to global `authCallback`
    // and finally to the base one in case none is passed
    // via options object or global SDK options
    const authCallback = options.authCallback || this.authCallback || this._authCallback;

    // determines if the cache entry should be invalidated before
    // making the request, it should only be invalidate in case the
    // the current request is being done with the cached flag
    let invalidate = options.invalidate;
    invalidate = typeof invalidate === "undefined" ? false : invalidate;
    invalidate = cached && invalidate;

    // initializes the cache object in the current instance
    // in case it does not exists already
    this._cache = this._cache === undefined ? {} : this._cache;

    // in case the invalidate flag is set then the cache for the
    // current key should be removed (invalidated)
    if (invalidate) delete this._cache[fullKey];

    // in case there's already a valid value in cache,
    // retrieves it and calls the callback with the value
    if (this._cache[fullKey] !== undefined && cached) {
        if (callback) callback(this._cache[fullKey], true, null);
        return null;
    }

    // otherwise runs the "normal" request URL call and
    // sets the result cache key on return
    return this._requestURL(url, options, (result, request, flags = {}) => {
        // unpacks the flags parameters into the components that
        // are going to be used for processing
        const { isValid, isAuthError } = flags;

        // in case the error found in the request qualifies as an
        // authentication one and there are retries left then tries
        // the authentication callback and retries the request
        if (isAuthError && retries > 0) {
            // returns `authCallback` allowing the authentication
            // to be done inside the callback and to return the
            // request result after a successful auth
            return authCallback(extraParams => {
                options.retries = retries - 1;
                options.params = { ...options.params, ...extraParams };
                return this._cacheURL(url, options, callback);
            });
        }

        // in case the result of the request is valid and caching
        // has been request set the cache value for the full key
        // with the result of the request
        if (isValid && cached) this._cache[fullKey] = result;

        // in case a callback is set then calls it with the expected
        // set of parameters (should include the original request object)
        if (callback) callback(result, isValid, request);
    });
};

/**
 * @ignore
 */
ripe.Ripe.prototype._requestURL = function(url, options, callback) {
    if (typeof fetch !== "undefined") return this._requestURLFetch(url, options, callback);
    else return this._requestURLLegacy(url, options, callback);
};

/**
 * @ignore
 */
ripe.Ripe.prototype._requestURLFetch = function(url, options, callback) {
    callback = typeof options === "function" ? options : callback;
    options = typeof options === "function" || options === undefined ? {} : options;

    const context = this;
    const method = options.method || "GET";
    const params = options.params || {};
    const headers = options.headers || {};
    let data = options.data || null;
    const dataJ = options.dataJ || null;
    const dataM = options.dataM || null;
    let contentType = options.contentType || null;
    const validCodes = options.validCodes || [200];
    const authErrorCodes = options.authErrorCodes || [401, 403, 440, 499];
    const credentials = options.credentials || "omit";
    const keepAlive = options.keepAlive === undefined ? true : options.keepAlive;

    const query = this._buildQuery(params);
    const isEmpty = ["GET", "DELETE"].indexOf(method) !== -1;
    const hasQuery = url.indexOf("?") !== -1;
    const separator = hasQuery ? "&" : "?";

    if (isEmpty || data) {
        url += separator + query;
    } else if (dataJ !== null) {
        data = JSON.stringify(dataJ);
        url += separator + query;
        contentType = "application/json";
    } else if (dataM !== null) {
        url += separator + query;
        [contentType, data] = this._encodeMultipart(dataM, contentType, true);
    } else {
        data = query;
        contentType = "application/x-www-form-urlencoded";
    }

    if (contentType) {
        headers["Content-Type"] = headers["Content-Type"] || contentType;
    }

    const response = fetch(url, {
        method: method,
        headers: headers || {},
        body: data,
        credentials: credentials,
        keepAlive: keepAlive
    });

    response
        .then(async response => {
            let result = null;
            const isValid = validCodes.includes(response.status);
            const isAuthError = authErrorCodes.includes(response.status);
            const contentType = response.headers.get("content-type").toLowerCase();
            try {
                if (contentType.startsWith("application/json")) {
                    result = await response.json();
                } else if (contentType.startsWith("text/")) {
                    result = await response.text();
                } else {
                    result = await response.blob();
                }
            } catch (error) {
                response.error = response.error || error;
                callback.call(context, result, response, {
                    isValid: isValid,
                    isAuthError: isAuthError
                });
                return;
            }
            if (callback) {
                callback.call(context, result, response, {
                    isValid: isValid,
                    isAuthError: isAuthError
                });
            }
        })
        .catch(error => {
            response.error = response.error || error;
            if (callback) {
                callback.call(context, null, response, {
                    isValid: false,
                    isAuthError: false
                });
            }
        });

    return response;
};

/**
 * @ignore
 */
ripe.Ripe.prototype._requestURLLegacy = function(url, options, callback) {
    callback = typeof options === "function" ? options : callback;
    options = typeof options === "function" || options === undefined ? {} : options;

    const context = this;
    const method = options.method || "GET";
    const params = options.params || {};
    const headers = options.headers || {};
    let data = options.data || null;
    const dataJ = options.dataJ || null;
    const dataM = options.dataM || null;
    let contentType = options.contentType || null;
    const timeout = options.timeout || 10000;
    const timeoutConnect = options.timeoutConnect || parseInt(timeout / 2);
    const validCodes = options.validCodes || [200];
    const authErrorCodes = options.authErrorCodes || [401, 403, 440, 499];
    const withCredentials = options.withCredentials || false;

    const query = this._buildQuery(params);
    const isEmpty = ["GET", "DELETE"].indexOf(method) !== -1;
    const hasQuery = url.indexOf("?") !== -1;
    const separator = hasQuery ? "&" : "?";

    if (isEmpty || data) {
        url += separator + query;
    } else if (dataJ !== null) {
        data = JSON.stringify(dataJ);
        url += separator + query;
        contentType = "application/json";
    } else if (dataM !== null) {
        throw new Error("Multipart is not supported using legacy");
    } else {
        data = query;
        contentType = "application/x-www-form-urlencoded";
    }

    const request = new XMLHttpRequest();
    request.timeout = timeoutConnect;
    request.callback = callback;
    request.validCodes = validCodes;
    request.authErrorCodes = authErrorCodes;
    request.withCredentials = withCredentials;

    request.addEventListener("load", function() {
        let result = null;
        const isValid = this.validCodes.includes(this.status);
        const isAuthError = this.authErrorCodes.includes(this.status);
        try {
            result = JSON.parse(this.responseText);
        } catch (error) {
            result = this.responseText;
        }
        if (this.callback) {
            this.callback.call(context, result, this, {
                isValid: isValid,
                isAuthError: isAuthError
            });
        }
        if (this.timeoutHandler) {
            clearTimeout(this.timeoutHandler);
            this.timeoutHandler = null;
        }
    });

    request.addEventListener("error", function(error) {
        request.error = request.error || error;
        if (this.callback) {
            this.callback.call(context, null, this, {
                isValid: false,
                isAuthError: false
            });
        }
        if (this.timeoutHandler) {
            clearTimeout(this.timeoutHandler);
            this.timeoutHandler = null;
        }
    });

    request.addEventListener("loadstart", function() {
        context.trigger("pre_request", request, options);
    });

    request.addEventListener("loadend", function() {
        context.trigger("post_request", request, options);
    });

    request.open(method, url, true);

    request.timeoutHandler = setTimeout(() => {
        if (request.readyState === 4) return;
        request.error = new Error(`Timeout on request ${timeout}ms exceeded`);
        request.abort();
    }, timeout);

    // in case there's a content type to be set, then sets it according
    // to the inferred or requested content type
    if (contentType) {
        request.setRequestHeader("Content-Type", contentType);
    }

    for (const key in headers) {
        const value = headers[key];
        request.setRequestHeader(key, value);
    }

    if (!options.noEvents) this.trigger("build_request", request, options);

    if (data) {
        request.send(data);
    } else {
        request.send();
    }

    return request;
};

/**
 * @ignore
 */
ripe.Ripe.prototype._authCallback = function(callback) {
    if (this.username && this.password) {
        this.signin(this.username, this.password, undefined, () => {
            if (callback) callback();
        });
        return;
    }
    if (this.pid) {
        this.signinPid(this.pid, undefined, () => {
            if (callback) callback();
        });
        return;
    }
    if (callback) callback();
};

/**
 * @ignore
 */
ripe.Ripe.prototype._getQueryOptions = function(options = {}) {
    if (!options.noEvents) this.trigger("pre_query_options", options);

    const params = options.params || {};
    options.params = params;

    const brand = options.brand === undefined ? this.brand : options.brand;
    const model = options.model === undefined ? this.model : options.model;
    const version = options.version === undefined ? this.version : options.version;
    const variant = options.variant === undefined ? this.variant : options.variant;
    const frame = options.frame === undefined ? this.frame : options.frame;
    const parts = options.parts === undefined ? this.parts : options.parts;
    const engraving = options.engraving === undefined ? this.engraving : options.engraving;
    const country = options.country === undefined ? this.country : options.country;
    const currency = options.currency === undefined ? this.currency : options.currency;
    const flag = options.flag === undefined ? this.flag : options.flag;
    const full = options.full === undefined ? true : options.full;

    if (brand !== undefined && brand !== null) {
        params.brand = brand;
    }

    if (model !== undefined && model !== null) {
        params.model = model;
    }

    if (version !== undefined && version !== null) {
        params.version = version;
    }

    if (variant !== undefined && variant !== null) {
        params.variant = variant;
    }

    if (frame !== undefined && frame !== null) {
        params.frame = frame;
    }

    if (full && engraving !== undefined && engraving !== null) {
        params.engraving = engraving;
    }

    if (full && country !== undefined && country !== null) {
        params.country = country;
    }

    if (full && currency !== undefined && currency !== null) {
        params.currency = currency;
    }

    if (full && flag !== undefined && flag !== null) {
        params.flag = flag;
    }

    if (parts !== undefined && parts !== null && Object.keys(parts).length > 0) {
        params.p = this._partsMToTriplets(parts);
    }

    if (!options.noEvents) this.trigger("post_query_options", options);

    return options;
};

/**
 * @ignore
 */
ripe.Ripe.prototype._getInitialsOptions = function(options = {}) {
    if (!options.noEvents) this.trigger("pre_initials_options", options);

    const params = options.params || {};
    options.params = params;

    const initials = options.initials === undefined ? this.initials : options.initials;
    const engraving = options.engraving === undefined ? this.engraving : options.engraving;
    const initialsExtra =
        options.initialsExtra === undefined ? this.initialsExtra : options.initialsExtra;

    if (initials !== undefined && initials !== null && initials !== "") {
        params.initials = initials;
    }

    if (engraving !== undefined && engraving !== null) {
        params.engraving = engraving;
    }

    if (
        initialsExtra !== undefined &&
        initialsExtra !== null &&
        Object.keys(initialsExtra).length > 0
    ) {
        params.initials_extra = this._generateExtraS(initialsExtra);
    }

    if (!options.noEvents) this.trigger("post_initials_options", options);

    return options;
};

/**
 * @ignore
 */
ripe.Ripe.prototype._getQuery = function(options = {}) {
    options = this._getQueryOptions(options);
    return this._buildQuery(options.params);
};

/**
 * @ignore
 * @see {link http://docs.platforme.com/#config-endpoints-price}
 */
ripe.Ripe.prototype._getPriceOptions = function(options = {}) {
    if (!options.noEvents) this.trigger("pre_price_options", options);

    options = this._getQueryOptions(options);

    const params = options.params || {};
    options.params = params;

    const initials = options.initials === "" ? "$empty" : options.initials;

    if (initials !== undefined && initials !== null) {
        params.initials = initials;
    }

    const url = `${this.url}config/price`;

    options = Object.assign(options, {
        url: url,
        method: "GET"
    });

    if (!options.noEvents) this.trigger("post_price_options", options);

    return options;
};

/**
 * @ignore
 */
ripe.Ripe.prototype._getPricesOptions = function(options = {}) {
    options = this._getQueryOptions(options);

    const params = options.params || {};
    options.params = params;

    // delete the engraving and the part list from the params,
    // since they have to be specified for each item in the
    // config list
    delete options.params.engraving;
    delete options.params.p;

    let index = 0;
    options.configs.forEach(config => {
        let parts = config.parts;
        let initials = config.initials;
        let engraving = config.engraving;
        parts = parts === undefined ? this.parts : parts;
        initials = initials === undefined ? this.initials : initials;
        initials = initials === "" ? "$empty" : initials;
        engraving = engraving === undefined ? this.engraving : engraving;

        if (parts !== undefined && parts !== null && Object.keys(parts).length > 0) {
            options.params[`p${index}`] = this._partsMToTriplets(parts);
        }
        if (engraving !== undefined && engraving !== null) {
            options.params[`engraving${index}`] = engraving;
        }

        if (initials !== undefined && initials !== null) {
            options.params[`initials${index}`] = initials;
        }

        index++;
    });

    const url = `${this.url}config/prices`;
    options = Object.assign(options, {
        url: url,
        method: "GET"
    });

    return options;
};

/**
 * Returns the required options for requesting a video of a model's
 * customization.
 *
 * @param {Object} options An object containing the information required
 * to get a video for a model, more specifically `brand`, `model`, `name`
 * of the video and `p` containing the model's customization.
 * @returns {Object} The options for requesting a video.
 */
ripe.Ripe.prototype._getVideoOptions = function(options = {}) {
    options = this._getQueryOptions(options);

    const params = options.params || {};
    options.params = params;

    const name = options.name === undefined ? undefined : options.name;
    if (name !== undefined && name !== null) {
        options.params.name = name;
    }

    const url = `${this.url}video`;
    options = Object.assign(options, {
        url: url,
        method: "GET"
    });

    return options;
};

/**
 * Returns the required options for requesting a thumbnail image of a
 * model's customization.
 *
 * @param {Object} options An object containing the information required
 * to get the thumbnail of a video for a model, more specifically `brand`,
 * `model`, `name` of the video and `p` containing the model's customization.
 * @returns {Object} The options for requesting a thumbnail image of a video.
 */
ripe.Ripe.prototype._getVideoThumbnailOptions = function(options = {}) {
    options = this._getQueryOptions(options);

    const params = options.params || {};
    options.params = params;

    const name = options.name === undefined ? undefined : options.name;
    if (name !== undefined && name !== null) {
        options.params.name = name;
    }

    const url = `${this.url}video/thumbnail`;
    options = Object.assign(options, {
        url: url,
        method: "GET"
    });

    return options;
};

/**
 * @ignore
 * @see {link http://docs.platforme.com/#render-endpoints-compose}
 */
ripe.Ripe.prototype._getImageOptions = function(options = {}) {
    if (!options.noEvents) this.trigger("pre_image_options", options);

    options.country = options.country || null;
    options.currency = options.currency || null;
    options.full = options.full === undefined ? false : options.full;

    options = this._getQueryOptions(options);

    const params = options.params || {};
    options.params = params;

    const initials = options.initials === "" ? "$empty" : options.initials;

    if (options.format !== undefined && options.format !== null) {
        params.format = options.format;
    }

    if (options.width !== undefined && options.width !== null) {
        params.width = options.width;
    }

    if (options.height !== undefined && options.height !== null) {
        params.height = options.height;
    }

    if (options.size !== undefined && options.size !== null) {
        params.size = options.size;
    }

    if (options.background !== undefined && options.background !== null) {
        params.background = options.background;
    }

    if (options.crop !== undefined && options.crop !== null) {
        params.crop = options.crop ? "1" : "0";
    }

    if (options.profile !== undefined && options.profile !== null) {
        params.initials_profile = options.profile.join(",");
    }

    if (initials !== undefined && initials !== null) {
        params.initials = initials;
    }

    if (options.rotation !== undefined && options.rotation !== null) {
        params.rotation = options.rotation;
    }

    if (options.flip !== undefined && options.flip !== null) {
        params.flip = options.flip;
    }

    if (options.mirror !== undefined && options.mirror !== null) {
        params.mirror = options.mirror;
    }

    if (options.boundingBox !== undefined && options.boundingBox !== null) {
        params.bounding_box = options.boundingBox;
    }

    if (options.algorithm !== undefined && options.algorithm !== null) {
        params.algorithm = options.algorithm;
    }

    if (options.background !== undefined && options.background !== null) {
        params.background = options.background;
    }

    if (options.engine !== undefined && options.engine !== null) {
        params.engine = options.engine;
    }

    if (options.initialsX !== undefined && options.initialsX !== null) {
        params.initials_x = options.initialsX;
    }

    if (options.initialsY !== undefined && options.initialsY !== null) {
        params.initials_y = options.initialsY;
    }

    if (options.initialsWidth !== undefined && options.initialsWidth !== null) {
        params.initials_width = options.initialsWidth;
    }

    if (options.initialsHeight !== undefined && options.initialsHeight !== null) {
        params.initials_height = options.initialsHeight;
    }

    if (options.initialsViewport !== undefined && options.initialsViewport !== null) {
        params.initials_viewport = options.initialsViewport;
    }

    if (options.initialsColor !== undefined && options.initialsColor !== null) {
        params.initials_color = options.initialsColor;
    }

    if (options.initialsOpacity !== undefined && options.initialsOpacity !== null) {
        params.initials_opacity = options.initialsOpacity;
    }

    if (options.initialsAlign !== undefined && options.initialsAlign !== null) {
        params.initials_align = options.initialsAlign;
    }

    if (options.initialsVertical !== undefined && options.initialsVertical !== null) {
        params.initials_vertical = options.initialsVertical;
    }

    if (options.initialsEmbossing !== undefined && options.initialsEmbossing !== null) {
        params.initials_embossing = options.initialsEmbossing;
    }

    if (options.initialsRotation !== undefined && options.initialsRotation !== null) {
        params.initials_rotation = options.initialsRotation;
    }

    if (options.initialsZindex !== undefined && options.initialsZindex !== null) {
        params.initials_z_index = options.initialsZindex;
    }

    if (options.initialsAlgorithm !== undefined && options.initialsAlgorithm !== null) {
        params.initials_algorithm = options.initialsAlgorithm;
    }

    if (options.initialsBlendColor !== undefined && options.initialsBlendColor !== null) {
        params.initials_blend_color = options.initialsBlendColor;
    }

    if (options.initialsPattern !== undefined && options.initialsPattern !== null) {
        params.initials_pattern = options.initialsPattern;
    }

    if (options.initialsTexture !== undefined && options.initialsTexture !== null) {
        params.initials_texture = options.initialsTexture;
    }

    if (options.initialsExclusion !== undefined && options.initialsExclusion !== null) {
        params.initials_exclusion = options.initialsExclusion;
    }

    if (options.initialsInclusion !== undefined && options.initialsInclusion !== null) {
        params.initials_inclusion = options.initialsInclusion;
    }

    if (options.initialsImageRotation !== undefined && options.initialsImageRotation !== null) {
        params.initials_image_rotation = options.initialsImageRotation;
    }

    if (options.initialsImageFlip !== undefined && options.initialsImageFlip !== null) {
        params.initials_image_flip = options.initialsImageFlip;
    }

    if (options.initialsImageMirror !== undefined && options.initialsImageMirror !== null) {
        params.initials_image_mirror = options.initialsImageMirror;
    }

    if (options.debug !== undefined && options.debug !== null) {
        params.debug = options.debug;
    }

    if (options.fontFamily !== undefined && options.fontFamily !== null) {
        params.font_family = options.fontFamily;
    }

    if (options.fontWeight !== undefined && options.fontWeight !== null) {
        params.font_weight = options.fontWeight;
    }

    if (options.fontSize !== undefined && options.fontSize !== null) {
        params.font_size = options.fontSize;
    }

    if (options.fontSpacing !== undefined && options.fontSpacing !== null) {
        params.font_spacing = options.fontSpacing;
    }

    if (options.fontTrim !== undefined && options.fontTrim !== null) {
        params.font_trim = options.fontTrim;
    }

    if (options.fontMask !== undefined && options.fontMask !== null) {
        params.font_mask = options.fontMask;
    }

    if (options.fontMode !== undefined && options.fontMode !== null) {
        params.font_mode = options.fontMode;
    }

    if (options.lineHeight !== undefined && options.lineHeight !== null) {
        params.line_height = options.lineHeight;
    }

    if (options.lineBreaking !== undefined && options.lineBreaking !== null) {
        params.line_breaking = options.lineBreaking;
    }

    if (options.initialsOverflow !== undefined && options.initialsOverflow !== null) {
        params.initials_overflow = options.initialsOverflow;
    }

    if (options.shadow !== undefined && options.shadow !== null) {
        params.shadow = options.shadow;
    }

    if (options.shadowColor !== undefined && options.shadowColor !== null) {
        params.shadow_color = options.shadowColor;
    }

    if (options.shadowOffset !== undefined && options.shadowOffset !== null) {
        params.shadow_offset = options.shadowOffset;
    }

    if (options.offsets !== undefined && options.offsets !== null) {
        params.offsets = JSON.stringify(options.offsets);
    }

    if (options.curve !== undefined && options.curve !== null) {
        params.curve = JSON.stringify(options.curve);
    }

    if (options.curves !== undefined && options.curves !== null) {
        params.curves = JSON.stringify(options.curves);
    }

    if (options.logic !== undefined && options.logic !== null) {
        params.logic = options.logic;
    }

    if (options.options !== undefined && options.options !== null) {
        params.options = options.options;
    }

    const url = this.composeUrl;

    options = Object.assign(options, {
        url: url,
        method: "GET",
        params: params
    });

    if (!options.noEvents) this.trigger("post_image_options", options);

    return options;
};

/**
 * @ignore
 * @see {link http://docs.platforme.com/#render-endpoints-mask}
 */
ripe.Ripe.prototype._getMaskOptions = function(options = {}) {
    if (!options.noEvents) this.trigger("pre_mask_options", options);

    options.parts = options.parts || {};
    options.country = options.country || null;
    options.currency = options.currency || null;
    options.full = options.full === undefined ? false : options.full;

    options = this._getQueryOptions(options);

    const params = options.params || {};
    options.params = params;

    if (options.part !== undefined && options.part !== null) {
        params.part = options.part;
    }
    if (options.size !== undefined && options.size !== null) {
        params.size = options.size;
    }

    const url = `${this.url}mask`;

    options = Object.assign(options, {
        url: url,
        method: "GET",
        params: params
    });

    if (!options.noEvents) this.trigger("post_mask_options", options);

    return options;
};

/**
 * @ignore
 */
ripe.Ripe.prototype._getSwatchOptions = function(options = {}) {
    if (!options.noEvents) this.trigger("pre_swatch_options", options);

    const brand = options.brand === undefined ? this.brand : options.brand;
    const model = options.model === undefined ? this.model : options.model;
    const version = options.version === undefined ? this.version : options.version;
    const params = options.params || {};

    options.params = params;

    if (brand !== undefined && brand !== null) {
        params.brand = brand;
    }

    if (model !== undefined && model !== null) {
        params.model = model;
    }

    if (version !== undefined && version !== null) {
        params.version = version;
    }

    if (options.material !== undefined && options.material !== null) {
        params.material = options.material;
    }

    if (options.color !== undefined && options.color !== null) {
        params.color = options.color;
    }

    if (options.format !== undefined && options.format !== null) {
        params.format = options.format;
    }

    if (options.retina !== undefined && options.retina !== null) {
        params.retina = options.retina ? "1" : "0";
    }

    if (options.variant !== undefined && options.variant !== null) {
        params.variant = options.variant;
    }

    const url = `${this.url}swatch`;

    options = Object.assign(options, {
        url: url,
        method: "GET",
        params: params
    });

    if (!options.noEvents) this.trigger("post_swatch_options", options);

    return options;
};

/**
 * @ignore
 */
ripe.Ripe.prototype._getImageURL = function(options) {
    options = this._getImageOptions(options);
    return options.url + "?" + this._buildQuery(options.params);
};

/**
 * Returns the URL for a video based on the given name and the provided
 * customization.
 *
 * @param {Object} options An object containing the information required
 * to get a video for a model, more specifically `brand`, `model`, `name`
 * of the video and `p` containing the model's customization.
 * @returns {String} The URL to the video.
 */
ripe.Ripe.prototype._getVideoURL = function(options) {
    options = this._getVideoOptions(options);
    return options.url + "?" + this._buildQuery(options.params);
};

/**
 * Returns the URL for the thumbnail image of a video based on the given
 * name and the provided customization.
 *
 * @param {Object} options An object containing the information required
 * to get a video for a model, more specifically `brand`, `model`, `name`
 * of the video and `p` containing the model's customization.
 * @returns {String} The URL to the video thumbnail image.
 */
ripe.Ripe.prototype._getVideoThumbnailURL = function(options) {
    options = this._getVideoThumbnailOptions(options);
    return options.url + "?" + this._buildQuery(options.params);
};

/**
 * @ignore
 */
ripe.Ripe.prototype._getMaskURL = function(options) {
    options = this._getMaskOptions(options);
    return options.url + "?" + this._buildQuery(options.params);
};

/**
 * @ignore
 */
ripe.Ripe.prototype._getSwatchURL = function(options) {
    options = this._getSwatchOptions(options);
    return options.url + "?" + this._buildQuery(options.params);
};

/**
 * Runs the options object building operation meaning that a series
 * of default values are going to be added to the provided options
 * so that it becomes as compatible as possible.
 *
 * If authentication is required the current session identifier is
 * also added to the request.
 *
 * @param {Object} options The HTTP request options object that is
 * going to be completed with default information and session info.
 * @returns {Object} The same options object references that has been
 * provided with the proper default information populated.
 *
 * @ignore
 */
ripe.Ripe.prototype._build = function(options) {
    const url = options.url || "";
    const method = options.method || "GET";
    const params = options.params || {};
    const headers = options.headers || {};
    const auth = options.auth || false;
    if (auth && this.sid !== undefined && this.sid !== null) {
        params.sid = this.sid;
    }
    if (auth && this.key !== undefined && this.key !== null) {
        headers["X-Secret-Key"] = this.key;
    }
    if (
        auth &&
        (this.sid === undefined || this.sid === null) &&
        (this.key === undefined || this.key === null) &&
        (options.authCallback === undefined || options.authCallback === null) &&
        (this.authCallback === undefined || this.authCallback === null)
    ) {
        throw new Error("Authorization requested but none is available");
    }
    options.url = url;
    options.method = method;
    options.params = Object.assign({}, this.params, params);
    options.headers = Object.assign({}, this.headers, headers);
    options.auth = auth;
    return options;
};

/**
 * Builds a GET query string from the provided Array or Object parameter.
 *
 * If the provided parameter is an Array order of the GET parameters is
 * preserved, otherwise alphabetical order is going to be used.
 *
 * @param {Object} params The object or array that contains the sequence
 * of parameters for the generated GET query.
 * @returns {String} The GET query string that should contain the complete
 * set of passed arguments (serialization).
 *
 * @ignore
 */
ripe.Ripe.prototype._buildQuery = function(params) {
    let key;
    let value;
    let index;
    const buffer = [];

    params = typeof params === "undefined" ? {} : params;

    if (Array.isArray(params)) {
        for (index = 0; index < params.length; index++) {
            const tuple = params[index];
            key = tuple[0];
            value = tuple.length > 1 ? tuple[1] : "";
            if (value === null || value === undefined) continue;
            key = encodeURIComponent(key);
            value = encodeURIComponent(value);
            buffer.push(key + "=" + value);
        }
    } else {
        const keys = Object.keys(params);
        keys.sort();
        for (index = 0; index < keys.length; index++) {
            key = keys[index];
            value = params[key];
            key = encodeURIComponent(key);
            if (Array.isArray(value)) {
                for (let _index = 0; _index < value.length; _index++) {
                    let _value = value[_index];
                    if (_value === null || _value === undefined) continue;
                    _value = encodeURIComponent(_value);
                    buffer.push(key + "=" + _value);
                }
            } else {
                if (value === null || value === undefined) continue;
                value = encodeURIComponent(value);
                buffer.push(key + "=" + value);
            }
        }
    }

    return buffer.join("&");
};

/**
 * Unpacks the provided query string into it's components inside
 * a key, value object for easy usage.
 *
 * This operation is considered to be the opposite of the `_buildQuery`
 * operation.
 *
 * @param {String} The GET query string that is going to be parsed
 * for the creation of the output Object.
 * @returns {Object} The object that contains the key, value information
 * on the query string.
 *
 * @ignore
 */
ripe.Ripe.prototype._unpackQuery = function(query) {
    query = query[0] === "?" ? query.slice(1) : query;

    const parts = query.split("&");
    const options = {};

    for (let index = 0; index < parts.length; index++) {
        const part = parts[index];

        if (part.indexOf("=") === -1) {
            options[decodeURIComponent(part).trim()] = true;
        } else {
            const tuple = part.split("=");
            const key = decodeURIComponent(tuple[0].replace(/\+/g, "%20")).trim();
            const value = decodeURIComponent(tuple[1].replace(/\+/g, "%20")).trim();
            if (options[key] === undefined) {
                options[key] = value;
            } else if (Array.isArray(options[key])) {
                options[key].push(value);
            } else {
                options[key] = [options[key], value];
            }
        }
    }

    return options;
};

ripe.Ripe.prototype._queryToSpec = function(query) {
    const options = this._unpackQuery(query);
    const brand = options.brand || null;
    const model = options.model || null;
    const variant = options.variant || null;
    const version = options.version || null;
    const description = options.description || null;
    const initials = options.initials || null;
    const engraving = options.engraving || null;
    let initialsExtra = options.initials_extra || [];
    let tuples = options.p || [];
    initialsExtra = Array.isArray(initialsExtra) ? initialsExtra : [initialsExtra];
    initialsExtra = this._parseExtraS(initialsExtra);
    tuples = Array.isArray(tuples) ? tuples : [tuples];
    const parts = this._tuplesToParts(tuples);
    const partsM = this._partsToPartsM(parts);
    const spec = {
        brand: brand,
        model: model,
        parts: partsM,
        initials: initials,
        engraving: engraving,
        initials_extra: initialsExtra
    };
    if (variant) spec.variant = variant;
    if (version) spec.version = version;
    if (description) spec.description = description;
    return spec;
};

ripe.Ripe.prototype._queryToImageUrl = function(query, options) {
    const spec = this._queryToSpec(query);
    return this._getImageURL({ ...spec, ...options });
};

ripe.Ripe.prototype._specToQuery = function(spec) {
    const queryL = [];
    const brand = spec.brand || null;
    const model = spec.model || null;
    const variant = spec.variant || null;
    const version = spec.version || null;
    const description = spec.description || null;
    const parts = spec.parts || null;
    const initials = spec.initials || null;
    const engraving = spec.engraving || null;
    const initialsExtra = spec.initials_extra || null;
    if (brand) queryL.push(`brand=${brand}`);
    if (model) queryL.push(`model=${model}`);
    if (variant) queryL.push(`variant=${variant}`);
    if (version) queryL.push(`version=${version}`);
    if (description) queryL.push(`description=${description}`);
    if (parts) queryL.push(this._partsMToQuery(parts));
    if (initials) queryL.push(`initials=${initials}`);
    if (engraving) queryL.push(`engraving=${engraving}`);
    if (initialsExtra) {
        for (const extraS of this._generateExtraS(initialsExtra)) {
            queryL.push(`initials_extra=${extraS}`);
        }
    }
    return queryL.join("&");
};

ripe.Ripe.prototype._tuplesToParts = function(tuples) {
    const parts = [];
    for (const tuple of tuples) {
        const [name, material, color] = ripe.splitUnescape(tuple, ":", 2);
        const part = {
            name: name,
            material: material,
            color: color
        };
        parts.push(part);
    }
    return parts;
};

ripe.Ripe.prototype._partsToPartsM = function(parts) {
    const partsM = {};
    for (const part of parts) {
        const name = part.name;
        const material = part.material;
        const color = part.color;
        partsM[name] = {
            material: material,
            color: color
        };
    }
    return partsM;
};

ripe.Ripe.prototype._partsMToQuery = function(partsM, sort = true) {
    const queryL = this._partsMToTriplets(partsM, sort).map(triplet => `p=${triplet}`);
    return queryL.join("&");
};

ripe.Ripe.prototype._partsMToTriplets = function(partsM, sort = true) {
    const triplets = [];
    const names = Object.keys(partsM);
    if (sort) names.sort();
    for (const name of names) {
        const part = partsM[name];
        const [nameE, initialsE, engravingE] = [
            ripe.escape(name, ":"),
            ripe.escape(part.material, ":"),
            ripe.escape(part.color, ":")
        ];
        triplets.push(`${nameE}:${initialsE}:${engravingE}`);
    }
    return triplets;
};

ripe.Ripe.prototype._parseExtraS = function(extraS) {
    const extra = {};
    for (const extraI of extraS) {
        const [name, initials, engraving] = ripe.splitUnescape(extraI, ":", 2);
        extra[name] = {
            initials: initials,
            engraving: engraving || null
        };
    }
    return extra;
};

ripe.Ripe.prototype._generateExtraS = function(extra, sort = true, minimize = true) {
    const extraS = [];
    const names = Object.keys(extra);
    if (sort) names.sort();
    for (const name of names) {
        const values = extra[name];
        const [initials, engraving] = [values.initials, values.engraving];
        if (!initials && minimize) continue;
        const [nameE, initialsE, engravingE] = [
            ripe.escape(name, ":"),
            ripe.escape(initials, ":"),
            ripe.escape(engraving || "", ":")
        ];
        const extraI = `${nameE}:${initialsE}:${engravingE}`;
        extraS.push(extraI);
    }
    return extraS;
};

ripe.Ripe.prototype._encodeMultipart = function(fields, mime = null, doseq = false) {
    mime = mime || "multipart/form-data";

    const boundary = this._createBoundary(fields, undefined, doseq);

    const encoder = new TextEncoder("utf-8");

    const buffer = [];

    for (let [key, values] of Object.entries(fields)) {
        const isList = doseq && Array.isArray(values);
        values = isList ? values : [values];

        for (let value of values) {
            if (value === null) continue;

            let header;

            if (
                typeof value === "object" &&
                !(value instanceof Array) &&
                value.constructor !== Uint8Array
            ) {
                const headerL = [];
                let data = null;
                for (const [key, item] of Object.entries(value)) {
                    if (key === "data") data = item;
                    else headerL.push(`${key}: ${item}`);
                }
                value = data;
                header = headerL.join("\r\n");
            } else if (value instanceof Array) {
                let name = null;
                let contents = null;
                let contentTypeD = null;
                if (value.length === 2) [name, contents] = value;
                else [name, contentTypeD, contents] = value;
                header = `Content-Disposition: form-data; name="${key}"; filename="${name}"`;
                if (contentTypeD) header += `\r\nContent-Type: ${contentTypeD}`;
                value = contents;
            } else {
                header = `Content-Disposition: form-data; name="${key}"`;
                value = value.constructor === Uint8Array ? value : encoder.encode(value);
            }

            buffer.push(encoder.encode("--" + boundary + "\r\n"));
            buffer.push(encoder.encode(header + "\r\n"));
            buffer.push(encoder.encode("\r\n"));
            buffer.push(value);
            buffer.push(encoder.encode("\r\n"));
        }
    }

    buffer.push(encoder.encode("--" + boundary + "--\r\n"));
    buffer.push(encoder.encode("\r\n"));
    const body = this._joinBuffer(buffer);
    const contentType = `${mime}; boundary=${boundary}`;

    return [contentType, body];
};

ripe.Ripe.prototype._createBoundary = function(fields, size = 32, doseq = false) {
    return "Vq2xNWWHbmWYF644q9bC5T2ALtj5CynryArNQRXGYsfm37vwFKMNsqPBrpPeprFs";
};

ripe.Ripe.prototype._joinBuffer = function(bufferArray) {
    const bufferSize = bufferArray.map(item => item.byteLength).reduce((a, v) => a + v, 0);
    const buffer = new Uint8Array(bufferSize);
    let offset = 0;
    for (const item of bufferArray) {
        buffer.set(item, offset);
        offset += item.byteLength;
    }
    return buffer;
};