2 * @filedescription Object Schema
7 //-----------------------------------------------------------------------------
9 //-----------------------------------------------------------------------------
11 const { MergeStrategy } = require("./merge-strategy");
12 const { ValidationStrategy } = require("./validation-strategy");
14 //-----------------------------------------------------------------------------
16 //-----------------------------------------------------------------------------
18 const strategies = Symbol("strategies");
19 const requiredKeys = Symbol("requiredKeys");
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 when
27 * this key is present.
28 * @param {Function} strategy.merge A method to call when merging two objects
30 * @param {Function} strategy.validate A method to call when validating an
31 * object with the key.
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.
37 function validateDefinition(name, strategy) {
39 let hasSchema = false;
40 if (strategy.schema) {
41 if (typeof strategy.schema === "object") {
44 throw new TypeError("Schema must be an object.");
48 if (typeof strategy.merge === "string") {
49 if (!(strategy.merge in MergeStrategy)) {
50 throw new TypeError(`Definition for key "${name}" missing valid merge strategy.`);
52 } else if (!hasSchema && typeof strategy.merge !== "function") {
53 throw new TypeError(`Definition for key "${name}" must have a merge property.`);
56 if (typeof strategy.validate === "string") {
57 if (!(strategy.validate in ValidationStrategy)) {
58 throw new TypeError(`Definition for key "${name}" missing valid validation strategy.`);
60 } else if (!hasSchema && typeof strategy.validate !== "function") {
61 throw new TypeError(`Definition for key "${name}" must have a validate() method.`);
66 //-----------------------------------------------------------------------------
68 //-----------------------------------------------------------------------------
71 * Represents an object validation/merging schema.
76 * Creates a new instance.
78 constructor(definitions) {
81 throw new Error("Schema definitions missing.");
85 * Track all strategies in the schema by key.
87 * @property strategies
89 this[strategies] = new Map();
92 * Separately track any keys that are required for faster validation.
94 * @property requiredKeys
96 this[requiredKeys] = new Map();
98 // add in all strategies
99 for (const key of Object.keys(definitions)) {
100 validateDefinition(key, definitions[key]);
102 // normalize merge and validate methods if subschema is present
103 if (typeof definitions[key].schema === "object") {
104 const schema = new ObjectSchema(definitions[key].schema);
107 merge(first = {}, second = {}) {
108 return schema.merge(first, second);
111 ValidationStrategy.object(value);
112 schema.validate(value);
117 // normalize the merge method in case there's a string
118 if (typeof definitions[key].merge === "string") {
121 merge: MergeStrategy[definitions[key].merge]
125 // normalize the validate method in case there's a string
126 if (typeof definitions[key].validate === "string") {
129 validate: ValidationStrategy[definitions[key].validate]
133 this[strategies].set(key, definitions[key]);
135 if (definitions[key].required) {
136 this[requiredKeys].set(key, definitions[key]);
142 * Determines if a strategy has been registered for the given object key.
143 * @param {string} key The object key to find a strategy for.
144 * @returns {boolean} True if the key has a strategy registered, false if not.
147 return this[strategies].has(key);
151 * Merges objects together to create a new object comprised of the keys
152 * of the all objects. Keys are merged based on the each key's merge
154 * @param {...Object} objects The objects to merge.
155 * @returns {Object} A new object with a mix of all objects' keys.
156 * @throws {Error} If any object is invalid.
160 // double check arguments
161 if (objects.length < 2) {
162 throw new Error("merge() requires at least two arguments.");
165 if (objects.some(object => (object == null || typeof object !== "object"))) {
166 throw new Error("All arguments must be objects.");
169 return objects.reduce((result, object) => {
171 this.validate(object);
173 for (const [key, strategy] of this[strategies]) {
175 if (key in result || key in object) {
176 const value = strategy.merge.call(this, result[key], object[key]);
177 if (value !== undefined) {
182 ex.message = `Key "${key}": ` + ex.message;
191 * Validates an object's keys based on the validate strategy for each key.
192 * @param {Object} object The object to validate.
194 * @throws {Error} When the object is invalid.
198 // check existing keys first
199 for (const key of Object.keys(object)) {
201 // check to see if the key is defined
202 if (!this.hasKey(key)) {
203 throw new Error(`Unexpected key "${key}" found.`);
206 // validate existing keys
207 const strategy = this[strategies].get(key);
209 // first check to see if any other keys are required
210 if (Array.isArray(strategy.requires)) {
211 if (!strategy.requires.every(otherKey => otherKey in object)) {
212 throw new Error(`Key "${key}" requires keys "${strategy.requires.join("\", \"")}".`);
216 // now apply remaining validation strategy
218 strategy.validate.call(strategy, object[key]);
220 ex.message = `Key "${key}": ` + ex.message;
225 // ensure required keys aren't missing
226 for (const [key] of this[requiredKeys]) {
227 if (!(key in object)) {
228 throw new Error(`Missing required key "${key}".`);
235 exports.ObjectSchema = ObjectSchema;