massive update, probably broken
[dotfiles/.git] / .config / coc / extensions / node_modules / coc-prettier / node_modules / @humanwhocodes / object-schema / src / object-schema.js
1 /**
2  * @filedescription Object Schema
3  */
4
5 "use strict";
6
7 //-----------------------------------------------------------------------------
8 // Requirements
9 //-----------------------------------------------------------------------------
10
11 const { MergeStrategy } = require("./merge-strategy");
12 const { ValidationStrategy } = require("./validation-strategy");
13
14 //-----------------------------------------------------------------------------
15 // Private
16 //-----------------------------------------------------------------------------
17
18 const strategies = Symbol("strategies");
19 const 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 when
27  *      this key is present.
28  * @param {Function} strategy.merge A method to call when merging two objects
29  *      with the same key.
30  * @param {Function} strategy.validate A method to call when validating an
31  *      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  */
37 function 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 //-----------------------------------------------------------------------------
67 // Class
68 //-----------------------------------------------------------------------------
69
70 /**
71  * Represents an object validation/merging schema.
72  */
73 class ObjectSchema {
74
75     /**
76      * Creates a new instance.
77      */
78     constructor(definitions) {
79
80         if (!definitions) {
81             throw new Error("Schema definitions missing.");
82         }
83
84         /**
85          * Track all strategies in the schema by key.
86          * @type {Map}
87          * @property strategies
88          */
89         this[strategies] = new Map();
90
91         /**
92          * Separately track any keys that are required for faster validation.
93          * @type {Map}
94          * @property requiredKeys
95          */
96         this[requiredKeys] = new Map();
97
98         // add in all strategies
99         for (const key of Object.keys(definitions)) {
100             validateDefinition(key, definitions[key]);
101
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);
105                 definitions[key] = {
106                     ...definitions[key],
107                     merge(first = {}, second = {}) {
108                         return schema.merge(first, second);
109                     },
110                     validate(value) {
111                         ValidationStrategy.object(value);
112                         schema.validate(value);
113                     }
114                 };
115             }
116
117             // normalize the merge method in case there's a string
118             if (typeof definitions[key].merge === "string") {
119                 definitions[key] = {
120                     ...definitions[key],
121                     merge: MergeStrategy[definitions[key].merge]
122                 };
123             };
124
125             // normalize the validate method in case there's a string
126             if (typeof definitions[key].validate === "string") {
127                 definitions[key] = {
128                     ...definitions[key],
129                     validate: ValidationStrategy[definitions[key].validate]
130                 };
131             };
132
133             this[strategies].set(key, definitions[key]);
134
135             if (definitions[key].required) {
136                 this[requiredKeys].set(key, definitions[key]);
137             }
138         }
139     }
140
141     /**
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. 
145      */
146     hasKey(key) {
147         return this[strategies].has(key);
148     }
149
150     /**
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
153      * strategy.
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.
157      */
158     merge(...objects) {
159
160         // double check arguments
161         if (objects.length < 2) {
162             throw new Error("merge() requires at least two arguments.");
163         }
164
165         if (objects.some(object => (object == null || typeof object !== "object"))) {
166             throw new Error("All arguments must be objects.");
167         }
168
169         return objects.reduce((result, object) => {
170             
171             this.validate(object);
172             
173             for (const [key, strategy] of this[strategies]) {
174                 try {
175                     if (key in result || key in object) {
176                         const value = strategy.merge.call(this, result[key], object[key]);
177                         if (value !== undefined) {
178                             result[key] = value;
179                         }
180                     }
181                 } catch (ex) {
182                     ex.message = `Key "${key}": ` + ex.message;
183                     throw ex;
184                 }
185             }
186             return result;
187         }, {});
188     }
189
190     /**
191      * Validates an object's keys based on the validate strategy for each key.
192      * @param {Object} object The object to validate.
193      * @returns {void}
194      * @throws {Error} When the object is invalid. 
195      */
196     validate(object) {
197
198         // check existing keys first
199         for (const key of Object.keys(object)) {
200
201             // check to see if the key is defined
202             if (!this.hasKey(key)) {
203                 throw new Error(`Unexpected key "${key}" found.`);
204             }
205
206             // validate existing keys
207             const strategy = this[strategies].get(key);
208
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("\", \"")}".`);
213                 }
214             }
215
216             // now apply remaining validation strategy
217             try {
218                 strategy.validate.call(strategy, object[key]);
219             } catch (ex) {
220                 ex.message = `Key "${key}": ` + ex.message;
221                 throw ex;
222             }
223         }
224
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}".`);
229             }
230         }
231
232     }
233 }
234
235 exports.ObjectSchema = ObjectSchema;