Source: services/customizationValidator.js

"use strict";

const _ = require("underscore");
const winston = require("winston");

const optionData = require("./optionData");
const optionGroups = optionData.optionGroups;
const optionsMap = optionData.optionsMap;


/**
 * Validates given character customization.
 *
 * A character customization given by user is like following:
 *
 * ```
 * [{"optionId": 1000, "color": "#DDDDDD"},
 *  {"optionId": 2000, "color": "#DDDDDD"},
 *  {"optionId": 7002}]
 * ```
 *
 * Option group rules:
 * - There must be a selection for each group with forceSelect=true
 * - Number of selected options for a group must be less than allowed maximum for the group
 *
 * Option rules:
 * - Selected options must exist
 * - If color is not received for a selected option, color must not have color selection required
 * - If color is received, colors must be allowed for that option and received one is in the allowed colors list
 *
 *
 * @module services/customizationValidator
 * @param {UserCharPart[]} charParts  - character customization
 * @return {string|undefined} A meaningful message in case of problems; `undefined` otherwise
 */

module.exports = function (charParts) {
    // --------- OPTION VALIDATION PROCEDURE ---------------
    // - check if all selected parts exist
    //    - if color is not received, check if a color is required
    //    - if color is received, check if colors are allowed and received one is in the list

    // --------- OPTION GROUP VALIDATION PROCEDURE ---------------
    // - create a map of <groupId, charPart[]>
    // - validate groups
    //    - check if there is a selection for each group with forceSelect=true
    //    - check if number of selected options for a group is < allowMax
    //    -

    if (_.isEmpty(charParts)) {
        return "No character customization is provided";
    }

    if (!_.isArray(charParts)) {
        return "Character customization provided is not an array";
    }

    // --------- OPTION VALIDATION PROCEDURE ---------------
    for (let charPart of charParts) {
        const validationError = validateSingleCharPart(charPart);
        if (validationError) {
            return validationError;
        }
    }

    // --------- OPTION GROUP VALIDATION PROCEDURE ---------------
    // - create a map of <groupId, charPart[]>
    const groupCharPartsMap = _.groupBy(charParts, function (charPart) {
        return optionsMap[charPart.optionId].groupId;
    });
    for (let optionGroup of optionGroups) {
        const userSelectedPartsForGroup = groupCharPartsMap[optionGroup.id];
        const validationError = validateSelectedCharPartsForGroup(optionGroup, userSelectedPartsForGroup);
        if (validationError) {
            return validationError;
        }
    }

    return undefined;
};

function validateSingleCharPart(charPart) {
    if (!charPart) {
        return "Falsy char part : " + JSON.stringify(charPart);
    }
    const optionId = charPart.optionId;
    const color = charPart.color;

    if (!optionId || !_.isFinite(optionId)) {
        winston.debug("Invalid option id : %s", charPart);
        return "Invalid option id : " + charPart.optionId;
    }

    // - check if all selected parts exist
    if (!_.has(optionsMap, optionId)) {
        winston.debug("Option doesn't exist: %s", charPart);
        return "Option doesn't exist: " + charPart.optionId;
    }

    const option = optionsMap[optionId];
    if (color) {
        const optionColors = option.colors;

        //    - if color is not selected, do not allow color selection
        if (!optionColors) {
            winston.debug("Option doesn't have colors, but a color is received: %s", charPart);
            return "Option doesn't have colors, but a color is received: " + option.name;
        }

        //    - if color is selected, check if colors are allowed and selected one is in the list
        const indexOfSelectedColor = _.indexOf(optionColors, color);
        if (indexOfSelectedColor < 0) {
            winston.debug("Given color is not allowed for the option: %s ", charPart);
            return "Given color is not allowed for the option: " + option.name;
        }
    }

    return undefined;
}

function validateSelectedCharPartsForGroup(optionGroup, userSelectedPartsForGroup) {
    //    - check if there is a selection for each group with forceSelect=true
    if (optionGroup.forceSelect) {
        if (_.isEmpty(userSelectedPartsForGroup)) {
            winston.debug("Selection is required for the group: %s" + optionGroup);
            return "Selection is required for the group: " + JSON.stringify(_.pick(optionGroup, "id", "name"));
        }
    }

    if (_.isEmpty(userSelectedPartsForGroup)) {
        // no force select, nothing selected.. just skip it
        return undefined;
    }

    //    - check if number of selected options for a group is < allowMax
    if (optionGroup.allowMax) {
        if (userSelectedPartsForGroup.length > optionGroup.allowMax) {
            winston.debug("Too many options selected for group: %s" + userSelectedPartsForGroup);
            return "Too many options selected for group: " + JSON.stringify(userSelectedPartsForGroup);
        }
    }
}