source: imaps-frontend/node_modules/@humanwhocodes/object-schema/src/object-schema.js@ d565449

main
Last change on this file since d565449 was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

  • Property mode set to 100644
File size: 9.4 KB
RevLine 
[d565449]1/**
2 * @filedescription Object Schema
3 */
4
5"use strict";
6
7//-----------------------------------------------------------------------------
8// Requirements
9//-----------------------------------------------------------------------------
10
11const { MergeStrategy } = require("./merge-strategy");
12const { ValidationStrategy } = require("./validation-strategy");
13
14//-----------------------------------------------------------------------------
15// Private
16//-----------------------------------------------------------------------------
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 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 */
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// Errors
67//-----------------------------------------------------------------------------
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 represented
126 for (const key of Object.keys(source)) {
127 if (!(key in this)) {
128 this[key] = source[key];
129 }
130 }
131 }
132}
133
134//-----------------------------------------------------------------------------
135// Main
136//-----------------------------------------------------------------------------
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 strategies
156 */
157 this[strategies] = new Map();
158
159 /**
160 * Separately track any keys that are required for faster validation.
161 * @type {Map}
162 * @property requiredKeys
163 */
164 this[requiredKeys] = new Map();
165
166 // add in all strategies
167 for (const key of Object.keys(definitions)) {
168 validateDefinition(key, definitions[key]);
169
170 // normalize merge and validate methods if subschema is present
171 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 string
186 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 string
194 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 keys
220 * of the all objects. Keys are merged based on the each key's merge
221 * 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 arguments
229 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 first
266 for (const key of Object.keys(object)) {
267
268 // check to see if the key is defined
269 if (!this.hasKey(key)) {
270 throw new UnexpectedKeyError(key);
271 }
272
273 // validate existing keys
274 const strategy = this[strategies].get(key);
275
276 // first check to see if any other keys are required
277 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 strategy
284 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 missing
292 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;
Note: See TracBrowser for help on using the repository browser.