Source: modules/config/BaseObject.js

/**
 * @module config/BaseObject.js
 * @name BaseObject
 * @copyright 2023 3Liz
 * @author DHONT René-Luc
 * @license MPL-2.0
 */

import { convertNumber, convertBoolean, convertArray } from './../utils/Converters.js';
import { Extent } from './../utils/Extent.js';
import { getNotContains } from './Tools.js';
import { ValidationError, ConversionError } from './../Errors.js';

/**
 * The function to update an instance based on required and optional properties description
 * The values of each properties defined in requiredProperties and optionalProperties will be converted to is type:
 * boolean, number, extent; and will be stored in an _{name} attribute
 * This will help to get values respecting the type defined in getter and to validate the config
 * @param {object} obj                  - the instance on which apply the config
 * @param {object} cfg                  - the lizmap config object
 * @param {object} [requiredProperties] - the required properties definition
 * @param {object} [optionalProperties] - the optional properties definition
 * @returns {object} - The instance updated
 * @throws {ValidationError} for required properties
 * @throws {ConversionError} for values
 */
export function applyConfig(obj, cfg, requiredProperties={}, optionalProperties={}) {
    if (!cfg || typeof cfg !== "object") {
        throw new ValidationError('The cfg parameter is not an Object!');
    }

    const cfgOwnPropertyNames = Object.getOwnPropertyNames(cfg);
    const requiredOwnPropertyNames = Object.getOwnPropertyNames(requiredProperties);
    if (cfgOwnPropertyNames.length < requiredOwnPropertyNames.length) {
        let errorMsg = 'The cfg object has not enough properties compared to required!';
        errorMsg += '\n- The cfg properties: '+cfgOwnPropertyNames;
        errorMsg += '\n- The required properties: '+requiredOwnPropertyNames;
        throw new ValidationError(errorMsg);
    }

    const requiredNotContainsInCfg = getNotContains(requiredOwnPropertyNames, cfgOwnPropertyNames);
    if (requiredNotContainsInCfg.length > 0) {
        throw new ValidationError('The properties: `' + requiredNotContainsInCfg + '` are required in the cfg object!');
    }

    for (const prop in requiredProperties) {
        if (!cfg.hasOwnProperty(prop)) {
            throw new ValidationError('No `' + prop + '` in the cfg object!');
        }
        const def = requiredProperties[prop];
        switch (def.type){
            case 'boolean':
                obj['_'+prop] = convertBoolean(cfg[prop]);
                break;
            case 'number':
                obj['_'+prop] = convertNumber(cfg[prop]);
                break;
            case 'array':
                obj['_'+prop] = convertArray(cfg[prop], def.contentType);
                break;
            case 'extent':
                obj['_'+prop] = new Extent(...cfg[prop]);
                break;
            default:
                obj['_'+prop] = cfg[prop];
        }
    }

    for (const prop in optionalProperties) {
        const def = optionalProperties[prop];
        if (cfg.hasOwnProperty(prop)) {
            // keep null value for nullable property
            if (def.hasOwnProperty('nullable') &&
                def['nullable'] &&
                cfg[prop] === null) {
                obj['_'+prop] = null;
                continue;
            }
            // convert value
            switch (def.type){
                case 'boolean':
                    obj['_'+prop] = convertBoolean(cfg[prop]);
                    break;
                case 'number':
                    obj['_'+prop] = convertNumber(cfg[prop]);
                    break;
                case 'extent':
                    obj['_'+prop] = new Extent(...cfg[prop]);
                    break;
                default:
                    obj['_'+prop] = cfg[prop];
            }
        } else if (def.hasOwnProperty('default')) {
            obj['_'+prop] = def.default;
        } else {
            obj['_'+prop] = null;
        }
    }
    return obj;
}

/**
 * Class representing a base object config
 * @class
 */
export class BaseObjectConfig {
    /**
     * The generic constructor to build an instance based on required and optional properties description
     * The values of each properties defined in requiredProperties and optionalProperties will be converted to is type:
     * boolean, number, extent ; and will be stored in an _{name} attribute
     * This will help to get values respecting the type defined in getter and to validate the config
     * @param {object} cfg                     - the lizmap config object
     * @param {object} [requiredProperties] - the required properties definition
     * @param {object} [optionalProperties] - the optional properties definition
     */
    constructor(cfg, requiredProperties={}, optionalProperties={}) {
        applyConfig(this, cfg, requiredProperties, optionalProperties);
    }
}

/**
 * Class representing an object layer config with layerId and order attribute
 * @name BaseObjectLayerConfig
 * @class
 * @augments BaseObjectConfig
 */
