UNPKG

12 kBJavaScriptView Raw
1import util from 'vis-util';
2
3let errorFound = false;
4let allOptions;
5let printStyle = 'background: #FFeeee; color: #dd0000';
6/**
7 * Used to validate options.
8 */
9class Validator {
10 /**
11 * @ignore
12 */
13 constructor() {
14 }
15
16 /**
17 * Main function to be called
18 * @param {Object} options
19 * @param {Object} referenceOptions
20 * @param {Object} subObject
21 * @returns {boolean}
22 * @static
23 */
24 static validate(options, referenceOptions, subObject) {
25 errorFound = false;
26 allOptions = referenceOptions;
27 let usedOptions = referenceOptions;
28 if (subObject !== undefined) {
29 usedOptions = referenceOptions[subObject];
30 }
31 Validator.parse(options, usedOptions, []);
32 return errorFound;
33 }
34
35
36 /**
37 * Will traverse an object recursively and check every value
38 * @param {Object} options
39 * @param {Object} referenceOptions
40 * @param {array} path | where to look for the actual option
41 * @static
42 */
43 static parse(options, referenceOptions, path) {
44 for (let option in options) {
45 if (options.hasOwnProperty(option)) {
46 Validator.check(option, options, referenceOptions, path);
47 }
48 }
49 }
50
51
52 /**
53 * Check every value. If the value is an object, call the parse function on that object.
54 * @param {string} option
55 * @param {Object} options
56 * @param {Object} referenceOptions
57 * @param {array} path | where to look for the actual option
58 * @static
59 */
60 static check(option, options, referenceOptions, path) {
61 if (referenceOptions[option] === undefined && referenceOptions.__any__ === undefined) {
62 Validator.getSuggestion(option, referenceOptions, path);
63 return;
64 }
65
66 let referenceOption = option;
67 let is_object = true;
68
69 if (referenceOptions[option] === undefined && referenceOptions.__any__ !== undefined) {
70 // NOTE: This only triggers if the __any__ is in the top level of the options object.
71 // THAT'S A REALLY BAD PLACE TO ALLOW IT!!!!
72 // TODO: Examine if needed, remove if possible
73
74 // __any__ is a wildcard. Any value is accepted and will be further analysed by reference.
75 referenceOption = '__any__';
76
77 // if the any-subgroup is not a predefined object in the configurator,
78 // we do not look deeper into the object.
79 is_object = (Validator.getType(options[option]) === 'object');
80 }
81 else {
82 // Since all options in the reference are objects, we can check whether
83 // they are supposed to be the object to look for the __type__ field.
84 // if this is an object, we check if the correct type has been supplied to account for shorthand options.
85 }
86
87 let refOptionObj = referenceOptions[referenceOption];
88 if (is_object && refOptionObj.__type__ !== undefined) {
89 refOptionObj = refOptionObj.__type__;
90 }
91
92 Validator.checkFields(option, options, referenceOptions, referenceOption, refOptionObj, path);
93 }
94
95 /**
96 *
97 * @param {string} option | the option property
98 * @param {Object} options | The supplied options object
99 * @param {Object} referenceOptions | The reference options containing all options and their allowed formats
100 * @param {string} referenceOption | Usually this is the same as option, except when handling an __any__ tag.
101 * @param {string} refOptionObj | This is the type object from the reference options
102 * @param {Array} path | where in the object is the option
103 * @static
104 */
105 static checkFields(option, options, referenceOptions, referenceOption, refOptionObj, path) {
106 let log = function(message) {
107 console.log('%c' + message + Validator.printLocation(path, option), printStyle);
108 };
109
110 let optionType = Validator.getType(options[option]);
111 let refOptionType = refOptionObj[optionType];
112
113 if (refOptionType !== undefined) {
114 // if the type is correct, we check if it is supposed to be one of a few select values
115 if (Validator.getType(refOptionType) === 'array' && refOptionType.indexOf(options[option]) === -1) {
116 log('Invalid option detected in "' + option + '".' +
117 ' Allowed values are:' + Validator.print(refOptionType) +
118 ' not "' + options[option] + '". ');
119 errorFound = true;
120 }
121 else if (optionType === 'object' && referenceOption !== "__any__") {
122 path = util.copyAndExtendArray(path, option);
123 Validator.parse(options[option], referenceOptions[referenceOption], path);
124 }
125 }
126 else if (refOptionObj['any'] === undefined) {
127 // type of the field is incorrect and the field cannot be any
128 log('Invalid type received for "' + option +
129 '". Expected: ' + Validator.print(Object.keys(refOptionObj)) +
130 '. Received [' + optionType + '] "' + options[option] + '"');
131 errorFound = true;
132 }
133 }
134
135 /**
136 *
137 * @param {Object|boolean|number|string|Array.<number>|Date|Node|Moment|undefined|null} object
138 * @returns {string}
139 * @static
140 */
141 static getType(object) {
142 var type = typeof object;
143
144 if (type === 'object') {
145 if (object === null) {
146 return 'null';
147 }
148 if (object instanceof Boolean) {
149 return 'boolean';
150 }
151 if (object instanceof Number) {
152 return 'number';
153 }
154 if (object instanceof String) {
155 return 'string';
156 }
157 if (Array.isArray(object)) {
158 return 'array';
159 }
160 if (object instanceof Date) {
161 return 'date';
162 }
163 if (object.nodeType !== undefined) {
164 return 'dom';
165 }
166 if (object._isAMomentObject === true) {
167 return 'moment';
168 }
169 return 'object';
170 }
171 else if (type === 'number') {
172 return 'number';
173 }
174 else if (type === 'boolean') {
175 return 'boolean';
176 }
177 else if (type === 'string') {
178 return 'string';
179 }
180 else if (type === undefined) {
181 return 'undefined';
182 }
183 return type;
184 }
185
186 /**
187 * @param {string} option
188 * @param {Object} options
189 * @param {Array.<string>} path
190 * @static
191 */
192 static getSuggestion(option, options, path) {
193 let localSearch = Validator.findInOptions(option,options,path,false);
194 let globalSearch = Validator.findInOptions(option,allOptions,[],true);
195
196 let localSearchThreshold = 8;
197 let globalSearchThreshold = 4;
198
199 let msg;
200 if (localSearch.indexMatch !== undefined) {
201 msg = ' in ' + Validator.printLocation(localSearch.path, option,'') +
202 'Perhaps it was incomplete? Did you mean: "' + localSearch.indexMatch + '"?\n\n';
203 }
204 else if (globalSearch.distance <= globalSearchThreshold && localSearch.distance > globalSearch.distance) {
205 msg = ' in ' + Validator.printLocation(localSearch.path, option,'') +
206 'Perhaps it was misplaced? Matching option found at: ' +
207 Validator.printLocation(globalSearch.path, globalSearch.closestMatch,'');
208 }
209 else if (localSearch.distance <= localSearchThreshold) {
210 msg = '. Did you mean "' + localSearch.closestMatch + '"?' +
211 Validator.printLocation(localSearch.path, option);
212 }
213 else {
214 msg = '. Did you mean one of these: ' + Validator.print(Object.keys(options)) +
215 Validator.printLocation(path, option);
216 }
217
218 console.log('%cUnknown option detected: "' + option + '"' + msg, printStyle);
219 errorFound = true;
220 }
221
222 /**
223 * traverse the options in search for a match.
224 * @param {string} option
225 * @param {Object} options
226 * @param {Array} path | where to look for the actual option
227 * @param {boolean} [recursive=false]
228 * @returns {{closestMatch: string, path: Array, distance: number}}
229 * @static
230 */
231 static findInOptions(option, options, path, recursive = false) {
232 let min = 1e9;
233 let closestMatch = '';
234 let closestMatchPath = [];
235 let lowerCaseOption = option.toLowerCase();
236 let indexMatch = undefined;
237 for (let op in options) { // eslint-disable-line guard-for-in
238 let distance;
239 if (options[op].__type__ !== undefined && recursive === true) {
240 let result = Validator.findInOptions(option, options[op], util.copyAndExtendArray(path,op));
241 if (min > result.distance) {
242 closestMatch = result.closestMatch;
243 closestMatchPath = result.path;
244 min = result.distance;
245 indexMatch = result.indexMatch;
246 }
247 }
248 else {
249 if (op.toLowerCase().indexOf(lowerCaseOption) !== -1) {
250 indexMatch = op;
251 }
252 distance = Validator.levenshteinDistance(option, op);
253 if (min > distance) {
254 closestMatch = op;
255 closestMatchPath = util.copyArray(path);
256 min = distance;
257 }
258 }
259 }
260 return {closestMatch:closestMatch, path:closestMatchPath, distance:min, indexMatch: indexMatch};
261 }
262
263 /**
264 * @param {Array.<string>} path
265 * @param {Object} option
266 * @param {string} prefix
267 * @returns {String}
268 * @static
269 */
270 static printLocation(path, option, prefix = 'Problem value found at: \n') {
271 let str = '\n\n' + prefix + 'options = {\n';
272 for (let i = 0; i < path.length; i++) {
273 for (let j = 0; j < i + 1; j++) {
274 str += ' ';
275 }
276 str += path[i] + ': {\n'
277 }
278 for (let j = 0; j < path.length + 1; j++) {
279 str += ' ';
280 }
281 str += option + '\n';
282 for (let i = 0; i < path.length + 1; i++) {
283 for (let j = 0; j < path.length - i; j++) {
284 str += ' ';
285 }
286 str += '}\n'
287 }
288 return str + '\n\n';
289 }
290
291 /**
292 * @param {Object} options
293 * @returns {String}
294 * @static
295 */
296 static print(options) {
297 return JSON.stringify(options).replace(/(\")|(\[)|(\])|(,"__type__")/g, "").replace(/(\,)/g, ', ')
298 }
299
300
301 /**
302 * Compute the edit distance between the two given strings
303 * http://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#JavaScript
304 *
305 * Copyright (c) 2011 Andrei Mackenzie
306 *
307 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
308 *
309 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
310 *
311 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
312 *
313 * @param {string} a
314 * @param {string} b
315 * @returns {Array.<Array.<number>>}}
316 * @static
317 */
318 static levenshteinDistance(a, b) {
319 if (a.length === 0) return b.length;
320 if (b.length === 0) return a.length;
321
322 var matrix = [];
323
324 // increment along the first column of each row
325 var i;
326 for (i = 0; i <= b.length; i++) {
327 matrix[i] = [i];
328 }
329
330 // increment each column in the first row
331 var j;
332 for (j = 0; j <= a.length; j++) {
333 matrix[0][j] = j;
334 }
335
336 // Fill in the rest of the matrix
337 for (i = 1; i <= b.length; i++) {
338 for (j = 1; j <= a.length; j++) {
339 if (b.charAt(i - 1) == a.charAt(j - 1)) {
340 matrix[i][j] = matrix[i - 1][j - 1];
341 } else {
342 matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution
343 Math.min(matrix[i][j - 1] + 1, // insertion
344 matrix[i - 1][j] + 1)); // deletion
345 }
346 }
347 }
348
349 return matrix[b.length][a.length];
350 }
351}
352
353
354export {
355 Validator,
356 printStyle
357};