CoolFace
Apppublic

Umama-at-Bluchip/Quick-UI

sourceHugging Faceupdated 2y agoView on Hugging Face
0likes
object-schema.js302 linesDownload Raw Back to src
1/**2 * @filedescription Object Schema3 */4 5"use strict";6 7//-----------------------------------------------------------------------------8// Requirements9//-----------------------------------------------------------------------------10 11const { MergeStrategy } = require("./merge-strategy");12const { ValidationStrategy } = require("./validation-strategy");13 14//-----------------------------------------------------------------------------15// Private16//-----------------------------------------------------------------------------17 18const strategies = Symbol("strategies");19const requiredKeys = Symbol("requiredKeys");20 21/**22 * Validates a schema strategy.23 * @param {string} name The name of the key this strategy is for.24 * @param {Object} strategy The strategy for the object key.25 * @param {boolean} [strategy.required=true] Whether the key is required.26 * @param {string[]} [strategy.requires] Other keys that are required when27 *      this key is present.28 * @param {Function} strategy.merge A method to call when merging two objects29 *      with the same key.30 * @param {Function} strategy.validate A method to call when validating an31 *      object with the key.32 * @returns {void}33 * @throws {Error} When the strategy is missing a name.34 * @throws {Error} When the strategy is missing a merge() method.35 * @throws {Error} When the strategy is missing a validate() method.36 */37function validateDefinition(name, strategy) {38 39    let hasSchema = false;40    if (strategy.schema) {41        if (typeof strategy.schema === "object") {42            hasSchema = true;43        } else {44            throw new TypeError("Schema must be an object.");45        }46    }47 48    if (typeof strategy.merge === "string") {49        if (!(strategy.merge in MergeStrategy)) {50            throw new TypeError(`Definition for key "${name}" missing valid merge strategy.`);51        }52    } else if (!hasSchema && typeof strategy.merge !== "function") {53        throw new TypeError(`Definition for key "${name}" must have a merge property.`);54    }55 56    if (typeof strategy.validate === "string") {57        if (!(strategy.validate in ValidationStrategy)) {58            throw new TypeError(`Definition for key "${name}" missing valid validation strategy.`);59        }60    } else if (!hasSchema && typeof strategy.validate !== "function") {61        throw new TypeError(`Definition for key "${name}" must have a validate() method.`);62    }63}64 65//-----------------------------------------------------------------------------66// Errors67//-----------------------------------------------------------------------------68 69/**70 * Error when an unexpected key is found.71 */72class UnexpectedKeyError extends Error {73 74    /**75     * Creates a new instance.76     * @param {string} key The key that was unexpected. 77     */78    constructor(key) {79        super(`Unexpected key "${key}" found.`);80    }81}82 83/**84 * Error when a required key is missing.85 */86class MissingKeyError extends Error {87 88    /**89     * Creates a new instance.90     * @param {string} key The key that was missing. 91     */92    constructor(key) {93        super(`Missing required key "${key}".`);94    }95}96 97/**98 * Error when a key requires other keys that are missing.99 */100class MissingDependentKeysError extends Error {101 102    /**103     * Creates a new instance.104     * @param {string} key The key that was unexpected.105     * @param {Array<string>} requiredKeys The keys that are required.106     */107    constructor(key, requiredKeys) {108        super(`Key "${key}" requires keys "${requiredKeys.join("\", \"")}".`);109    }110}111 112/**113 * Wrapper error for errors occuring during a merge or validate operation.114 */115class WrapperError extends Error {116 117    /**118     * Creates a new instance.119     * @param {string} key The object key causing the error. 120     * @param {Error} source The source error. 121     */122    constructor(key, source) {123        super(`Key "${key}": ${source.message}`, { cause: source });124 125        // copy over custom properties that aren't represented126        for (const key of Object.keys(source)) {127            if (!(key in this)) {128                this[key] = source[key];129            }130        }131    }132}133 134//-----------------------------------------------------------------------------135// Main136//-----------------------------------------------------------------------------137 138/**139 * Represents an object validation/merging schema.140 */141class ObjectSchema {142 143    /**144     * Creates a new instance.145     */146    constructor(definitions) {147 148        if (!definitions) {149            throw new Error("Schema definitions missing.");150        }151 152        /**153         * Track all strategies in the schema by key.154         * @type {Map}155         * @property strategies156         */157        this[strategies] = new Map();158 159        /**160         * Separately track any keys that are required for faster validation.161         * @type {Map}162         * @property requiredKeys163         */164        this[requiredKeys] = new Map();165 166        // add in all strategies167        for (const key of Object.keys(definitions)) {168            validateDefinition(key, definitions[key]);169 170            // normalize merge and validate methods if subschema is present171            if (typeof definitions[key].schema === "object") {172                const schema = new ObjectSchema(definitions[key].schema);173                definitions[key] = {174                    ...definitions[key],175                    merge(first = {}, second = {}) {176                        return schema.merge(first, second);177                    },178                    validate(value) {179                        ValidationStrategy.object(value);180                        schema.validate(value);181                    }182                };183            }184 185            // normalize the merge method in case there's a string186            if (typeof definitions[key].merge === "string") {187                definitions[key] = {188                    ...definitions[key],189                    merge: MergeStrategy[definitions[key].merge]190                };191            };192 193            // normalize the validate method in case there's a string194            if (typeof definitions[key].validate === "string") {195                definitions[key] = {196                    ...definitions[key],197                    validate: ValidationStrategy[definitions[key].validate]198                };199            };200 201            this[strategies].set(key, definitions[key]);202 203            if (definitions[key].required) {204                this[requiredKeys].set(key, definitions[key]);205            }206        }207    }208 209    /**210     * Determines if a strategy has been registered for the given object key.211     * @param {string} key The object key to find a strategy for.212     * @returns {boolean} True if the key has a strategy registered, false if not. 213     */214    hasKey(key) {215        return this[strategies].has(key);216    }217 218    /**219     * Merges objects together to create a new object comprised of the keys220     * of the all objects. Keys are merged based on the each key's merge221     * strategy.222     * @param {...Object} objects The objects to merge.223     * @returns {Object} A new object with a mix of all objects' keys.224     * @throws {Error} If any object is invalid.225     */226    merge(...objects) {227 228        // double check arguments229        if (objects.length < 2) {230            throw new TypeError("merge() requires at least two arguments.");231        }232 233        if (objects.some(object => (object == null || typeof object !== "object"))) {234            throw new TypeError("All arguments must be objects.");235        }236 237        return objects.reduce((result, object) => {238            239            this.validate(object);240            241            for (const [key, strategy] of this[strategies]) {242                try {243                    if (key in result || key in object) {244                        const value = strategy.merge.call(this, result[key], object[key]);245                        if (value !== undefined) {246                            result[key] = value;247                        }248                    }249                } catch (ex) {250                    throw new WrapperError(key, ex);251                }252            }253            return result;254        }, {});255    }256 257    /**258     * Validates an object's keys based on the validate strategy for each key.259     * @param {Object} object The object to validate.260     * @returns {void}261     * @throws {Error} When the object is invalid. 262     */263    validate(object) {264 265        // check existing keys first266        for (const key of Object.keys(object)) {267 268            // check to see if the key is defined269            if (!this.hasKey(key)) {270                throw new UnexpectedKeyError(key);271            }272 273            // validate existing keys274            const strategy = this[strategies].get(key);275 276            // first check to see if any other keys are required277            if (Array.isArray(strategy.requires)) {278                if (!strategy.requires.every(otherKey => otherKey in object)) {279                    throw new MissingDependentKeysError(key, strategy.requires);280                }281            }282 283            // now apply remaining validation strategy284            try {285                strategy.validate.call(strategy, object[key]);286            } catch (ex) {287                throw new WrapperError(key, ex);288            }289        }290 291        // ensure required keys aren't missing292        for (const [key] of this[requiredKeys]) {293            if (!(key in object)) {294                throw new MissingKeyError(key);295            }296        }297 298    }299}300 301exports.ObjectSchema = ObjectSchema;302