Source: jscore/DataFunctions.js

(function() {
    /**
     * @namespace DataFunctions
     * @memberof Help4.jscore
     */

    /**
     * @typedef {Function} Help4.jscore.DataFunctions.ToObject
     * @returns {Object}
     */

    /**
     * @typedef {Function} Help4.jscore.DataFunctions.Equals
     * @param {Help4.jscore.ControlBase} instance - the class instance to do the comparison with
     * @returns {boolean}
     */

    /**
     * <b>WARNING:</b> This function does NOT deliver by-value but by-reference!
     * @typedef {Function} Help4.jscore.DataFunctions.Get
     * @param {string[]} keys - keys to select the data output
     * @returns {Object}
     */

    /**
     * @typedef {Function} Help4.jscore.DataFunctions.Set
     * @param {Object} json
     * @param {Object} [params = {}]
     * @param {boolean} [params.allowReadonlyOverride = false] - allow to write readonly parameters
     * @returns {Help4.jscore.ControlBase}
     */

    /**
     * @typedef {Function} Help4.jscore.DataFunctions.Clone
     * @returns {Help4.jscore.ControlBase}
     */

    /**
     * @typedef {Function} Help4.jscore.DataFunctions.GetKeys
     * @returns {string[]}
     */

    /**
     * @typedef {Function} Help4.jscore.DataFunctions.GetConfig
     * @param {string} name - name of the config param
     * @returns {Object|null}
     */

    /**
     * @typedef {Function} Help4.jscore.DataFunctions.ForEach
     * @param {function} executor - function to execute on every data item
     */

    /**
     * @typedef {Function} Help4.jscore.DataFunctions.Every
     * @param {function} executor - function to execute on every data item
     * @returns {boolean}
     */

    /**
     * @typedef {Function} Help4.jscore.DataFunctions.Merge
     * @param {Help4.jscore.ControlBase} instance - the to be merged instance
     * @param {function} mergeFn - the merge function
     * @returns {Help4.jscore.ControlBase}
     */

    /**
     * @typedef {Function} Help4.jscore.DataFunctions.GetTexts
     * @returns {Object}
     */

    /**
     * definition of all data functions
     * @typedef {Object} Help4.jscore.DataFunctions.Embedded
     * @property {Help4.jscore.DataFunctions.ToObject} toObject - exports class data to an object
     * @property {Help4.jscore.DataFunctions.Equals} equals - compares class data with another instance
     * @property {Help4.jscore.DataFunctions.Get} get - get certain class data
     * @property {Help4.jscore.DataFunctions.Set} set - set certain class data
     * @property {Help4.jscore.DataFunctions.Clone} clone - clone instance with its data
     * @property {Help4.jscore.DataFunctions.GetKeys} getKeys - deliver all data keys
     * @property {Help4.jscore.DataFunctions.GetConfig} getConfig - deliver a certain parameter configuration
     * @property {Help4.jscore.DataFunctions.ForEach} forEach - execute a function on every data item
     * @property {Help4.jscore.DataFunctions.Every} every - execute a function on every data item
     * @property {Help4.jscore.DataFunctions.Merge} merge - merge with another instance and deliver the merged result
     * @property {Help4.jscore.DataFunctions.GetTexts} getTexts - get all texts
     * @property {Help4.EmbeddedEvent} onChange - listener for data change events
     */

    Help4.jscore.DataFunctions = {
        toObject: function() {
            const {PARAM_KEYS, DATA_KEYS} = this.constructor;
            const classConfig = this.____getClassConfig();
            const {[PARAM_KEYS]: paramKeys, [DATA_KEYS]: dataKeys, params} = classConfig;
            const json = {};

            paramKeys?.forEach(key => {
                const accessKey = params[key].private ? `__${key}` : key;
                json[key] = this[accessKey];  // use getter
            });

            dataKeys?.forEach(key => json[key] = this[key]);  // use getter

            return json;
        },

        equals: function(instance) {
            if (this === instance) return true;
            if (!(instance instanceof this.constructor)) return false;


            const {PARAM_KEYS, DATA_KEYS, EQUALS, CUSTOM_TYPE} = this.constructor;
            const classConfig = this.____getClassConfig();
            const {[PARAM_KEYS]: paramKeys, [DATA_KEYS]: dataKeys, params, data} = classConfig;

            const compare = key => {
                const {private: p, type, equals} = params[key];  // "private" reserved by JSDoc
                const accessKey = p ? `__${key}` : key;
                if (!type) throw new Error(`Properties need to define a type: ${key}`);

                const funEquals = type === CUSTOM_TYPE ? equals : EQUALS[type];
                if (!funEquals) throw new Error(`No "equals" function defined: ${key}`);

                return funEquals.call(this, this[accessKey], instance[accessKey]);
            }

            for (const key of paramKeys || []) {
                if (!compare(key)) return false;
            }

            for (const key of dataKeys || []) {
                if (!compare(key)) return false;
            }

            return true;
        },

        get: function(keys= []) {
            const data = this.____getData();
            const result = {};
            keys.forEach(key => result[key] = data[key]);
            return result;
        },

        set: function(json, {allowReadonlyOverride = false} = {}) {
            if (json === this) return this;  // is not a JSON but myself

            // is not a JSON but another instance of my class
            if (json instanceof this.constructor) {
                json = json.dataFunctions.toObject();
            }

            const {params = {}, data = {}} = this.____getClassConfig();

            for (const [key, value] of Object.entries(json)) {
                if (data.hasOwnProperty(key)) {
                    this[key] = value;  // use setter
                } else if (params.hasOwnProperty(key)) {
                    const {private: p, readonly} = params[key];  // "private" reserved by JSDoc
                    const accessKey = p ? `__${key}` : key;

                    if (readonly) {
                        // readonly parameters only to be overridden if specifically requested
                        // setter not used here, therefore no data update callbacks!
                        if (allowReadonlyOverride) this.____getData()[key] = value;
                    } else {
                        this[accessKey] = value;  // use setter
                    }
                }
            }

            return this;
        },

        clone: function() {
            const data = this.dataFunctions.toObject();
            return new this.constructor(data);
        },

        getKeys: function() {
            const {PARAM_KEYS, DATA_KEYS} = this.constructor;
            const {[PARAM_KEYS]: paramKeys = [], [DATA_KEYS]: dataKeys = []} = this.____getClassConfig();
            return paramKeys.concat(dataKeys);
        },

        getConfig: function(name) {
            const {params, data} = this.____getClassConfig();
            return params?.[name] || data?.[name] || null;
        },

        forEach: function(executor) {
            const keys = this.dataFunctions.getKeys();
            for (const [index, key] of Help4.arrayEntries(keys)) {
                executor.call(this, key, index, keys);
            }
        },

        every: function(executor) {
            const keys = this.dataFunctions.getKeys();
            for (const [index, key] of Help4.arrayEntries(keys)) {
                if (!executor.call(this, key, index, keys)) return false;
            }
            return true;
        },

        merge: function(instance, mergeFn) {
            if (instance instanceof this.constructor) {
                return mergeFn(this.dataFunctions.clone(), instance);
            }

            throw new Error('Use merge for same classes only!');
        },

        getTexts: function() {
            const {TEXT_CONTENT_TYPE, TEXT_SET, TEXT_GET} = this.constructor;
            const {texts = {}} = this.____getClassConfig();
            const result = {};
            for (const [key, name] of Object.entries(texts)) {
                result[key] = {
                    name,
                    contentType: TEXT_CONTENT_TYPE[name],
                    set: TEXT_SET[name],
                    get: TEXT_GET[name]
                };
            }
            return result;
        },
    }
})();