UNPKG

11.7 kBJavaScriptView Raw
1"use strict";
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.fuzzySearch = fuzzySearch;
7exports.default = void 0;
8
9var _diagnostic = _interopRequireWildcard(require("@parcel/diagnostic"));
10
11var _jsLevenshtein = _interopRequireDefault(require("js-levenshtein"));
12
13function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
14
15function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }
16
17function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
18
19// flowlint-next-line untyped-import:off
20function validateSchema(schema, data) {
21 function walk(schemaAncestors, dataNode, dataPath) {
22 let [schemaNode] = schemaAncestors;
23
24 if (schemaNode.type) {
25 let type = Array.isArray(dataNode) ? 'array' : typeof dataNode;
26
27 if (schemaNode.type !== type) {
28 return {
29 type: 'type',
30 dataType: 'value',
31 dataPath,
32 expectedTypes: [schemaNode.type],
33 ancestors: schemaAncestors,
34 prettyType: schemaNode.__type
35 };
36 } else {
37 switch (schemaNode.type) {
38 case 'array':
39 {
40 if (schemaNode.items) {
41 let results = []; // $FlowFixMe type was already checked
42
43 for (let i = 0; i < dataNode.length; i++) {
44 let result = walk([schemaNode.items].concat(schemaAncestors), // $FlowFixMe type was already checked
45 dataNode[i], dataPath + '/' + i);
46 if (result) results.push(result);
47 }
48
49 if (results.length) return results.reduce((acc, v) => acc.concat(v), []);
50 }
51
52 break;
53 }
54
55 case 'string':
56 {
57 // $FlowFixMe type was already checked
58 let value = dataNode;
59
60 if (schemaNode.enum) {
61 if (!schemaNode.enum.includes(value)) {
62 return {
63 type: 'enum',
64 dataType: 'value',
65 dataPath,
66 expectedValues: schemaNode.enum,
67 actualValue: value,
68 ancestors: schemaAncestors
69 };
70 }
71 } else if (schemaNode.__validate) {
72 let validationError = schemaNode.__validate(value); // $FlowFixMe
73
74
75 if (validationError) {
76 return {
77 type: 'other',
78 dataType: 'value',
79 dataPath,
80 message: validationError,
81 actualValue: value,
82 ancestors: schemaAncestors
83 };
84 }
85 }
86
87 break;
88 }
89
90 case 'object':
91 {
92 let results = [];
93 let invalidProps;
94
95 if (schemaNode.__forbiddenProperties) {
96 // $FlowFixMe type was already checked
97 let keys = Object.keys(dataNode);
98 invalidProps = schemaNode.__forbiddenProperties.filter(val => keys.includes(val));
99 results.push(...invalidProps.map(k => ({
100 type: 'forbidden-prop',
101 dataPath: dataPath + '/' + k,
102 dataType: 'key',
103 prop: k,
104 expectedProps: Object.keys(schemaNode.properties),
105 actualProps: keys,
106 ancestors: schemaAncestors
107 })));
108 }
109
110 if (schemaNode.required) {
111 // $FlowFixMe type was already checked
112 let keys = Object.keys(dataNode);
113 let missingKeys = schemaNode.required.filter(val => !keys.includes(val));
114 results.push(...missingKeys.map(k => ({
115 type: 'missing-prop',
116 dataPath,
117 dataType: null,
118 prop: k,
119 expectedProps: schemaNode.required,
120 actualProps: keys,
121 ancestors: schemaAncestors
122 })));
123 }
124
125 if (schemaNode.properties) {
126 let {
127 additionalProperties = true
128 } = schemaNode; // $FlowFixMe type was already checked
129
130 for (let k in dataNode) {
131 if (invalidProps && invalidProps.includes(k)) {
132 // Don't check type on forbidden props
133 continue;
134 } else if (k in schemaNode.properties) {
135 let result = walk([schemaNode.properties[k]].concat(schemaAncestors), // $FlowFixMe type was already checked
136 dataNode[k], dataPath + '/' + k);
137 if (result) results.push(result);
138 } else {
139 if (typeof additionalProperties === 'boolean') {
140 if (!additionalProperties) {
141 results.push({
142 type: 'enum',
143 dataType: 'key',
144 dataPath: dataPath + '/' + k,
145 expectedValues: Object.keys(schemaNode.properties).filter( // $FlowFixMe type was already checked
146 p => !(p in dataNode)),
147 actualValue: k,
148 ancestors: schemaAncestors,
149 prettyType: schemaNode.__type
150 });
151 }
152 } else {
153 let result = walk([additionalProperties].concat(schemaAncestors), // $FlowFixMe type was already checked
154 dataNode[k], dataPath + '/' + k);
155 if (result) results.push(result);
156 }
157 }
158 }
159 }
160
161 if (results.length) return results.reduce((acc, v) => acc.concat(v), []);
162 break;
163 }
164
165 case 'boolean':
166 // NOOP, type was checked already
167 break;
168
169 default:
170 throw new Error(`Unimplemented schema type ${type}?`);
171 }
172 }
173 } else {
174 if (schemaNode.enum && !schemaNode.enum.includes(dataNode)) {
175 return {
176 type: 'enum',
177 dataType: 'value',
178 dataPath: dataPath,
179 expectedValues: schemaNode.enum,
180 actualValue: schemaNode,
181 ancestors: schemaAncestors
182 };
183 }
184
185 if (schemaNode.oneOf || schemaNode.allOf) {
186 let list = schemaNode.oneOf || schemaNode.allOf;
187 let results = [];
188
189 for (let f of list) {
190 let result = walk([f].concat(schemaAncestors), dataNode, dataPath);
191 if (result) results.push(result);
192 }
193
194 if (schemaNode.oneOf ? results.length == schemaNode.oneOf.length : results.length > 0) {
195 // return the result with more values / longer key
196 results.sort((a, b) => Array.isArray(a) || Array.isArray(b) ? Array.isArray(a) && !Array.isArray(b) ? -1 : !Array.isArray(a) && Array.isArray(b) ? 1 : Array.isArray(a) && Array.isArray(b) ? b.length - a.length : 0 : b.dataPath.length - a.dataPath.length);
197 return results[0];
198 }
199 } else if (schemaNode.not) {
200 let result = walk([schemaNode.not].concat(schemaAncestors), dataNode, dataPath);
201
202 if (!result || result.length == 0) {
203 return {
204 type: 'other',
205 dataPath,
206 dataType: null,
207 message: schemaNode.__message,
208 actualValue: dataNode,
209 ancestors: schemaAncestors
210 };
211 }
212 }
213 }
214
215 return undefined;
216 }
217
218 let result = walk([schema], data, '');
219 return Array.isArray(result) ? result : result ? [result] : [];
220}
221
222var _default = validateSchema;
223exports.default = _default;
224
225function fuzzySearch(expectedValues, actualValue) {
226 let result = expectedValues.map(exp => [exp, (0, _jsLevenshtein.default)(exp, actualValue)]).filter( // Remove if more than half of the string would need to be changed
227 ([, d]) => d * 2 < actualValue.length);
228 result.sort(([, a], [, b]) => a - b);
229 return result.map(([v]) => v);
230}
231
232validateSchema.diagnostic = function (schema, data, dataContentsPath, dataContents, origin, prependKey, message) {
233 let errors = validateSchema(schema, data);
234
235 if (errors.length) {
236 let dataContentsString = typeof dataContents === 'string' ? dataContents : // $FlowFixMe
237 JSON.stringify(dataContents, null, '\t');
238 let keys = errors.map(e => {
239 let message;
240
241 if (e.type === 'enum') {
242 let {
243 actualValue
244 } = e;
245 let expectedValues = e.expectedValues.map(String);
246 let likely = actualValue != null ? fuzzySearch(expectedValues, String(actualValue)) : [];
247
248 if (likely.length > 0) {
249 message = `Did you mean ${likely.map(v => JSON.stringify(v)).join(', ')}?`;
250 } else if (expectedValues.length > 0) {
251 message = `Possible values: ${expectedValues.map(v => JSON.stringify(v)).join(', ')}`;
252 } else {
253 message = 'Unexpected value';
254 }
255 } else if (e.type === 'forbidden-prop') {
256 let {
257 prop,
258 expectedProps,
259 actualProps
260 } = e;
261 let likely = fuzzySearch(expectedProps, prop).filter(v => !actualProps.includes(v));
262
263 if (likely.length > 0) {
264 message = `Did you mean ${likely.map(v => JSON.stringify(v)).join(', ')}?`;
265 } else {
266 message = 'Unexpected property';
267 }
268 } else if (e.type === 'missing-prop') {
269 let {
270 prop,
271 actualProps
272 } = e;
273 let likely = fuzzySearch(actualProps, prop);
274
275 if (likely.length > 0) {
276 message = `Did you mean ${likely.map(v => JSON.stringify(v)).join(', ')}?`;
277 e.dataPath += '/' + prop;
278 e.dataType = 'key';
279 } else {
280 message = `Missing property ${prop}`;
281 }
282 } else if (e.type === 'type') {
283 if (e.prettyType != null) {
284 message = `Expected ${e.prettyType}`;
285 } else {
286 message = `Expected type ${e.expectedTypes.join(', ')}`;
287 }
288 } else {
289 message = e.message;
290 }
291
292 return {
293 key: e.dataPath,
294 type: e.dataType,
295 message
296 };
297 });
298 let codeFrame = {
299 code: dataContentsString,
300 codeHighlights: (0, _diagnostic.generateJSONCodeHighlights)(dataContentsString, keys.map(({
301 key,
302 type,
303 message
304 }) => ({
305 key: prependKey + key,
306 type: type,
307 message
308 })))
309 };
310 throw new _diagnostic.default({
311 diagnostic: {
312 message,
313 origin,
314 // $FlowFixMe should be a sketchy string check
315 filePath: dataContentsPath || undefined,
316 language: 'json',
317 codeFrame
318 }
319 });
320 }
321};
\No newline at end of file