plugins/sync.js

const base = require("./base");
const ripe = base.ripe;

/**
 * @class
 * @augments Plugin
 * @classdesc Plugin responsible for applying synchronization rules.
 *
 * @param {Object} rules A Map with synchronization rules to be applied.
 * If defined, overrides the rules defined on the model's config.
 * @param {Object} options An object with options to configure the plugin.
 */
ripe.Ripe.plugins.SyncPlugin = function(rules, options = {}) {
    ripe.Ripe.plugins.Plugin.call(this);

    this.rules = this._normalizeRules(rules);
    this.manual = Boolean(options.manual || rules);
    this.auto = !this.manual;
};

ripe.Ripe.plugins.SyncPlugin.prototype = ripe.build(ripe.Ripe.plugins.Plugin.prototype);
ripe.Ripe.plugins.SyncPlugin.prototype.constructor = ripe.Ripe.plugins.SyncPlugin;

/**
 * The Sync Plugin binds the 'post_config' and 'part' events,
 * in order to:
 * - retrieve the model's configuration.
 * - change the necessary parts making them comply with the syncing rules.
 *
 * @param {Ripe} The Ripe instance in use.
 */
ripe.Ripe.plugins.SyncPlugin.prototype.register = function(owner) {
    ripe.Ripe.plugins.Plugin.prototype.register.call(this, owner);

    // sets the initial set of rules from the owner, in case the
    // auto mode is enabled for the current instance
    this.rules =
        this.auto && owner.loadedConfig
            ? this._normalizeRules(owner.loadedConfig.sync)
            : this.rules;

    // listens for model changes and if the load config option is
    // set then retrieves the new model's post config, otherwise
    // unregisters itself as its rules are no longer valid
    this._postConfigBind = this.manual
        ? null
        : this.owner.bind("post_config", config => {
              this.rules = config ? this._normalizeRules(config.sync) : {};
          });

    // binds to the part event to change the necessary parts
    // so that they comply with the product's sync rules
    this._partBind = this.owner.bind("part", this._applySync.bind(this));
};

/**
 * The unregister to be called (by the owner)
 * the plugins unbinds events and executes
 * any necessary cleanup operation.
 *
 * @param {Ripe} The Ripe instance in use.
 */
ripe.Ripe.plugins.SyncPlugin.prototype.unregister = function(owner) {
    this.owner && this.owner.unbind("part", this._partBind);
    this.owner && this.owner.unbind("post_config", this._postConfigBind);

    ripe.Ripe.plugins.Plugin.prototype.unregister.call(this, owner);
};

/**
 * Traverses the provided rules and transforms string rules
 * into object rules to keep the internal representation
 * of the rules consistent.
 *
 * @param {Array} rules The rules that will be normalized
 * into object rules.
 * @returns {Object} The normalized version of the rules.
 *
 * @ignore
 */
ripe.Ripe.plugins.SyncPlugin.prototype._normalizeRules = function(rules) {
    const _rules = {};

    if (!rules) {
        return _rules;
    }

    for (const ruleName in rules) {
        const rule = rules[ruleName];
        for (let index = 0; index < rule.length; index++) {
            let part = rule[index];
            if (typeof part === "string") {
                part = {
                    part: part
                };
                rule[index] = part;
            }
        }
        _rules[ruleName] = rule;
    }
    return _rules;
};

/**
 * Checks if any of the sync rules should apply to the
 * provided part, meaning that the other parts of the rule
 * have to be changed accordingly.
 *
 * @param {String} name The name of the part that may be
 * affected by a rule.
 * @param {Object} value The material and color of the part
 * as a object map.
 *
 * @ignore
 */
ripe.Ripe.plugins.SyncPlugin.prototype._applySync = function(name, value) {
    // iterates over the complete set of rules to determine
    // if any of them should apply to the provided part
    for (const key in this.rules) {
        // if a part was selected and it is part of
        // the rule then its value is used otherwise
        // the first part of the rule is used
        const rule = this.rules[key];
        const firstPart = rule[0];
        name = name || firstPart.part;
        value = value || this.owner.parts[name];

        // checks if the part triggers the sync rule
        // and skips to the next rule if it doesn't
        if (this._shouldSync(rule, name, value) === false) {
            continue;
        }

        // iterates through the parts of the rule and
        // sets their material and color according to
        // the sync rule in case there's a match
        for (let index = 0; index < rule.length; index++) {
            const _part = rule[index];

            // in case the current rule definition references the current
            // part in rule deinition, ignores the current loop
            if (_part.part === name) {
                continue;
            }

            // tries to find the target part configuration an in case
            // no such part is found throws an error
            const target = this.owner.parts[_part.part];
            if (!target) {
                throw new Error(`Target part for rule not found '${_part.part}'`);
            }

            if (_part.color === undefined) {
                target.material = _part.material ? _part.material : value.material;
            }

            target.color = _part.color ? _part.color : value.color;
        }
    }
};

/**
 * Checks if the sync rule contains the provided part
 * meaning that the other parts of the rule have to
 * be changed accordingly.
 *
 * @param {Object} rule The sync rule that will be checked.
 * @param {String} name The name of the part that may be
 * affected by the rule.
 * @param {Object} value The material and color of the part.
 * @returns {Boolean} If the provided rule is valid for the provided
 * part and value (material and color).
 *
 * @ignore
 */
ripe.Ripe.plugins.SyncPlugin.prototype._shouldSync = function(rule, name, value) {
    for (let index = 0; index < rule.length; index++) {
        const rulePart = rule[index];
        const part = rulePart.part;
        const material = rulePart.material;
        const color = rulePart.color;
        const materialSync = !material || material === value.material;
        const colorSync = !color || color === value.color;
        if (part === name && materialSync && colorSync) {
            return true;
        }
    }
    return false;
};