export class BaseObjectLayerConfig extends BaseObjectConfig {
    /**
     * @param {string} layerName                                          - the layer name
     * @param {object} cfg                                                - the lizmap config object
     * @param {string} cfg.layerId                                        - the layer id
     * @param {number} [cfg.order]                                        - the layer order
     * @param {object} [requiredProperties] - the required properties definition
     * @param {object} [optionalProperties]   - the optional properties definition
     */
    constructor(layerName, cfg, requiredProperties={'layerId': {type: 'string'}}, optionalProperties={'order': {type: 'number'}}) {
        if (!layerName) {
            throw new ValidationError('The layerName parameter is mandatory!');
        }
        if (!requiredProperties.hasOwnProperty('layerId')) {
            requiredProperties['layerId'] = {type: 'string'};
        }
        if (!optionalProperties.hasOwnProperty('order')) {
            optionalProperties['order'] = {type: 'number'};
        }
        super(cfg, requiredProperties, optionalProperties)
        this._layerName = layerName;
    }

    /**
     * The layer id
     * @type {string}
     */
    get id() {
        return this._layerId;
    }

    /**
     * The layer name
     * @type {string}
     */
    get name() {
        return this._layerName;
    }

    /**
     * The layer config order in the list
     * @type {number}
     */
    get order() {
        if (this._order == undefined || this._order == null) {
            return -1;
        }
        return this._order;
    }
}

/**
 * Class representing an object with layer configs with layerId and order attribute
 * @class
 */
export class BaseObjectLayersConfig {

    /**
     * @param {Function} layerConfig - the class name to construct instances contain in cfg
     * @param {object}   cfg         - the lizmap layers config object
     */
    constructor(layerConfig, cfg) {
        if (!cfg || typeof cfg !== "object") {
            throw new ValidationError('The cfg parameter is not an Object!');
        }

        this._names = [];
        this._ids = [];
        this._configs = [];

        for (const key in cfg) {
            const lConfig = new layerConfig(key, cfg[key]);
            this._configs.push(lConfig);
        }

        this._configs.sort((a, b) => {
            return a.order - b.order;
        });
        this._ids = this._configs.map((v) => {
            return v.id;
        });
        this._names = this._configs.map((v) => {
            return v.name;
        });
    }

    /**
     * The copy of the layer names
     * @type {string[]}
     */
    get layerNames() {
        return [...this._names];
    }

    /**
     * The copy of the layer ids
     * @type {string[]}
     */
    get layerIds() {
        return [...this._ids];
    }

    /**
     * The copy of the base layer configs or extended class
     * @type {BaseObjectLayerConfig[]}
     */
    get layerConfigs() {
        return [...this._configs];
    }

    /**
     * Iterate through layer names
     * @generator
     * @yields {string} The next layer name
     */
    *getLayerNames() {
        for (const name of this._names) {
            yield name;
        }
    }

    /**
     * Iterate through layer ids
     * @generator
     * @yields {string} The next layer id
     */
    *getLayerIds() {
        for (const id of this._ids) {
            yield id;
        }
    }

    /**
     * Iterate through layer configs
     * @generator
     * @yields {BaseObjectLayerConfig} The next layer config or extended class
     */
    *getLayerConfigs() {
        for (const config of this._configs) {
            yield config;
        }
    }

    /**
     * Get a layer config or extended class by layer name
     * @param {string} name the layer name
     * @returns {BaseObjectLayerConfig} The base layer config or extended class associated to the name
     * @throws {RangeError|Error} The layer name is unknown or the config has been corrupted
     */
    getLayerConfigByLayerName(name) {
        const idx = this._names.indexOf(name);
        if (idx == -1) {
            throw new RangeError('The layer name `'+ name +'` is unknown!');
        }

        const cfg = this._configs[idx];
        if (cfg.name != name) {
            throw 'The config has been corrupted!'
        }

        return cfg;
    }

    /**
     * Get a layer config or extended class by layer id
     * @param {string} id the layer id
     * @returns {BaseObjectLayerConfig} The base layer config or extended class associated to the id
     * @throws {RangeError|Error} The layer name is unknown or the config has been corrupted
     */
    getLayerConfigByLayerId(id) {
        const idx = this._ids.indexOf(id);
        if (idx == -1) {
            throw new RangeError('The layer id `'+ id +'` is unknown!');
        }

        const cfg = this._configs[idx];
        if (cfg.id != id) {
            throw 'The config has been corrupted!'
        }

        return cfg;
    }
}