1 | import { ErrorObject } from 'ajv';
|
2 |
|
3 | import groupby from 'lodash.groupby';
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 | export function describeErrors(allErrors: ErrorObject[]): string[] {
|
10 | const processedErrors = filterRelevantErrors(allErrors);
|
11 | return processedErrors.map(describeError);
|
12 | }
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 | function filterRelevantErrors(allErrors: ErrorObject[]): ErrorObject[] {
|
56 |
|
57 | const META_SCHEMA_KEYWORDS = Object.freeze(['anyOf', 'allOf', 'oneOf']);
|
58 |
|
59 |
|
60 | const [metaErrors, singleErrors] = split(allErrors, (error) => META_SCHEMA_KEYWORDS.includes(error.keyword));
|
61 |
|
62 |
|
63 | const nonShadowedSingleErrors = removeShadowingErrors(singleErrors, metaErrors);
|
64 |
|
65 |
|
66 | const [typeErrors, nonTypeErrors] = split(nonShadowedSingleErrors, (error) => error.keyword === 'type');
|
67 |
|
68 |
|
69 |
|
70 |
|
71 | const nonShadowingTypeErrors = typeErrors.filter(
|
72 | (typeError) => !nonTypeErrors.some((nonTypeError) => nonTypeError.instancePath === typeError.instancePath)
|
73 | );
|
74 | const typeErrorsMerged = mergeTypeErrorsByPath(nonShadowingTypeErrors);
|
75 | return [...nonTypeErrors, ...typeErrorsMerged];
|
76 | }
|
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 |
|
85 |
|
86 | function removeShadowingErrors(singleErrors: ErrorObject[], metaErrors: ErrorObject[]): ErrorObject[] {
|
87 | return singleErrors.filter((error) => {
|
88 | if (metaErrors.some((metaError) => error.instancePath.startsWith(metaError.instancePath))) {
|
89 | return !singleErrors.some(
|
90 | (otherError) => otherError.instancePath.startsWith(error.instancePath) && otherError.instancePath.length > error.instancePath.length
|
91 | );
|
92 | } else {
|
93 | return true;
|
94 | }
|
95 | });
|
96 | }
|
97 |
|
98 | function split<T>(items: T[], splitFn: (item: T) => boolean): [T[], T[]] {
|
99 | return [items.filter(splitFn), items.filter((error) => !splitFn(error))];
|
100 | }
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 | function mergeTypeErrorsByPath(typeErrors: ErrorObject[]): ErrorObject[] {
|
110 | const typeErrorsByPath = groupby(typeErrors, (error) => error.instancePath);
|
111 | return Object.values(typeErrorsByPath).map(mergeTypeErrors);
|
112 |
|
113 | function mergeTypeErrors(errors: ErrorObject[]): ErrorObject {
|
114 | const params = {
|
115 | type: errors.map((error) => error.params.type).join(','),
|
116 | };
|
117 | return {
|
118 | ...errors[0],
|
119 | params,
|
120 | };
|
121 | }
|
122 | }
|
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 | function describeError(error: ErrorObject): string {
|
129 | const errorPrefix = `Config option "${error.instancePath.substr(1)}"`;
|
130 |
|
131 | switch (error.keyword) {
|
132 | case 'type':
|
133 | const expectedTypeDescription = error.params.type.split(',').join(' or ');
|
134 | return `${errorPrefix} has the wrong type. It should be a ${expectedTypeDescription}, but was a ${jsonSchemaType(error.data)}.`;
|
135 | case 'enum':
|
136 | return `${errorPrefix} should be one of the allowed values (${error.params.allowedValues.map(stringify).join(', ')}), but was ${stringify(
|
137 | error.data
|
138 | )}.`;
|
139 | case 'minimum':
|
140 | case 'maximum':
|
141 | return `${errorPrefix} ${error.message}, was ${error.data}.`;
|
142 | default:
|
143 | return `${errorPrefix} ${error.message!.replace(/'/g, '"')}`;
|
144 | }
|
145 | }
|
146 |
|
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
152 | function jsonSchemaType(value: unknown): string {
|
153 | if (value === null) {
|
154 | return 'null';
|
155 | }
|
156 | if (value === undefined) {
|
157 | return 'undefined';
|
158 | }
|
159 | if (Array.isArray(value)) {
|
160 | return 'array';
|
161 | }
|
162 | return typeof value;
|
163 | }
|
164 |
|
165 | function stringify(value: unknown): string {
|
166 | if (typeof value === 'number' && isNaN(value)) {
|
167 | return 'NaN';
|
168 | } else {
|
169 | return JSON.stringify(value);
|
170 | }
|
171 | }
|