-
-
Notifications
You must be signed in to change notification settings - Fork 240
/
function.ts
205 lines (174 loc) · 6.62 KB
/
function.ts
1
2
3
4
5
6
7
8
9
10
11
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
import Ajv, { ErrorObject } from 'ajv';
import addFormats from 'ajv-formats';
import ajvErrors from 'ajv-errors';
import type { RequiredError } from 'ajv/dist/vocabularies/validation/required';
import type { AdditionalPropertiesError } from 'ajv/dist/vocabularies/applicator/additionalProperties';
import type { EnumError } from 'ajv/dist/vocabularies/validation/enum';
import type { JSONSchema7 } from 'json-schema';
import { printPath, PrintStyle, printValue } from '@stoplight/spectral-runtime';
import { RulesetValidationError } from './validation/index';
import { IFunctionResult, JSONSchema, RulesetFunction, RulesetFunctionWithValidator } from '../types';
import { isObject } from 'lodash';
import AggregateError = require('es-aggregate-error');
const ajv = new Ajv({ allErrors: true, allowUnionTypes: true, strict: true, keywords: ['x-internal'] });
ajvErrors(ajv);
addFormats(ajv);
export class RulesetFunctionValidationError extends RulesetValidationError {
constructor(fn: string, error: ErrorObject) {
super(
'invalid-function-options',
RulesetFunctionValidationError.printMessage(fn, error),
RulesetFunctionValidationError.getPath(error),
);
}
private static getPath(error: ErrorObject): string[] {
const path: string[] = [
'functionOptions',
...(error.instancePath === '' ? [] : error.instancePath.slice(1).split('/')),
];
switch (error.keyword) {
case 'additionalProperties': {
const additionalProperty = (error as AdditionalPropertiesError).params.additionalProperty;
path.push(additionalProperty);
break;
}
}
return path;
}
private static printMessage(fn: string, error: ErrorObject): string {
switch (error.keyword) {
case 'type': {
const path = printPath(error.instancePath.slice(1).split('/'), PrintStyle.Dot);
const values = Array.isArray(error.params.type) ? error.params.type.join(', ') : String(error.params.type);
return `"${fn}" function and its "${path}" option accepts only the following types: ${values}`;
}
case 'required': {
const missingProperty = (error as RequiredError).params.missingProperty;
const missingPropertyPath =
error.instancePath === ''
? missingProperty
: printPath([...error.instancePath.slice(1).split('/'), missingProperty], PrintStyle.Dot);
return `"${fn}" function is missing "${missingPropertyPath}" option`;
}
case 'additionalProperties': {
const additionalProperty = (error as AdditionalPropertiesError).params.additionalProperty;
const additionalPropertyPath =
error.instancePath === ''
? additionalProperty
: printPath([...error.instancePath.slice(1).split('/'), additionalProperty], PrintStyle.Dot);
return `"${fn}" function does not support "${additionalPropertyPath}" option`;
}
case 'enum': {
const path = printPath(error.instancePath.slice(1).split('/'), PrintStyle.Dot);
const values = (error as EnumError).params.allowedValues.map(printValue).join(', ');
return `"${fn}" function and its "${path}" option accepts only the following values: ${values}`;
}
default:
return error.message ?? 'unknown error';
}
}
}
type SchemaKeyedFragmentKeyword = 'properties' | 'patternProperties' | 'definitions';
type SchemaFragmentKeyword = 'additionalItems' | 'propertyNames' | 'if' | 'then' | 'else' | 'not';
type SchemaCompoundKeyword = 'allOf' | 'anyOf' | 'oneOf';
type Schema = (
| (Omit<
JSONSchema,
SchemaKeyedFragmentKeyword | SchemaFragmentKeyword | SchemaCompoundKeyword | 'items' | 'dependencies'
> & {
'x-internal'?: boolean;
errorMessage?: string | { [key in keyof JSONSchema]: string };
})
| { 'x-internal': boolean }
) & {
[key in SchemaKeyedFragmentKeyword]?: {
[key: string]: SchemaDefinition;
};
} & {
[key in SchemaFragmentKeyword]?: SchemaDefinition;
} & {
[key in SchemaCompoundKeyword]?: SchemaDefinition[];
} & {
items?: SchemaDefinition | SchemaDefinition[];
dependencies?: SchemaDefinition | string[];
};
export type SchemaDefinition = Schema | boolean;
const DEFAULT_OPTIONS_VALIDATOR = (o: unknown): boolean => o === null;
export function createRulesetFunction<I, O>(
{
input,
errorOnInvalidInput = false,
options,
}: {
input: Schema | null;
errorOnInvalidInput?: boolean;
options: Schema | null;
},
fn: RulesetFunction<I, O>,
): RulesetFunctionWithValidator<I, O> {
const validateOptions = options === null ? DEFAULT_OPTIONS_VALIDATOR : ajv.compile(options);
const validateInput = input !== null ? ajv.compile(input) : input;
type WrappedRulesetFunction = RulesetFunction<I, O> & {
validator<O = unknown>(options: unknown): asserts options is O;
schemas?: Readonly<{
input: Readonly<JSONSchema7> | null;
options: Readonly<JSONSchema7> | null;
}>;
};
const wrappedFn: WrappedRulesetFunction = function (
input,
options,
...args
): void | IFunctionResult[] | Promise<void | IFunctionResult[]> {
if (validateInput?.(input) === false) {
if (errorOnInvalidInput) {
return [
{
message: validateInput.errors?.find(error => error.keyword === 'errorMessage')?.message ?? 'invalid input',
},
];
}
return;
}
wrappedFn.validator(options);
return fn(input, options, ...args);
};
Reflect.defineProperty(wrappedFn, 'name', { value: fn.name });
const validOpts = new WeakSet();
wrappedFn.validator = function (o: unknown): asserts o is O {
if (isObject(o) && validOpts.has(o)) return; // I don't like this.
if (validateOptions(o)) {
if (isObject(o)) validOpts.add(o);
return;
}
if (options === null) {
throw new RulesetValidationError(
'invalid-function-options',
`"${fn.name || '<unknown>'}" function does not accept any options`,
['functionOptions'],
);
} else if (
'errors' in validateOptions &&
Array.isArray(validateOptions.errors) &&
validateOptions.errors.length > 0
) {
throw new AggregateError(
validateOptions.errors.map(error => new RulesetFunctionValidationError(fn.name || '<unknown>', error)),
);
} else {
throw new RulesetValidationError(
'invalid-function-options',
`"functionOptions" of "${fn.name || '<unknown>'}" function must be valid`,
['functionOptions'],
);
}
};
Reflect.defineProperty(wrappedFn, 'schemas', {
enumerable: false,
value: {
input,
options,
},
});
return wrappedFn as RulesetFunctionWithValidator<I, O>;
}