123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235 |
- /**
- * @filedescription Object Schema
- */
- "use strict";
- //-----------------------------------------------------------------------------
- // Requirements
- //-----------------------------------------------------------------------------
- const { MergeStrategy } = require("./merge-strategy");
- const { ValidationStrategy } = require("./validation-strategy");
- //-----------------------------------------------------------------------------
- // Private
- //-----------------------------------------------------------------------------
- const strategies = Symbol("strategies");
- const requiredKeys = Symbol("requiredKeys");
- /**
- * Validates a schema strategy.
- * @param {string} name The name of the key this strategy is for.
- * @param {Object} strategy The strategy for the object key.
- * @param {boolean} [strategy.required=true] Whether the key is required.
- * @param {string[]} [strategy.requires] Other keys that are required when
- * this key is present.
- * @param {Function} strategy.merge A method to call when merging two objects
- * with the same key.
- * @param {Function} strategy.validate A method to call when validating an
- * object with the key.
- * @returns {void}
- * @throws {Error} When the strategy is missing a name.
- * @throws {Error} When the strategy is missing a merge() method.
- * @throws {Error} When the strategy is missing a validate() method.
- */
- function validateDefinition(name, strategy) {
- let hasSchema = false;
- if (strategy.schema) {
- if (typeof strategy.schema === "object") {
- hasSchema = true;
- } else {
- throw new TypeError("Schema must be an object.");
- }
- }
- if (typeof strategy.merge === "string") {
- if (!(strategy.merge in MergeStrategy)) {
- throw new TypeError(`Definition for key "${name}" missing valid merge strategy.`);
- }
- } else if (!hasSchema && typeof strategy.merge !== "function") {
- throw new TypeError(`Definition for key "${name}" must have a merge property.`);
- }
- if (typeof strategy.validate === "string") {
- if (!(strategy.validate in ValidationStrategy)) {
- throw new TypeError(`Definition for key "${name}" missing valid validation strategy.`);
- }
- } else if (!hasSchema && typeof strategy.validate !== "function") {
- throw new TypeError(`Definition for key "${name}" must have a validate() method.`);
- }
- }
- //-----------------------------------------------------------------------------
- // Class
- //-----------------------------------------------------------------------------
- /**
- * Represents an object validation/merging schema.
- */
- class ObjectSchema {
- /**
- * Creates a new instance.
- */
- constructor(definitions) {
- if (!definitions) {
- throw new Error("Schema definitions missing.");
- }
- /**
- * Track all strategies in the schema by key.
- * @type {Map}
- * @property strategies
- */
- this[strategies] = new Map();
- /**
- * Separately track any keys that are required for faster validation.
- * @type {Map}
- * @property requiredKeys
- */
- this[requiredKeys] = new Map();
- // add in all strategies
- for (const key of Object.keys(definitions)) {
- validateDefinition(key, definitions[key]);
- // normalize merge and validate methods if subschema is present
- if (typeof definitions[key].schema === "object") {
- const schema = new ObjectSchema(definitions[key].schema);
- definitions[key] = {
- ...definitions[key],
- merge(first = {}, second = {}) {
- return schema.merge(first, second);
- },
- validate(value) {
- ValidationStrategy.object(value);
- schema.validate(value);
- }
- };
- }
- // normalize the merge method in case there's a string
- if (typeof definitions[key].merge === "string") {
- definitions[key] = {
- ...definitions[key],
- merge: MergeStrategy[definitions[key].merge]
- };
- };
- // normalize the validate method in case there's a string
- if (typeof definitions[key].validate === "string") {
- definitions[key] = {
- ...definitions[key],
- validate: ValidationStrategy[definitions[key].validate]
- };
- };
- this[strategies].set(key, definitions[key]);
- if (definitions[key].required) {
- this[requiredKeys].set(key, definitions[key]);
- }
- }
- }
- /**
- * Determines if a strategy has been registered for the given object key.
- * @param {string} key The object key to find a strategy for.
- * @returns {boolean} True if the key has a strategy registered, false if not.
- */
- hasKey(key) {
- return this[strategies].has(key);
- }
- /**
- * Merges objects together to create a new object comprised of the keys
- * of the all objects. Keys are merged based on the each key's merge
- * strategy.
- * @param {...Object} objects The objects to merge.
- * @returns {Object} A new object with a mix of all objects' keys.
- * @throws {Error} If any object is invalid.
- */
- merge(...objects) {
- // double check arguments
- if (objects.length < 2) {
- throw new Error("merge() requires at least two arguments.");
- }
- if (objects.some(object => (object == null || typeof object !== "object"))) {
- throw new Error("All arguments must be objects.");
- }
- return objects.reduce((result, object) => {
-
- this.validate(object);
-
- for (const [key, strategy] of this[strategies]) {
- try {
- if (key in result || key in object) {
- const value = strategy.merge.call(this, result[key], object[key]);
- if (value !== undefined) {
- result[key] = value;
- }
- }
- } catch (ex) {
- ex.message = `Key "${key}": ` + ex.message;
- throw ex;
- }
- }
- return result;
- }, {});
- }
- /**
- * Validates an object's keys based on the validate strategy for each key.
- * @param {Object} object The object to validate.
- * @returns {void}
- * @throws {Error} When the object is invalid.
- */
- validate(object) {
- // check existing keys first
- for (const key of Object.keys(object)) {
- // check to see if the key is defined
- if (!this.hasKey(key)) {
- throw new Error(`Unexpected key "${key}" found.`);
- }
- // validate existing keys
- const strategy = this[strategies].get(key);
- // first check to see if any other keys are required
- if (Array.isArray(strategy.requires)) {
- if (!strategy.requires.every(otherKey => otherKey in object)) {
- throw new Error(`Key "${key}" requires keys "${strategy.requires.join("\", \"")}".`);
- }
- }
- // now apply remaining validation strategy
- try {
- strategy.validate.call(strategy, object[key]);
- } catch (ex) {
- ex.message = `Key "${key}": ` + ex.message;
- throw ex;
- }
- }
- // ensure required keys aren't missing
- for (const [key] of this[requiredKeys]) {
- if (!(key in object)) {
- throw new Error(`Missing required key "${key}".`);
- }
- }
- }
- }
- exports.ObjectSchema = ObjectSchema;
|