@e22m4u/js-openapi 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/.c8rc +9 -0
  2. package/.commitlintrc +5 -0
  3. package/.editorconfig +13 -0
  4. package/.husky/commit-msg +1 -0
  5. package/.husky/pre-commit +6 -0
  6. package/.mocharc.json +4 -0
  7. package/.prettierrc +7 -0
  8. package/LICENSE +21 -0
  9. package/README.md +510 -0
  10. package/build-cjs.js +16 -0
  11. package/dist/cjs/index.cjs +2695 -0
  12. package/eslint.config.js +41 -0
  13. package/package.json +64 -0
  14. package/src/data-type/index.d.ts +1 -0
  15. package/src/data-type/index.js +1 -0
  16. package/src/data-type/infer-openapi-data-type.d.ts +30 -0
  17. package/src/data-type/infer-openapi-data-type.js +38 -0
  18. package/src/data-validation/data-format-validator-map.d.ts +13 -0
  19. package/src/data-validation/data-format-validator-map.js +36 -0
  20. package/src/data-validation/data-format-validator-map.spec.js +39 -0
  21. package/src/data-validation/data-format-validators.d.ts +84 -0
  22. package/src/data-validation/data-format-validators.js +217 -0
  23. package/src/data-validation/index.d.ts +3 -0
  24. package/src/data-validation/index.js +3 -0
  25. package/src/data-validation/validate-data-with-openapi-schema.d.ts +46 -0
  26. package/src/data-validation/validate-data-with-openapi-schema.js +1913 -0
  27. package/src/data-validation/validate-data-with-openapi-schema.spec.js +6953 -0
  28. package/src/errors/index.d.ts +1 -0
  29. package/src/errors/index.js +1 -0
  30. package/src/errors/oa-data-validation-error.d.ts +6 -0
  31. package/src/errors/oa-data-validation-error.js +6 -0
  32. package/src/errors/oa-data-validation-error.spec.js +17 -0
  33. package/src/index.d.ts +9 -0
  34. package/src/index.js +9 -0
  35. package/src/json-pointer/escape-json-pointer.d.ts +7 -0
  36. package/src/json-pointer/escape-json-pointer.js +18 -0
  37. package/src/json-pointer/escape-json-pointer.spec.js +36 -0
  38. package/src/json-pointer/index.d.ts +3 -0
  39. package/src/json-pointer/index.js +3 -0
  40. package/src/json-pointer/resolve-json-pointer.d.ts +10 -0
  41. package/src/json-pointer/resolve-json-pointer.js +83 -0
  42. package/src/json-pointer/resolve-json-pointer.spec.js +103 -0
  43. package/src/json-pointer/unescape-json-pointer.d.ts +8 -0
  44. package/src/json-pointer/unescape-json-pointer.js +18 -0
  45. package/src/json-pointer/unescape-json-pointer.spec.js +32 -0
  46. package/src/oa-document-builder.d.ts +312 -0
  47. package/src/oa-document-builder.js +450 -0
  48. package/src/oa-document-object/index.d.ts +1 -0
  49. package/src/oa-document-object/index.js +1 -0
  50. package/src/oa-document-object/validate-shallow-oa-document.d.ts +10 -0
  51. package/src/oa-document-object/validate-shallow-oa-document.js +209 -0
  52. package/src/oa-document-object/validate-shallow-oa-document.spec.js +362 -0
  53. package/src/oa-document-scope.d.ts +52 -0
  54. package/src/oa-document-scope.js +228 -0
  55. package/src/oa-reference-object/index.d.ts +3 -0
  56. package/src/oa-reference-object/index.js +3 -0
  57. package/src/oa-reference-object/is-oa-reference-object.d.ts +9 -0
  58. package/src/oa-reference-object/is-oa-reference-object.js +14 -0
  59. package/src/oa-reference-object/is-oa-reference-object.spec.js +19 -0
  60. package/src/oa-reference-object/oa-ref.d.ts +11 -0
  61. package/src/oa-reference-object/oa-ref.js +31 -0
  62. package/src/oa-reference-object/oa-ref.spec.js +56 -0
  63. package/src/oa-reference-object/resolve-oa-reference-object.d.ts +18 -0
  64. package/src/oa-reference-object/resolve-oa-reference-object.js +113 -0
  65. package/src/oa-reference-object/resolve-oa-reference-object.spec.js +233 -0
  66. package/src/oa-specification.d.ts +767 -0
  67. package/src/oa-specification.js +153 -0
  68. package/src/types.d.ts +4 -0
  69. package/src/utils/count-unicode.d.ts +11 -0
  70. package/src/utils/count-unicode.js +15 -0
  71. package/src/utils/index.d.ts +5 -0
  72. package/src/utils/index.js +5 -0
  73. package/src/utils/join-path.d.ts +6 -0
  74. package/src/utils/join-path.js +36 -0
  75. package/src/utils/join-path.spec.js +104 -0
  76. package/src/utils/normalize-path.d.ts +12 -0
  77. package/src/utils/normalize-path.js +22 -0
  78. package/src/utils/normalize-path.spec.js +56 -0
  79. package/src/utils/to-pascal-case.d.ts +6 -0
  80. package/src/utils/to-pascal-case.js +26 -0
  81. package/src/utils/to-pascal-case.spec.js +15 -0
  82. package/src/utils/to-spaced-json.d.ts +17 -0
  83. package/src/utils/to-spaced-json.js +27 -0
  84. package/tsconfig.json +14 -0
@@ -0,0 +1,1913 @@
1
+ import {countUnicode} from '../utils/count-unicode.js';
2
+ import {toSpacedJson} from '../utils/to-spaced-json.js';
3
+ import {toPascalCase} from '../utils/to-pascal-case.js';
4
+ import {OADataValidationError} from '../errors/index.js';
5
+ import {inferOpenApiDataType} from '../data-type/index.js';
6
+ import {format, InvalidArgumentError} from '@e22m4u/js-format';
7
+ import {resolveOAReferenceObject} from '../oa-reference-object/index.js';
8
+ import {OA_BUILT_IN_DATA_FORMAT_VALIDATOR_MAP} from './data-format-validator-map.js';
9
+
10
+ import {
11
+ OADataType,
12
+ OAAccessMode,
13
+ ACCESS_MODE_LIST,
14
+ } from '../oa-specification.js';
15
+
16
+ /**
17
+ * Регулярное выражение для целых чисел в формате JSON.
18
+ * Согласно RFC 8259, не допускает дробной части,
19
+ * экспоненты и явного знака "+".
20
+ */
21
+ export const JSON_INTEGER_REGEXP = /^-?\d+$/;
22
+
23
+ /**
24
+ * Регулярное выражение для чисел в формате JSON.
25
+ * Поддерживает дробную часть и научную нотацию
26
+ * (например, -1.5e+10).
27
+ */
28
+ export const JSON_NUMBER_REGEXP = /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/;
29
+
30
+ /**
31
+ * Unsupported schema properties.
32
+ */
33
+ const UNSUPPORTED_SCHEMA_KEYWORDS = [
34
+ 'nullable', // удалено в 3.1.0
35
+ 'contentEncoding',
36
+ 'contentMediaType',
37
+ 'contentSchema',
38
+ 'discriminator',
39
+ ];
40
+
41
+ /**
42
+ * Validate data with OpenAPI Schema.
43
+ *
44
+ * @param {*} data
45
+ * @param {boolean|object} schema
46
+ * @param {object} [options]
47
+ * @returns {object}
48
+ */
49
+ export function validateDataWithOpenApiSchema(data, schema, options = {}) {
50
+ if (
51
+ schema === null ||
52
+ (typeof schema !== 'boolean' && typeof schema !== 'object') ||
53
+ Array.isArray(schema)
54
+ ) {
55
+ throw new InvalidArgumentError(
56
+ 'Parameter "schema" must be a Boolean or an Object, but %v was given.',
57
+ schema,
58
+ );
59
+ }
60
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
61
+ throw new InvalidArgumentError(
62
+ 'Parameter "options" must be an Object, but %v was given.',
63
+ options,
64
+ );
65
+ }
66
+ if (options.rootDocument !== undefined) {
67
+ if (
68
+ !options.rootDocument ||
69
+ typeof options.rootDocument !== 'object' ||
70
+ Array.isArray(options.rootDocument)
71
+ ) {
72
+ throw new InvalidArgumentError(
73
+ 'Option "rootDocument" must be an Object, but %v was given.',
74
+ options.rootDocument,
75
+ );
76
+ }
77
+ }
78
+ if (options.required !== undefined) {
79
+ if (typeof options.required !== 'boolean') {
80
+ throw new InvalidArgumentError(
81
+ 'Option "required" must be a Boolean, but %v was given.',
82
+ options.required,
83
+ );
84
+ }
85
+ }
86
+ if (options.coerceTypes !== undefined) {
87
+ if (typeof options.coerceTypes !== 'boolean') {
88
+ throw new InvalidArgumentError(
89
+ 'Option "coerceTypes" must be a Boolean, but %v was given.',
90
+ options.coerceTypes,
91
+ );
92
+ }
93
+ }
94
+ if (options.applyDefaults !== undefined) {
95
+ if (typeof options.applyDefaults !== 'boolean') {
96
+ throw new InvalidArgumentError(
97
+ 'Option "applyDefaults" must be a Boolean, but %v was given.',
98
+ options.applyDefaults,
99
+ );
100
+ }
101
+ }
102
+ if (options.stripUnknown !== undefined) {
103
+ if (typeof options.stripUnknown !== 'boolean') {
104
+ throw new InvalidArgumentError(
105
+ 'Option "stripUnknown" must be a Boolean, but %v was given.',
106
+ options.stripUnknown,
107
+ );
108
+ }
109
+ }
110
+ if (options.dataSourceUri !== undefined) {
111
+ if (!options.dataSourceUri || typeof options.dataSourceUri !== 'string') {
112
+ throw new InvalidArgumentError(
113
+ 'Option "dataSourceUri" must be a non-empty String, but %v was given.',
114
+ options.dataSourceUri,
115
+ );
116
+ }
117
+ }
118
+ if (options.schemaSourceUri !== undefined) {
119
+ if (
120
+ !options.schemaSourceUri ||
121
+ typeof options.schemaSourceUri !== 'string'
122
+ ) {
123
+ throw new InvalidArgumentError(
124
+ 'Option "schemaSourceUri" must be a non-empty String, ' +
125
+ 'but %v was given.',
126
+ options.schemaSourceUri,
127
+ );
128
+ }
129
+ }
130
+ if (options.silent !== undefined) {
131
+ if (typeof options.silent !== 'boolean') {
132
+ throw new InvalidArgumentError(
133
+ 'Option "silent" must be a Boolean, but %v was given.',
134
+ options.silent,
135
+ );
136
+ }
137
+ }
138
+ if (options.accessMode !== undefined) {
139
+ if (!ACCESS_MODE_LIST.includes(options.accessMode)) {
140
+ throw new InvalidArgumentError(
141
+ 'Access mode %v is not supported.',
142
+ options.accessMode,
143
+ );
144
+ }
145
+ }
146
+ if (options.formatValidators !== undefined) {
147
+ if (
148
+ !options.formatValidators ||
149
+ typeof options.formatValidators !== 'object' ||
150
+ Array.isArray(options.formatValidators)
151
+ ) {
152
+ throw new InvalidArgumentError(
153
+ 'Option "formatValidators" must be an Object, but %v was given.',
154
+ options.formatValidators,
155
+ );
156
+ }
157
+ Object.keys(options.formatValidators).forEach(formatName => {
158
+ const formatValidator = options.formatValidators[formatName];
159
+ if (
160
+ formatValidator !== undefined &&
161
+ typeof formatValidator !== 'function'
162
+ ) {
163
+ throw new InvalidArgumentError(
164
+ 'Format validator %v must be a Function, but %v was given.',
165
+ formatName,
166
+ formatValidator,
167
+ );
168
+ }
169
+ });
170
+ }
171
+ if (options.parseJson !== undefined) {
172
+ if (typeof options.parseJson !== 'boolean') {
173
+ throw new InvalidArgumentError(
174
+ 'Option "parseJson" must be a Boolean, but %v was given.',
175
+ options.parseJson,
176
+ );
177
+ }
178
+ }
179
+ // если обязательное значение является
180
+ // undefined, то выбрасывается ошибка
181
+ if (options.required === true && data === undefined) {
182
+ const result = {
183
+ isValid: false,
184
+ value: data,
185
+ valueUri: options.dataSourceUri || '',
186
+ schemaUri: options.schemaSourceUri || '',
187
+ reason: format(
188
+ 'Value at %v is required, but %v was given.',
189
+ options.dataSourceUri || '',
190
+ data,
191
+ ),
192
+ };
193
+ if (!options.silent) {
194
+ throw new OADataValidationError(result.reason);
195
+ } else {
196
+ return result;
197
+ }
198
+ }
199
+ const subResult = _validate(data, schema, {
200
+ ...options,
201
+ dataSourceUri: options.dataSourceUri || '',
202
+ schemaSourceUri: options.schemaSourceUri || '',
203
+ });
204
+ // если данные не прошли проверку, а тихий
205
+ // режим отключен, то выбрасывается ошибка
206
+ if (!subResult.isValid && !options.silent) {
207
+ throw new OADataValidationError(subResult.reason);
208
+ }
209
+ const result = {
210
+ isValid: subResult.isValid,
211
+ reason: subResult.reason,
212
+ value: subResult.value,
213
+ valueUri: subResult.valueUri,
214
+ schemaUri: subResult.schemaUri,
215
+ };
216
+ // если проверка выполнена успешно,
217
+ // то из результата исключается "reason"
218
+ if (result.isValid) {
219
+ delete result.reason;
220
+ }
221
+ return result;
222
+ }
223
+
224
+ /**
225
+ * Validate (internal).
226
+ *
227
+ * @param {*} data
228
+ * @param {boolean|object} schema
229
+ * @param {object} options
230
+ * @returns {object}
231
+ */
232
+ function _validate(data, schema, options) {
233
+ if (
234
+ schema === null ||
235
+ (typeof schema !== 'boolean' && typeof schema !== 'object') ||
236
+ Array.isArray(schema)
237
+ ) {
238
+ throw new InvalidArgumentError(
239
+ 'Schema at %v must be a Boolean or an Object, but %v was given.',
240
+ options.schemaSourceUri,
241
+ schema,
242
+ );
243
+ }
244
+ // если схема является логическим значением,
245
+ // то значение используется как правило допуска
246
+ if (typeof schema === 'boolean') {
247
+ if (schema === true) {
248
+ return {
249
+ isValid: true,
250
+ value: data,
251
+ valueUri: options.dataSourceUri,
252
+ schemaUri: options.schemaSourceUri,
253
+ };
254
+ } else if (data !== undefined) {
255
+ return {
256
+ isValid: false,
257
+ value: data,
258
+ valueUri: options.dataSourceUri,
259
+ schemaUri: options.schemaSourceUri,
260
+ reason: format(
261
+ 'Value at %v is rejected by the boolean schema at %v.',
262
+ options.dataSourceUri,
263
+ options.schemaSourceUri,
264
+ ),
265
+ };
266
+ }
267
+ }
268
+ // сеты для сбора проверенных индексов массива
269
+ // и свойств объекта на текущем уровне вложенности
270
+ let evaluatedIndexes = new Set();
271
+ let evaluatedProperties = new Set();
272
+ // если параметр "applyDefaults" имеет значение true,
273
+ // а исходным значением является undefined, то исходное
274
+ // значение подменяется значением по умолчанию
275
+ if (
276
+ schema.default !== undefined &&
277
+ data === undefined &&
278
+ options.applyDefaults === true
279
+ ) {
280
+ data = schema.default;
281
+ }
282
+ // parse json
283
+ if (
284
+ options.parseJson === true &&
285
+ schema.type !== undefined &&
286
+ typeof data === 'string'
287
+ ) {
288
+ data = _parseJson(data, schema.type);
289
+ }
290
+ // coerce type
291
+ if (
292
+ options.coerceTypes === true &&
293
+ schema.type !== undefined &&
294
+ data !== undefined
295
+ ) {
296
+ data = _coerceType(data, schema.type);
297
+ }
298
+ // если текущая схема содержит свойство "$ref" или "$dynamicRef",
299
+ // то выполняется извлечение целевой схемы и проверка данных
300
+ // согласно извлеченной схемы
301
+ if (schema.$ref !== undefined || schema.$dynamicRef !== undefined) {
302
+ const resSchema = resolveOAReferenceObject(schema, {
303
+ rootDocument: options.rootDocument,
304
+ });
305
+ const result = _validate(data, resSchema, {
306
+ ...options,
307
+ schemaSourceUri: schema.$ref || schema.$dynamicRef,
308
+ });
309
+ if (!result.isValid) {
310
+ return result;
311
+ }
312
+ // данные могли измениться во время рекурсии,
313
+ // выполняется обновление исходного значения
314
+ data = result.value;
315
+ // внедрение аннотаций о проверенных индексах
316
+ // из результата успешной проверки
317
+ if (result.evaluatedIndexes) {
318
+ evaluatedIndexes = new Set([
319
+ ...evaluatedIndexes,
320
+ ...result.evaluatedIndexes,
321
+ ]);
322
+ }
323
+ // внедрение аннотаций о проверенных свойствах
324
+ // из результата успешной проверки
325
+ if (result.evaluatedProperties) {
326
+ evaluatedProperties = new Set([
327
+ ...evaluatedProperties,
328
+ ...result.evaluatedProperties,
329
+ ]);
330
+ }
331
+ // аннотации могут всплывать из "$ref" и "$dynamicRef"
332
+ // аппликаторов, но не передаются во вложенные схемы,
333
+ // на которые ссылаются данные ключевые слова
334
+ }
335
+ // при наличии неподдерживаемых ключевых
336
+ // слов схемы выбрасывается ошибка
337
+ UNSUPPORTED_SCHEMA_KEYWORDS.forEach(keyword => {
338
+ if (schema[keyword] !== undefined) {
339
+ throw new InvalidArgumentError(
340
+ 'Schema keyword %v is not supported.',
341
+ keyword,
342
+ );
343
+ }
344
+ });
345
+ // readOnly
346
+ if (
347
+ options.accessMode === OAAccessMode.WRITE &&
348
+ schema.readOnly === true &&
349
+ data !== undefined
350
+ ) {
351
+ return {
352
+ isValid: false,
353
+ value: data,
354
+ valueUri: options.dataSourceUri,
355
+ schemaUri: options.schemaSourceUri,
356
+ reason: format(
357
+ 'Value at %v is read-only and cannot be sent in a write context.',
358
+ options.dataSourceUri,
359
+ ),
360
+ };
361
+ }
362
+ // writeOnly
363
+ if (
364
+ options.accessMode === OAAccessMode.READ &&
365
+ schema.writeOnly === true &&
366
+ data !== undefined
367
+ ) {
368
+ return {
369
+ isValid: false,
370
+ value: data,
371
+ valueUri: options.dataSourceUri,
372
+ schemaUri: options.schemaSourceUri,
373
+ reason: format(
374
+ 'Value at %v is write-only and cannot be present in a read context.',
375
+ options.dataSourceUri,
376
+ ),
377
+ };
378
+ }
379
+ // прежде чем перейти к проверке данных "in-place" аппликаторами,
380
+ // нужно учитывать, что согласно спецификации JSON Schema 2020-12,
381
+ // аннотации должны всплывать снизу вверх или действовать на текущем
382
+ // уровне, а не погружаться в подсхемы аппликаторов
383
+ // allOf
384
+ if (schema.allOf && Array.isArray(schema.allOf)) {
385
+ for (const index in schema.allOf) {
386
+ const subSchema = schema.allOf[index];
387
+ const result = _validate(data, subSchema, {
388
+ ...options,
389
+ schemaSourceUri: options.schemaSourceUri + '/allOf/' + index,
390
+ });
391
+ if (!result.isValid) {
392
+ return result;
393
+ }
394
+ // данные могли измениться во время рекурсии,
395
+ // выполняется обновление исходного значения
396
+ data = result.value;
397
+ // внедрение аннотаций о проверенных индексах
398
+ // из результата успешной проверки
399
+ if (result.evaluatedIndexes) {
400
+ evaluatedIndexes = new Set([
401
+ ...evaluatedIndexes,
402
+ ...result.evaluatedIndexes,
403
+ ]);
404
+ }
405
+ // внедрение аннотаций о проверенных свойствах
406
+ // из результата успешной проверки
407
+ if (result.evaluatedProperties) {
408
+ evaluatedProperties = new Set([
409
+ ...evaluatedProperties,
410
+ ...result.evaluatedProperties,
411
+ ]);
412
+ }
413
+ }
414
+ }
415
+ // oneOf
416
+ if (schema.oneOf && Array.isArray(schema.oneOf)) {
417
+ let matches = 0;
418
+ let matchedResultValue = data;
419
+ let matchedEvaluatedIndexes = undefined;
420
+ let matchedEvaluatedProperties = undefined;
421
+ for (const index in schema.oneOf) {
422
+ const subSchema = schema.oneOf[index];
423
+ const result = _validate(data, subSchema, {
424
+ ...options,
425
+ schemaSourceUri: options.schemaSourceUri + '/oneOf/' + index,
426
+ });
427
+ if (result.isValid) {
428
+ matches++;
429
+ // сохранение результата приведения типов
430
+ // из последней успешной проверки
431
+ matchedResultValue = result.value;
432
+ // сохранение аннотаций о проверенных
433
+ // свойствах из успешной проверки
434
+ matchedEvaluatedIndexes = result.evaluatedIndexes;
435
+ // сохранение аннотаций о проверенных
436
+ // свойствах из успешной проверки
437
+ matchedEvaluatedProperties = result.evaluatedProperties;
438
+ }
439
+ }
440
+ if (matches !== 1) {
441
+ const schemaUri = options.schemaSourceUri + '/oneOf';
442
+ return {
443
+ isValid: false,
444
+ value: data,
445
+ valueUri: options.dataSourceUri,
446
+ schemaUri,
447
+ reason: format(
448
+ 'Value at %v must match exactly one of schemas at %v, ' +
449
+ 'but %d matched.',
450
+ options.dataSourceUri,
451
+ schemaUri,
452
+ matches,
453
+ ),
454
+ };
455
+ }
456
+ // обновление исходных данных
457
+ // значением успешной проверки
458
+ data = matchedResultValue;
459
+ // внедрение аннотаций о проверенных индексах
460
+ // из результата успешной проверки
461
+ if (matchedEvaluatedIndexes) {
462
+ evaluatedIndexes = new Set([
463
+ ...evaluatedIndexes,
464
+ ...matchedEvaluatedIndexes,
465
+ ]);
466
+ }
467
+ // внедрение аннотаций о проверенных свойствах
468
+ // из результата успешной проверки
469
+ if (matchedEvaluatedProperties) {
470
+ evaluatedProperties = new Set([
471
+ ...evaluatedProperties,
472
+ ...matchedEvaluatedProperties,
473
+ ]);
474
+ }
475
+ }
476
+ // anyOf
477
+ if (schema.anyOf && Array.isArray(schema.anyOf)) {
478
+ let matchFound = false;
479
+ for (const index in schema.anyOf) {
480
+ const subSchema = schema.anyOf[index];
481
+ const result = _validate(data, subSchema, {
482
+ ...options,
483
+ schemaSourceUri: options.schemaSourceUri + '/anyOf/' + index,
484
+ });
485
+ if (result.isValid) {
486
+ // обновление исходных данных значением
487
+ // из первой успешной проверки
488
+ if (!matchFound) {
489
+ data = result.value;
490
+ }
491
+ // внедрение аннотаций о проверенных индексах
492
+ // из результата успешной проверки
493
+ if (result.evaluatedIndexes) {
494
+ evaluatedIndexes = new Set([
495
+ ...evaluatedIndexes,
496
+ ...result.evaluatedIndexes,
497
+ ]);
498
+ }
499
+ // внедрение аннотаций о проверенных свойствах
500
+ // из результата успешной проверки
501
+ if (result.evaluatedProperties) {
502
+ evaluatedProperties = new Set([
503
+ ...evaluatedProperties,
504
+ ...result.evaluatedProperties,
505
+ ]);
506
+ }
507
+ matchFound = true;
508
+ }
509
+ }
510
+ if (!matchFound) {
511
+ const schemaUri = options.schemaSourceUri + '/anyOf';
512
+ return {
513
+ isValid: false,
514
+ value: data,
515
+ valueUri: options.dataSourceUri,
516
+ schemaUri,
517
+ reason: format(
518
+ 'Value at %v must match at least one of schemas at %v, ' +
519
+ 'but 0 matched.',
520
+ options.dataSourceUri,
521
+ schemaUri,
522
+ ),
523
+ };
524
+ }
525
+ }
526
+ // not
527
+ if (schema.not) {
528
+ const {isValid} = _validate(data, schema.not, {
529
+ ...options,
530
+ schemaSourceUri: options.schemaSourceUri + '/not',
531
+ });
532
+ if (isValid) {
533
+ return {
534
+ isValid: false,
535
+ value: data,
536
+ valueUri: options.dataSourceUri,
537
+ schemaUri: options.schemaSourceUri + '/not',
538
+ reason: format(
539
+ 'Value at %v must not match the "not" schema.',
540
+ options.dataSourceUri,
541
+ ),
542
+ };
543
+ }
544
+ // @todo должны ли мы учитывать свойства, которые
545
+ // прошли проверку в аннотациях проверенных свойств?
546
+ }
547
+ // if / then / else
548
+ if (schema.if) {
549
+ // если данные не соответствуют схеме "if",
550
+ // то результат проверки будет проигнорирован
551
+ // без ошибки для всего документа
552
+ const ifResult = _validate(data, schema.if, {
553
+ ...options,
554
+ schemaSourceUri: options.schemaSourceUri + '/if',
555
+ });
556
+ if (ifResult.isValid) {
557
+ // так как при проверке по схеме "if" данные
558
+ // могли измениться, выполняется обновление
559
+ // исходного значения
560
+ data = ifResult.value;
561
+ // внедрение аннотаций о проверенных индексах
562
+ // из результата успешной проверки схемой "if"
563
+ if (ifResult.evaluatedIndexes) {
564
+ evaluatedIndexes = new Set([
565
+ ...evaluatedIndexes,
566
+ ...ifResult.evaluatedIndexes,
567
+ ]);
568
+ }
569
+ // внедрение аннотаций о проверенных свойствах
570
+ // из результата успешной проверки схемой "if"
571
+ if (ifResult.evaluatedProperties) {
572
+ evaluatedProperties = new Set([
573
+ ...evaluatedProperties,
574
+ ...ifResult.evaluatedProperties,
575
+ ]);
576
+ }
577
+ // если определена схема "then", то данные
578
+ // обязаны соответствовать указанной схеме
579
+ if (schema.then) {
580
+ const result = _validate(data, schema.then, {
581
+ ...options,
582
+ schemaSourceUri: options.schemaSourceUri + '/then',
583
+ });
584
+ if (!result.isValid) {
585
+ return result;
586
+ }
587
+ // так как при проверке по схеме "then" данные
588
+ // могли измениться, выполняется обновление
589
+ // исходного значения
590
+ data = result.value;
591
+ // внедрение аннотаций о проверенных индексах
592
+ // из результата успешной проверки схемой "then"
593
+ if (result.evaluatedIndexes) {
594
+ evaluatedIndexes = new Set([
595
+ ...evaluatedIndexes,
596
+ ...result.evaluatedIndexes,
597
+ ]);
598
+ }
599
+ // внедрение аннотаций о проверенных свойствах
600
+ // из результата успешной проверки схемой "then"
601
+ if (result.evaluatedProperties) {
602
+ evaluatedProperties = new Set([
603
+ ...evaluatedProperties,
604
+ ...result.evaluatedProperties,
605
+ ]);
606
+ }
607
+ }
608
+ } else {
609
+ // если определена схема "else", то данные
610
+ // должны соответствовать указанной схеме
611
+ if (schema.else) {
612
+ const result = _validate(data, schema.else, {
613
+ ...options,
614
+ schemaSourceUri: options.schemaSourceUri + '/else',
615
+ });
616
+ if (!result.isValid) {
617
+ return result;
618
+ }
619
+ // так как при проверке по схеме "else" данные
620
+ // могли измениться, выполняется обновление
621
+ // исходного значения
622
+ data = result.value;
623
+ // внедрение аннотаций о проверенных индексах
624
+ // из результата успешной проверки схемой "else"
625
+ if (result.evaluatedIndexes) {
626
+ evaluatedIndexes = new Set([
627
+ ...evaluatedIndexes,
628
+ ...result.evaluatedIndexes,
629
+ ]);
630
+ }
631
+ // внедрение аннотаций о проверенных свойствах
632
+ // из результата успешной проверки схемой "else"
633
+ if (result.evaluatedProperties) {
634
+ evaluatedProperties = new Set([
635
+ ...evaluatedProperties,
636
+ ...result.evaluatedProperties,
637
+ ]);
638
+ }
639
+ }
640
+ }
641
+ }
642
+ // type
643
+ if (schema.type !== undefined && data !== undefined) {
644
+ const result = _validateType(data, schema.type, options);
645
+ if (!result.isValid) {
646
+ return result;
647
+ }
648
+ }
649
+ // format
650
+ if (schema.format) {
651
+ const result = _validateFormat(
652
+ data,
653
+ schema.format,
654
+ {...OA_BUILT_IN_DATA_FORMAT_VALIDATOR_MAP, ...options.formatValidators},
655
+ options,
656
+ );
657
+ if (!result.isValid) {
658
+ return result;
659
+ }
660
+ }
661
+ // const
662
+ if (schema.const !== undefined) {
663
+ if (!_areValuesEqual(schema.const, data)) {
664
+ return {
665
+ isValid: false,
666
+ value: data,
667
+ valueUri: options.dataSourceUri,
668
+ schemaUri: options.schemaSourceUri,
669
+ reason: format(
670
+ 'Value at %v must be equal to constant %s, but %s was given.',
671
+ options.dataSourceUri,
672
+ toSpacedJson(schema.const, {truncate: 50}),
673
+ toSpacedJson(data, {truncate: 50}),
674
+ ),
675
+ };
676
+ }
677
+ }
678
+ // enum
679
+ if (schema.enum && Array.isArray(schema.enum)) {
680
+ const isFoundInEnum = schema.enum.some(el => _areValuesEqual(el, data));
681
+ if (!isFoundInEnum) {
682
+ return {
683
+ isValid: false,
684
+ value: data,
685
+ valueUri: options.dataSourceUri,
686
+ schemaUri: options.schemaSourceUri,
687
+ reason: format(
688
+ 'Value at %v must be one of %s, but %s was given.',
689
+ options.dataSourceUri,
690
+ toSpacedJson(schema.enum, {truncate: 50}),
691
+ toSpacedJson(data, {truncate: 50}),
692
+ ),
693
+ };
694
+ }
695
+ }
696
+ // определение типа и проверка
697
+ // значения согласно данному типу
698
+ const currentType = inferOpenApiDataType(data, true);
699
+ if (currentType === OADataType.STRING) {
700
+ const result = _validateString(data, schema, options);
701
+ if (!result.isValid) {
702
+ return result;
703
+ }
704
+ } else if (
705
+ currentType === OADataType.NUMBER ||
706
+ currentType === OADataType.INTEGER
707
+ ) {
708
+ const result = _validateNumber(data, schema, options);
709
+ if (!result.isValid) {
710
+ return result;
711
+ }
712
+ } else if (currentType === OADataType.ARRAY) {
713
+ const result = _validateArray(data, schema, evaluatedIndexes, options);
714
+ if (!result.isValid) {
715
+ return result;
716
+ }
717
+ // данные могли измениться во время проверки,
718
+ // выполняется обновление исходного значения
719
+ data = result.value;
720
+ // внедрение аннотаций о проверенных
721
+ // индексах из результата проверки
722
+ if (result.evaluatedIndexes) {
723
+ evaluatedIndexes = new Set([
724
+ ...evaluatedIndexes,
725
+ ...result.evaluatedIndexes,
726
+ ]);
727
+ }
728
+ } else if (currentType === OADataType.OBJECT) {
729
+ const result = _validateObject(data, schema, evaluatedProperties, options);
730
+ if (!result.isValid) {
731
+ return result;
732
+ }
733
+ // данные могли измениться во время проверки,
734
+ // выполняется обновление исходного значения
735
+ data = result.value;
736
+ // внедрение аннотаций о проверенных
737
+ // свойствах из результата проверки
738
+ if (result.evaluatedProperties) {
739
+ evaluatedProperties = new Set([
740
+ ...evaluatedProperties,
741
+ ...result.evaluatedProperties,
742
+ ]);
743
+ }
744
+ }
745
+
746
+ return {
747
+ isValid: true,
748
+ value: data,
749
+ valueUri: options.dataSourceUri,
750
+ schemaUri: options.schemaSourceUri,
751
+ evaluatedIndexes,
752
+ evaluatedProperties,
753
+ };
754
+ }
755
+
756
+ /**
757
+ * Parse JSON.
758
+ *
759
+ * @param {*} data
760
+ * @param {string|string[]} expectedType
761
+ * @returns {*}
762
+ */
763
+ function _parseJson(data, expectedType) {
764
+ if (typeof data !== 'string') {
765
+ return data;
766
+ }
767
+ const types = Array.isArray(expectedType) ? expectedType : [expectedType];
768
+ // json спецификация RFC 8259 разрешает
769
+ // пробельные символы перед началом значения
770
+ const trimmedData = data.trim();
771
+ // разбор json строки должен выполняться только
772
+ // при наличии совместимого содержания с указанными
773
+ // в схеме типами данных
774
+ /* prettier-ignore */
775
+ if (
776
+ (types.includes(OADataType.ARRAY) && trimmedData[0] === '[') ||
777
+ (types.includes(OADataType.OBJECT) && trimmedData[0] === '{') ||
778
+ (types.includes(OADataType.INTEGER) && JSON_INTEGER_REGEXP.test(trimmedData)) ||
779
+ (types.includes(OADataType.NUMBER) && JSON_NUMBER_REGEXP.test(trimmedData)) ||
780
+ (types.includes(OADataType.BOOLEAN) && (trimmedData === 'true' || trimmedData === 'false')) ||
781
+ (types.includes(OADataType.NULL) && trimmedData === 'null')
782
+ ) {
783
+ try {
784
+ return JSON.parse(trimmedData);
785
+ } catch {
786
+ return data;
787
+ }
788
+ }
789
+ return data;
790
+ }
791
+
792
+ /**
793
+ * Coerce type.
794
+ *
795
+ * @param {*} data
796
+ * @param {string|string[]} expectedType
797
+ * @returns {*}
798
+ */
799
+ function _coerceType(data, expectedType) {
800
+ const types = Array.isArray(expectedType) ? expectedType : [expectedType];
801
+ const actualType = inferOpenApiDataType(data, true);
802
+ // если тип уже соответствует ожиданию,
803
+ // то значение возвращается без изменений
804
+ if (types.includes(actualType)) {
805
+ return data;
806
+ }
807
+ // если текущий тип является "integer",
808
+ // а ожидаемый "number" то значение
809
+ // возвращается без изменений
810
+ if (actualType === OADataType.INTEGER && types.includes(OADataType.NUMBER)) {
811
+ return data;
812
+ }
813
+ for (const type of types) {
814
+ switch (type) {
815
+ case OADataType.STRING: {
816
+ if (typeof data === 'number' || typeof data === 'boolean') {
817
+ return String(data);
818
+ }
819
+ break;
820
+ }
821
+ case OADataType.NUMBER:
822
+ case OADataType.INTEGER: {
823
+ if (typeof data === 'string' && data.trim() !== '') {
824
+ const num = Number(data);
825
+ if (!Number.isNaN(num)) {
826
+ if (type === OADataType.INTEGER && Number.isInteger(num)) {
827
+ return num;
828
+ }
829
+ if (type === OADataType.NUMBER) {
830
+ return num;
831
+ }
832
+ }
833
+ }
834
+ break;
835
+ }
836
+ case OADataType.BOOLEAN: {
837
+ if (data === 'true') {
838
+ return true;
839
+ }
840
+ if (data === 'false') {
841
+ return false;
842
+ }
843
+ break;
844
+ }
845
+ }
846
+ }
847
+ return data;
848
+ }
849
+
850
+ /**
851
+ * Validate type.
852
+ *
853
+ * @param {unknown} data
854
+ * @param {string|string[]} expectedType
855
+ * @param {object} options
856
+ * @returns {object}
857
+ */
858
+ function _validateType(data, expectedType, options) {
859
+ const allowedTypes = [expectedType].flat();
860
+ const actualType = inferOpenApiDataType(data, true);
861
+ const isMatch = allowedTypes.some(allowedType => {
862
+ if (allowedType === actualType) {
863
+ return true;
864
+ }
865
+ if (
866
+ allowedType === OADataType.NUMBER &&
867
+ actualType === OADataType.INTEGER
868
+ ) {
869
+ return true;
870
+ }
871
+ return false;
872
+ });
873
+ if (allowedTypes.length && !isMatch) {
874
+ return {
875
+ isValid: false,
876
+ value: data,
877
+ valueUri: options.dataSourceUri,
878
+ schemaUri: options.schemaSourceUri,
879
+ reason: format(
880
+ 'Value at %v must be %s, but %s was given.',
881
+ options.dataSourceUri,
882
+ allowedTypes.map(v => toPascalCase(v)).join(' or '),
883
+ toPascalCase(actualType),
884
+ ),
885
+ };
886
+ }
887
+ return {
888
+ isValid: true,
889
+ value: data,
890
+ valueUri: options.dataSourceUri,
891
+ schemaUri: options.schemaSourceUri,
892
+ };
893
+ }
894
+
895
+ /**
896
+ * Validate format.
897
+ *
898
+ * @param {*} data
899
+ * @param {string} expectedFormat
900
+ * @param {object} validatorsMap
901
+ * @param {object} options
902
+ * @returns {object}
903
+ */
904
+ function _validateFormat(data, expectedFormat, validatorsMap, options) {
905
+ const validator = validatorsMap[expectedFormat];
906
+ if (validator) {
907
+ const isValid = validator(data);
908
+ if (!isValid) {
909
+ return {
910
+ isValid: false,
911
+ value: data,
912
+ valueUri: options.dataSourceUri,
913
+ schemaUri: options.schemaSourceUri,
914
+ reason: format(
915
+ 'Value at %v must match format %v, but %s was given.',
916
+ options.dataSourceUri,
917
+ expectedFormat,
918
+ toSpacedJson(data, {truncate: 50}),
919
+ ),
920
+ };
921
+ }
922
+ }
923
+ // если валидатора нет, спецификация OpenAPI
924
+ // рекомендует игнорировать неизвестные форматы
925
+ return {
926
+ isValid: true,
927
+ value: data,
928
+ valueUri: options.dataSourceUri,
929
+ schemaUri: options.schemaSourceUri,
930
+ };
931
+ }
932
+
933
+ /**
934
+ * Deep equality check for "enum" and "uniqueItems" option.
935
+ *
936
+ * @param {unknown} a
937
+ * @param {unknown} b
938
+ * @returns {boolean}
939
+ */
940
+ function _areValuesEqual(a, b) {
941
+ if (a === b) {
942
+ return true;
943
+ }
944
+ if (
945
+ typeof a !== 'object' ||
946
+ a === null ||
947
+ typeof b !== 'object' ||
948
+ b === null
949
+ ) {
950
+ return false;
951
+ }
952
+ // Date
953
+ if (a instanceof Date && b instanceof Date) {
954
+ return a.getTime() === b.getTime();
955
+ }
956
+ // RegExp
957
+ if (a instanceof RegExp && b instanceof RegExp) {
958
+ return a.toString() === b.toString();
959
+ }
960
+ // Array
961
+ if (Array.isArray(a) !== Array.isArray(b)) {
962
+ return false;
963
+ }
964
+ if (Array.isArray(a)) {
965
+ if (a.length !== b.length) {
966
+ return false;
967
+ }
968
+ for (let i = 0; i < a.length; i++) {
969
+ if (!_areValuesEqual(a[i], b[i])) {
970
+ return false;
971
+ }
972
+ }
973
+ return true;
974
+ }
975
+ // Object
976
+ const keysA = Object.keys(a);
977
+ const keysB = Object.keys(b);
978
+ if (keysA.length !== keysB.length) {
979
+ return false;
980
+ }
981
+ for (const key of keysA) {
982
+ if (!Object.prototype.hasOwnProperty.call(b, key)) {
983
+ return false;
984
+ }
985
+ if (!_areValuesEqual(a[key], b[key])) {
986
+ return false;
987
+ }
988
+ }
989
+ return true;
990
+ }
991
+
992
+ /**
993
+ * Validate string.
994
+ *
995
+ * @param {string} data
996
+ * @param {object} schema
997
+ * @param {object} options
998
+ * @returns {object}
999
+ */
1000
+ function _validateString(data, schema, options) {
1001
+ // minLength
1002
+ if (typeof schema.minLength === 'number') {
1003
+ const dataLength = countUnicode(data);
1004
+ if (dataLength < schema.minLength) {
1005
+ return {
1006
+ isValid: false,
1007
+ value: data,
1008
+ valueUri: options.dataSourceUri,
1009
+ schemaUri: options.schemaSourceUri,
1010
+ reason: format(
1011
+ 'String at %v must be at least %d characters long, but %d was given.',
1012
+ options.dataSourceUri,
1013
+ schema.minLength,
1014
+ dataLength,
1015
+ ),
1016
+ };
1017
+ }
1018
+ }
1019
+ // maxLength
1020
+ if (typeof schema.maxLength === 'number') {
1021
+ const dataLength = countUnicode(data);
1022
+ if (dataLength > schema.maxLength) {
1023
+ return {
1024
+ isValid: false,
1025
+ value: data,
1026
+ valueUri: options.dataSourceUri,
1027
+ schemaUri: options.schemaSourceUri,
1028
+ reason: format(
1029
+ 'String at %v must be at most %d characters long, but %d was given.',
1030
+ options.dataSourceUri,
1031
+ schema.maxLength,
1032
+ dataLength,
1033
+ ),
1034
+ };
1035
+ }
1036
+ }
1037
+ // pattern
1038
+ if (schema.pattern) {
1039
+ let regex;
1040
+ try {
1041
+ regex = new RegExp(schema.pattern, 'u');
1042
+ } catch {
1043
+ throw new InvalidArgumentError(
1044
+ 'Schema pattern %v at %v has an invalid expression.',
1045
+ schema.pattern,
1046
+ options.schemaSourceUri,
1047
+ );
1048
+ }
1049
+ if (!regex.test(data)) {
1050
+ const reString =
1051
+ schema.pattern instanceof RegExp
1052
+ ? String(schema.pattern)
1053
+ : schema.pattern;
1054
+ return {
1055
+ isValid: false,
1056
+ value: data,
1057
+ valueUri: options.dataSourceUri,
1058
+ schemaUri: options.schemaSourceUri,
1059
+ reason: format(
1060
+ 'String at %v must match pattern %v, but %s was given.',
1061
+ options.dataSourceUri,
1062
+ reString,
1063
+ toSpacedJson(data, {truncate: 50}),
1064
+ ),
1065
+ };
1066
+ }
1067
+ }
1068
+ return {
1069
+ isValid: true,
1070
+ value: data,
1071
+ valueUri: options.dataSourceUri,
1072
+ schemaUri: options.schemaSourceUri,
1073
+ };
1074
+ }
1075
+
1076
+ /**
1077
+ * Validate number/integer.
1078
+ *
1079
+ * @param {number} data
1080
+ * @param {object} schema
1081
+ * @param {object} options
1082
+ * @returns {object}
1083
+ */
1084
+ function _validateNumber(data, schema, options) {
1085
+ // minimum
1086
+ if (typeof schema.minimum === 'number') {
1087
+ if (data < schema.minimum) {
1088
+ return {
1089
+ isValid: false,
1090
+ value: data,
1091
+ valueUri: options.dataSourceUri,
1092
+ schemaUri: options.schemaSourceUri,
1093
+ reason: format(
1094
+ 'Value at %v must be greater than or equal to %d, but %v was given.',
1095
+ options.dataSourceUri,
1096
+ schema.minimum,
1097
+ data,
1098
+ ),
1099
+ };
1100
+ }
1101
+ }
1102
+ // maximum
1103
+ if (typeof schema.maximum === 'number') {
1104
+ if (data > schema.maximum) {
1105
+ return {
1106
+ isValid: false,
1107
+ value: data,
1108
+ valueUri: options.dataSourceUri,
1109
+ schemaUri: options.schemaSourceUri,
1110
+ reason: format(
1111
+ 'Value at %v must be less than or equal to %d, but %v was given.',
1112
+ options.dataSourceUri,
1113
+ schema.maximum,
1114
+ data,
1115
+ ),
1116
+ };
1117
+ }
1118
+ }
1119
+ // exclusiveMinimum
1120
+ if (typeof schema.exclusiveMinimum === 'number') {
1121
+ if (data <= schema.exclusiveMinimum) {
1122
+ return {
1123
+ isValid: false,
1124
+ value: data,
1125
+ valueUri: options.dataSourceUri,
1126
+ schemaUri: options.schemaSourceUri,
1127
+ reason: format(
1128
+ 'Value at %v must be greater than %d, but %v was given.',
1129
+ options.dataSourceUri,
1130
+ schema.exclusiveMinimum,
1131
+ data,
1132
+ ),
1133
+ };
1134
+ }
1135
+ }
1136
+ // exclusiveMaximum
1137
+ if (typeof schema.exclusiveMaximum === 'number') {
1138
+ if (data >= schema.exclusiveMaximum) {
1139
+ return {
1140
+ isValid: false,
1141
+ value: data,
1142
+ valueUri: options.dataSourceUri,
1143
+ schemaUri: options.schemaSourceUri,
1144
+ reason: format(
1145
+ 'Value at %v must be less than %d, but %v was given.',
1146
+ options.dataSourceUri,
1147
+ schema.exclusiveMaximum,
1148
+ data,
1149
+ ),
1150
+ };
1151
+ }
1152
+ }
1153
+ // multipleOf
1154
+ if (typeof schema.multipleOf === 'number') {
1155
+ // если "multipleOf" равен или меньше 0,
1156
+ // то выбрасывается ошибка
1157
+ if (schema.multipleOf <= 0) {
1158
+ throw new InvalidArgumentError(
1159
+ 'Schema keyword "multipleOf" must be a Number strictly ' +
1160
+ 'greater than 0, but %v was given.',
1161
+ schema.multipleOf,
1162
+ );
1163
+ }
1164
+ const quotient = data / schema.multipleOf;
1165
+ // EPSILON (1e-9 достаточно для большинства API задач)
1166
+ // используется для компенсации ошибок округления IEEE 754,
1167
+ // например, чтобы 0.3 считалось кратным 0.1, хотя в бинарном
1168
+ // виде 0.3 / 0.1 дает 2.9999999999999996
1169
+ const isInteger = Math.abs(quotient - Math.round(quotient)) < 1e-9;
1170
+ if (!isInteger) {
1171
+ return {
1172
+ isValid: false,
1173
+ value: data,
1174
+ valueUri: options.dataSourceUri,
1175
+ schemaUri: options.schemaSourceUri,
1176
+ reason: format(
1177
+ 'Value at %v must be a multiple of %v, but %v was given.',
1178
+ options.dataSourceUri,
1179
+ schema.multipleOf,
1180
+ data,
1181
+ ),
1182
+ };
1183
+ }
1184
+ }
1185
+ return {
1186
+ isValid: true,
1187
+ value: data,
1188
+ valueUri: options.dataSourceUri,
1189
+ schemaUri: options.schemaSourceUri,
1190
+ };
1191
+ }
1192
+
1193
+ /**
1194
+ * Validate array.
1195
+ *
1196
+ * @param {Array} data
1197
+ * @param {object} schema
1198
+ * @param {Set} evaluatedIndexes
1199
+ * @param {object} options
1200
+ * @returns {object}
1201
+ */
1202
+ function _validateArray(data, schema, evaluatedIndexes, options) {
1203
+ // чтобы избежать мутации набора проверенных
1204
+ // индексов, выполняется копирование набора
1205
+ evaluatedIndexes = new Set(evaluatedIndexes);
1206
+ // чтобы избежать проблем с производительностью,
1207
+ // копирование данных будет выполнено только в момент
1208
+ // изменения элементов массива
1209
+ const originalData = data;
1210
+ // если определен "prefixItems", то "items" проверяет
1211
+ // только хвост массива, по этой причине, необходимо
1212
+ // отслеживать последний проверенный элемент
1213
+ let validationStartIndex = 0;
1214
+ // prefixItems
1215
+ if (Array.isArray(schema.prefixItems)) {
1216
+ // при проверке кортежа, элементов может быть меньше,
1217
+ // чем схем в "prefixItems", это является допустимым
1218
+ // (проверка длины выполняется через minItems).
1219
+ const loopLimit = Math.min(schema.prefixItems.length, data.length);
1220
+ for (let i = 0; i < loopLimit; i++) {
1221
+ const item = data[i];
1222
+ const subSchema = schema.prefixItems[i];
1223
+ const result = _validate(item, subSchema, {
1224
+ ...options,
1225
+ dataSourceUri: `${options.dataSourceUri}/${i}`,
1226
+ schemaSourceUri: `${options.schemaSourceUri}/prefixItems/${i}`,
1227
+ });
1228
+ if (!result.isValid) {
1229
+ return result;
1230
+ }
1231
+ // если элемент массива изменился, то выполняется
1232
+ // обновление исходных данных без мутации оригинала
1233
+ // (поверхностное копирование массива)
1234
+ if (item !== result.value) {
1235
+ if (data === originalData) {
1236
+ data = [...originalData];
1237
+ }
1238
+ data[i] = result.value;
1239
+ }
1240
+ // если проверка элемента выполнена успешно,
1241
+ // то элемент помечается как проверенный
1242
+ evaluatedIndexes.add(i);
1243
+ }
1244
+ // свойство схемы "items" должно применяться
1245
+ // только к элементам после проверки "prefixItems"
1246
+ validationStartIndex = schema.prefixItems.length;
1247
+ }
1248
+ // items
1249
+ if (schema.items !== undefined) {
1250
+ // схема "items" проверяет только элементы,
1251
+ // выходящие за пределы длины "prefixItems"
1252
+ for (let i = validationStartIndex; i < data.length; i++) {
1253
+ const item = data[i];
1254
+ const result = _validate(item, schema.items, {
1255
+ ...options,
1256
+ dataSourceUri: `${options.dataSourceUri}/${i}`,
1257
+ schemaSourceUri: `${options.schemaSourceUri}/items`,
1258
+ });
1259
+ if (!result.isValid) {
1260
+ return result;
1261
+ }
1262
+ // если элемент массива изменился, то выполняется
1263
+ // обновление исходных данных без мутации оригинала
1264
+ // (поверхностное копирование массива)
1265
+ if (item !== result.value) {
1266
+ if (data === originalData) {
1267
+ data = [...originalData];
1268
+ }
1269
+ data[i] = result.value;
1270
+ }
1271
+ // если проверка элемента выполнена успешно,
1272
+ // то элемент помечается как проверенный
1273
+ evaluatedIndexes.add(i);
1274
+ }
1275
+ }
1276
+ // minItems
1277
+ if (typeof schema.minItems === 'number') {
1278
+ if (data.length < schema.minItems) {
1279
+ return {
1280
+ isValid: false,
1281
+ value: data,
1282
+ valueUri: options.dataSourceUri,
1283
+ schemaUri: options.schemaSourceUri,
1284
+ reason: format(
1285
+ 'Array at %v must contain at least %d items, but %d was given.',
1286
+ options.dataSourceUri,
1287
+ schema.minItems,
1288
+ data.length,
1289
+ ),
1290
+ };
1291
+ }
1292
+ }
1293
+ // maxItems
1294
+ if (typeof schema.maxItems === 'number') {
1295
+ if (data.length > schema.maxItems) {
1296
+ return {
1297
+ isValid: false,
1298
+ value: data,
1299
+ valueUri: options.dataSourceUri,
1300
+ schemaUri: options.schemaSourceUri,
1301
+ reason: format(
1302
+ 'Array at %v must contain at most %d items, but %d was given.',
1303
+ options.dataSourceUri,
1304
+ schema.maxItems,
1305
+ data.length,
1306
+ ),
1307
+ };
1308
+ }
1309
+ }
1310
+ // uniqueItems
1311
+ if (schema.uniqueItems === true) {
1312
+ // для проверки уникальности используется массив
1313
+ // уже просмотренных элементов и глубокое сравнение
1314
+ // для каждого нового элемента
1315
+ for (let i = 0; i < data.length; i++) {
1316
+ // алгоритм O(N^2) является приемлемым для проверки
1317
+ // конфигураций или запросов типичного размера
1318
+ for (let j = i + 1; j < data.length; j++) {
1319
+ if (_areValuesEqual(data[i], data[j])) {
1320
+ return {
1321
+ isValid: false,
1322
+ value: data,
1323
+ valueUri: options.dataSourceUri,
1324
+ schemaUri: options.schemaSourceUri,
1325
+ reason: format(
1326
+ 'Array at %v must contain unique items, ' +
1327
+ 'but items at index %d and %d are identical.',
1328
+ options.dataSourceUri,
1329
+ i,
1330
+ j,
1331
+ ),
1332
+ };
1333
+ }
1334
+ }
1335
+ }
1336
+ }
1337
+ // contains / minContains / maxContains
1338
+ if (schema.contains) {
1339
+ let matches = 0;
1340
+ for (let i = 0, l = data.length; i < l; i++) {
1341
+ const item = data[i];
1342
+ // проверка каждого элемента на соответствие схеме "contains",
1343
+ // используется опция "silent", чтобы не выбрасывать ошибку,
1344
+ // а только проверить факт соответствия
1345
+ const {isValid} = _validate(item, schema.contains, {
1346
+ ...options,
1347
+ silent: true,
1348
+ schemaSourceUri: options.schemaSourceUri + '/contains',
1349
+ });
1350
+ if (isValid) {
1351
+ matches++;
1352
+ // если проверка элемента выполнена успешно,
1353
+ // то элемент помечается как проверенный
1354
+ evaluatedIndexes.add(i);
1355
+ }
1356
+ }
1357
+ // minContains
1358
+ // если "minContains" не указан, но есть "contains", то по умолчанию
1359
+ // он равен 1, но если "minContains" явно равен 0, то проверка
1360
+ // на соответствие "хотя бы один" отключается
1361
+ const minContains =
1362
+ typeof schema.minContains === 'number' ? schema.minContains : 1;
1363
+ if (matches < minContains) {
1364
+ return {
1365
+ isValid: false,
1366
+ value: data,
1367
+ valueUri: options.dataSourceUri,
1368
+ schemaUri: options.schemaSourceUri + '/contains',
1369
+ reason: format(
1370
+ 'Array at %v must contain at least %d item(s) matching ' +
1371
+ 'the "contains" schema, but %d was found.',
1372
+ options.dataSourceUri,
1373
+ minContains,
1374
+ matches,
1375
+ ),
1376
+ };
1377
+ }
1378
+ // maxContains
1379
+ if (typeof schema.maxContains === 'number') {
1380
+ if (matches > schema.maxContains) {
1381
+ return {
1382
+ isValid: false,
1383
+ value: data,
1384
+ valueUri: options.dataSourceUri,
1385
+ schemaUri: options.schemaSourceUri + '/contains',
1386
+ reason: format(
1387
+ 'Array at %v must contain at most %d item(s) matching ' +
1388
+ 'the "contains" schema, but %d was found.',
1389
+ options.dataSourceUri,
1390
+ schema.maxContains,
1391
+ matches,
1392
+ ),
1393
+ };
1394
+ }
1395
+ }
1396
+ }
1397
+ // unevaluatedItems
1398
+ if (schema.unevaluatedItems !== undefined) {
1399
+ // ключевое слово применяется к элементам, которые еще не были
1400
+ // проверены ключевыми словами "prefixItems", "items", "contains"
1401
+ // или другими аппликаторами
1402
+ for (let i = 0, l = data.length; i < l; i++) {
1403
+ // если индекс уже есть в сете, значит данный
1404
+ // элемент массива известен и был проверен ранее
1405
+ if (evaluatedIndexes.has(i)) {
1406
+ continue;
1407
+ }
1408
+ const result = _validate(data[i], schema.unevaluatedItems, {
1409
+ ...options,
1410
+ dataSourceUri: `${options.dataSourceUri}/${i}`,
1411
+ schemaSourceUri: `${options.schemaSourceUri}/unevaluatedItems`,
1412
+ });
1413
+ if (!result.isValid) {
1414
+ // так как логическое значение false для данной опции
1415
+ // является наиболее частым случаем, то возвращается
1416
+ // особое сообщение
1417
+ if (schema.unevaluatedItems === false) {
1418
+ return {
1419
+ isValid: false,
1420
+ value: data[i],
1421
+ valueUri: `${options.dataSourceUri}/${i}`,
1422
+ schemaUri: `${options.schemaSourceUri}/unevaluatedItems`,
1423
+ reason: format(
1424
+ 'Array at %v has unevaluated item at index %d, ' +
1425
+ 'which is not allowed.',
1426
+ options.dataSourceUri,
1427
+ i,
1428
+ ),
1429
+ };
1430
+ }
1431
+ return result;
1432
+ }
1433
+ // если элемент массива изменился, то выполняется
1434
+ // обновление исходных данных без мутации оригинала
1435
+ // (поверхностное копирование массива)
1436
+ if (data[i] !== result.value) {
1437
+ if (data === originalData) {
1438
+ data = [...originalData];
1439
+ }
1440
+ data[i] = result.value;
1441
+ }
1442
+ // если проверка элемента выполнена успешно,
1443
+ // то элемент помечается как проверенный,
1444
+ // хотя на этом уровне это уже последний шаг
1445
+ evaluatedIndexes.add(i);
1446
+ }
1447
+ }
1448
+ // stripUnknown
1449
+ if (options.stripUnknown === true) {
1450
+ // если на данном этапе есть хотя бы один проверенный
1451
+ // индекс элемента, то это значит, что структура массива
1452
+ // определена схемой, и можно выполнить удаление
1453
+ // неизвестных элементов
1454
+ if (evaluatedIndexes.size > 0) {
1455
+ for (let i = 0, l = data.length; i < l; i++) {
1456
+ if (evaluatedIndexes.has(i)) {
1457
+ continue;
1458
+ }
1459
+ // чтобы избежать мутации исходных данных,
1460
+ // выполняется копирование оригинала
1461
+ if (data === originalData) {
1462
+ data = [...originalData];
1463
+ }
1464
+ data.splice(i, 1);
1465
+ }
1466
+ }
1467
+ }
1468
+ return {
1469
+ isValid: true,
1470
+ value: data,
1471
+ valueUri: options.dataSourceUri,
1472
+ schemaUri: options.schemaSourceUri,
1473
+ evaluatedIndexes,
1474
+ };
1475
+ }
1476
+
1477
+ /**
1478
+ * Validate object.
1479
+ *
1480
+ * @param {object} data
1481
+ * @param {object} schema
1482
+ * @param {Set} evaluatedProperties
1483
+ * @param {object} options
1484
+ * @returns {object}
1485
+ */
1486
+ function _validateObject(data, schema, evaluatedProperties, options) {
1487
+ // чтобы избежать мутации набора проверенных
1488
+ // свойств, выполняется копирование набора
1489
+ evaluatedProperties = new Set(evaluatedProperties);
1490
+ // чтобы избежать проблем с производительностью,
1491
+ // копирование данных будет выполнено только в момент
1492
+ // изменения элементов массива
1493
+ const originalData = data;
1494
+ // maxProperties
1495
+ if (typeof schema.maxProperties === 'number') {
1496
+ const propsNumber = Object.keys(data).length;
1497
+ if (propsNumber > schema.maxProperties) {
1498
+ return {
1499
+ isValid: false,
1500
+ value: data,
1501
+ valueUri: options.dataSourceUri,
1502
+ schemaUri: options.schemaSourceUri,
1503
+ reason: format(
1504
+ 'Object at %v must contain at most %d properties, but %d was given.',
1505
+ options.dataSourceUri,
1506
+ schema.maxProperties,
1507
+ propsNumber,
1508
+ ),
1509
+ };
1510
+ }
1511
+ }
1512
+ // minProperties
1513
+ if (typeof schema.minProperties === 'number') {
1514
+ const propsNumber = Object.keys(data).length;
1515
+ if (propsNumber < schema.minProperties) {
1516
+ return {
1517
+ isValid: false,
1518
+ value: data,
1519
+ valueUri: options.dataSourceUri,
1520
+ schemaUri: options.schemaSourceUri,
1521
+ reason: format(
1522
+ 'Object at %v must contain at least %d properties, but %d was given.',
1523
+ options.dataSourceUri,
1524
+ schema.minProperties,
1525
+ propsNumber,
1526
+ ),
1527
+ };
1528
+ }
1529
+ }
1530
+ // required
1531
+ if (Array.isArray(schema.required)) {
1532
+ for (const propName of schema.required) {
1533
+ if (data[propName] === undefined) {
1534
+ // прежде чем выбросить ошибку, что обязательное свойство
1535
+ // не определено, требуется проверить режим доступа, так
1536
+ // как свойство может быть исключено в данном режиме
1537
+ if (options.accessMode) {
1538
+ const propSchema =
1539
+ schema.properties !== null &&
1540
+ typeof schema.properties === 'object' &&
1541
+ schema.properties[propName] !== null &&
1542
+ typeof schema.properties[propName] === 'object' &&
1543
+ schema.properties[propName];
1544
+ // проверка на наличие исключающего ключевого
1545
+ // слова "readOnly" или "writeOnly" выполняется,
1546
+ // только если определена схема свойства
1547
+ if (propSchema) {
1548
+ if (
1549
+ (options.accessMode === OAAccessMode.WRITE &&
1550
+ propSchema.readOnly === true) ||
1551
+ (options.accessMode === OAAccessMode.READ &&
1552
+ propSchema.writeOnly === true)
1553
+ ) {
1554
+ continue;
1555
+ }
1556
+ }
1557
+ }
1558
+ const propValue = data[propName];
1559
+ return {
1560
+ isValid: false,
1561
+ value: propValue,
1562
+ valueUri: `${options.dataSourceUri}/${propName}`,
1563
+ schemaUri: options.schemaSourceUri,
1564
+ reason: format(
1565
+ 'Property %v at %v is required, but %v was given.',
1566
+ propName,
1567
+ options.dataSourceUri,
1568
+ propValue,
1569
+ ),
1570
+ };
1571
+ }
1572
+ }
1573
+ }
1574
+ // dependentRequired
1575
+ if (
1576
+ schema.dependentRequired &&
1577
+ typeof schema.dependentRequired === 'object' &&
1578
+ !Array.isArray(schema.dependentRequired)
1579
+ ) {
1580
+ // формат конфигурации {"propertyA": ["propertyB", "propertyC"]}
1581
+ // если есть "propertyA", то "propertyB" и "propertyC" тоже обязательны
1582
+ for (const triggerProp of Object.keys(schema.dependentRequired)) {
1583
+ const requiredProps = schema.dependentRequired[triggerProp];
1584
+ // если свойство-триггер присутствует в данных
1585
+ if (data[triggerProp] !== undefined) {
1586
+ if (Array.isArray(requiredProps)) {
1587
+ for (const requiredProp of requiredProps) {
1588
+ if (data[requiredProp] === undefined) {
1589
+ // прежде чем выбросить ошибку, что обязательное свойство
1590
+ // не определено, требуется проверить режим доступа, так
1591
+ // как свойство может быть исключено в данном режиме
1592
+ if (options.accessMode) {
1593
+ const requiredPropSchema =
1594
+ schema.properties !== null &&
1595
+ typeof schema.properties === 'object' &&
1596
+ schema.properties[requiredProp] !== null &&
1597
+ typeof schema.properties[requiredProp] === 'object' &&
1598
+ schema.properties[requiredProp];
1599
+ // проверка на наличие исключающего ключевого
1600
+ // слова "readOnly" или "writeOnly" выполняется,
1601
+ // только если определена схема обязательного
1602
+ // свойства
1603
+ if (requiredPropSchema) {
1604
+ if (
1605
+ (options.accessMode === OAAccessMode.WRITE &&
1606
+ requiredPropSchema.readOnly === true) ||
1607
+ (options.accessMode === OAAccessMode.READ &&
1608
+ requiredPropSchema.writeOnly === true)
1609
+ ) {
1610
+ continue;
1611
+ }
1612
+ }
1613
+ }
1614
+ return {
1615
+ isValid: false,
1616
+ value: data[requiredProp],
1617
+ valueUri: `${options.dataSourceUri}/${requiredProp}`,
1618
+ schemaUri: options.schemaSourceUri,
1619
+ reason: format(
1620
+ 'Property %v at %v is required because ' +
1621
+ 'the property %v is present.',
1622
+ requiredProp,
1623
+ options.dataSourceUri,
1624
+ triggerProp,
1625
+ ),
1626
+ };
1627
+ }
1628
+ }
1629
+ }
1630
+ }
1631
+ }
1632
+ }
1633
+ // dependentSchemas
1634
+ if (
1635
+ schema.dependentSchemas &&
1636
+ typeof schema.dependentSchemas === 'object' &&
1637
+ !Array.isArray(schema.dependentSchemas)
1638
+ ) {
1639
+ for (const triggerProp of Object.keys(schema.dependentSchemas)) {
1640
+ const subSchema = schema.dependentSchemas[triggerProp];
1641
+ if (data[triggerProp] !== undefined) {
1642
+ const result = _validate(data, subSchema, {
1643
+ ...options,
1644
+ schemaSourceUri: `${options.schemaSourceUri}/dependentSchemas/${triggerProp}`,
1645
+ });
1646
+ if (!result.isValid) {
1647
+ return result;
1648
+ }
1649
+ // данные могли измениться во время проверки,
1650
+ // выполняется обновление исходного значения
1651
+ data = result.value;
1652
+ // внедрение аннотаций о проверенных свойствах
1653
+ // из результата успешной проверки
1654
+ if (result.evaluatedProperties) {
1655
+ evaluatedProperties = new Set([
1656
+ ...evaluatedProperties,
1657
+ ...result.evaluatedProperties,
1658
+ ]);
1659
+ }
1660
+ }
1661
+ }
1662
+ }
1663
+ // propertyNames
1664
+ if (
1665
+ schema.propertyNames &&
1666
+ typeof schema.propertyNames === 'object' &&
1667
+ !Array.isArray(schema.propertyNames)
1668
+ ) {
1669
+ for (const propName of Object.keys(data)) {
1670
+ const result = _validate(propName, schema.propertyNames, {
1671
+ ...options,
1672
+ dataSourceUri: `${options.dataSourceUri}`,
1673
+ schemaSourceUri: `${options.schemaSourceUri}/propertyNames`,
1674
+ });
1675
+ if (!result.isValid) {
1676
+ return result;
1677
+ }
1678
+ }
1679
+ }
1680
+ // properties
1681
+ if (schema.properties && typeof schema.properties === 'object') {
1682
+ for (const propName of Object.keys(schema.properties)) {
1683
+ const propSchema = schema.properties[propName];
1684
+ // перед началом проверки свойства, требуется
1685
+ // определить его наличие и необходимость
1686
+ // установки значения по умолчанию
1687
+ const isPropValuePresent = data[propName] !== undefined;
1688
+ const shouldApplyDefault =
1689
+ options.applyDefaults === true &&
1690
+ propSchema !== null &&
1691
+ typeof propSchema === 'object' &&
1692
+ propSchema.default !== undefined;
1693
+ // проверка свойства выполняется только если
1694
+ // свойство присутствует в данных, либо ожидается
1695
+ // установка значения по умолчанию
1696
+ if (isPropValuePresent || shouldApplyDefault) {
1697
+ const propValue = data[propName];
1698
+ const result = _validate(propValue, propSchema, {
1699
+ ...options,
1700
+ dataSourceUri: `${options.dataSourceUri}/${propName}`,
1701
+ schemaSourceUri: `${options.schemaSourceUri}/properties/${propName}`,
1702
+ });
1703
+ if (!result.isValid) {
1704
+ return result;
1705
+ }
1706
+ // если значение свойства изменилось, то выполняется
1707
+ // обновление исходных данных без мутации оригинала
1708
+ // (поверхностное копирование объекта)
1709
+ if (data[propName] !== result.value) {
1710
+ if (data === originalData) {
1711
+ data = {...originalData};
1712
+ }
1713
+ data[propName] = result.value;
1714
+ }
1715
+ // если проверка свойства прошла успешно,
1716
+ // то свойство помечается как проверенное
1717
+ evaluatedProperties.add(propName);
1718
+ }
1719
+ }
1720
+ }
1721
+ // patternProperties
1722
+ const passedPatternProperties = [];
1723
+ if (
1724
+ schema.patternProperties &&
1725
+ typeof schema.patternProperties === 'object'
1726
+ ) {
1727
+ // проверка каждого имени свойств объекта
1728
+ // на соответствие регулярным выражениям
1729
+ for (const pattern of Object.keys(schema.patternProperties)) {
1730
+ const patternSchema = schema.patternProperties[pattern];
1731
+ let regex;
1732
+ try {
1733
+ regex = new RegExp(pattern, 'u');
1734
+ } catch {
1735
+ throw new InvalidArgumentError(
1736
+ 'Property pattern %v at %v has an invalid expression.',
1737
+ pattern,
1738
+ options.schemaSourceUri,
1739
+ );
1740
+ }
1741
+ for (const propName of Object.keys(data)) {
1742
+ if (regex.test(propName)) {
1743
+ const propValue = data[propName];
1744
+ const result = _validate(propValue, patternSchema, {
1745
+ ...options,
1746
+ dataSourceUri: `${options.dataSourceUri}/${propName}`,
1747
+ schemaSourceUri: `${options.schemaSourceUri}/patternProperties/${pattern}`,
1748
+ });
1749
+ if (!result.isValid) {
1750
+ return result;
1751
+ }
1752
+ // если значение свойства изменилось, то выполняется
1753
+ // обновление исходных данных без мутации оригинала
1754
+ // (поверхностное копирование объекта)
1755
+ if (data[propName] !== result.value) {
1756
+ if (data === originalData) {
1757
+ data = {...originalData};
1758
+ }
1759
+ data[propName] = result.value;
1760
+ }
1761
+ // если проверка свойства прошла успешно,
1762
+ // то свойство помечается как проверенное
1763
+ evaluatedProperties.add(propName);
1764
+ // добавление проверенного свойства
1765
+ // в массив проверенных по шаблону
1766
+ passedPatternProperties.push(propName);
1767
+ }
1768
+ }
1769
+ }
1770
+ }
1771
+ // additionalProperties
1772
+ if (schema.additionalProperties !== undefined) {
1773
+ const definedProps = schema.properties
1774
+ ? Object.keys(schema.properties)
1775
+ : [];
1776
+ for (const propName of Object.keys(data)) {
1777
+ // если свойство определено в "properties",
1778
+ // то свойство не является дополнительным
1779
+ if (definedProps.includes(propName)) {
1780
+ continue;
1781
+ }
1782
+ // если свойство обработано "patternProperties",
1783
+ // то такое свойство не является дополнительным
1784
+ if (passedPatternProperties.includes(propName)) {
1785
+ continue;
1786
+ }
1787
+ // если значение параметра "additionalProperties" определено
1788
+ // как false, то любые неизвестные свойства вызывают ошибку
1789
+ if (schema.additionalProperties === false) {
1790
+ return {
1791
+ isValid: false,
1792
+ value: data[propName],
1793
+ valueUri: `${options.dataSourceUri}/${propName}`,
1794
+ schemaUri: `${options.schemaSourceUri}/additionalProperties`,
1795
+ reason: format(
1796
+ 'Property %v at %v is not allowed.',
1797
+ propName,
1798
+ options.dataSourceUri,
1799
+ ),
1800
+ };
1801
+ }
1802
+ // если значение параметра "additionalProperties" является
1803
+ // схемой, то проверяется значение неизвестного свойства
1804
+ if (
1805
+ schema.additionalProperties !== null &&
1806
+ typeof schema.additionalProperties === 'object' &&
1807
+ !Array.isArray(schema.additionalProperties)
1808
+ ) {
1809
+ const propValue = data[propName];
1810
+ const result = _validate(propValue, schema.additionalProperties, {
1811
+ ...options,
1812
+ dataSourceUri: `${options.dataSourceUri}/${propName}`,
1813
+ schemaSourceUri: `${options.schemaSourceUri}/additionalProperties`,
1814
+ });
1815
+ if (!result.isValid) {
1816
+ return result;
1817
+ }
1818
+ // если значение свойства изменилось, то выполняется
1819
+ // обновление исходных данных без мутации оригинала
1820
+ // (поверхностное копирование объекта)
1821
+ if (data[propName] !== result.value) {
1822
+ if (data === originalData) {
1823
+ data = {...originalData};
1824
+ }
1825
+ data[propName] = result.value;
1826
+ }
1827
+ }
1828
+ // если проверка свойства прошла успешно,
1829
+ // то свойство помечается как проверенное
1830
+ evaluatedProperties.add(propName);
1831
+ }
1832
+ }
1833
+ // unevaluatedProperties
1834
+ if (schema.unevaluatedProperties !== undefined) {
1835
+ // проход по всем ключам объекта для обработки
1836
+ // незатронутых свойств
1837
+ for (const propName of Object.keys(data)) {
1838
+ // если свойство было проверено ранее,
1839
+ // то свойство пропускается
1840
+ if (evaluatedProperties.has(propName)) {
1841
+ continue;
1842
+ }
1843
+ // если свойство не было проверено,
1844
+ // применяется схема "unevaluatedProperties"
1845
+ if (schema.unevaluatedProperties === false) {
1846
+ return {
1847
+ isValid: false,
1848
+ value: data[propName],
1849
+ valueUri: `${options.dataSourceUri}/${propName}`,
1850
+ schemaUri: `${options.schemaSourceUri}/unevaluatedProperties`,
1851
+ reason: format(
1852
+ 'Property %v at %v is not evaluated and not allowed.',
1853
+ propName,
1854
+ options.dataSourceUri,
1855
+ ),
1856
+ };
1857
+ } else if (
1858
+ schema.unevaluatedProperties &&
1859
+ typeof schema.unevaluatedProperties === 'object' &&
1860
+ !Array.isArray(schema.unevaluatedProperties)
1861
+ ) {
1862
+ const propValue = data[propName];
1863
+ const result = _validate(propValue, schema.unevaluatedProperties, {
1864
+ ...options,
1865
+ dataSourceUri: `${options.dataSourceUri}/${propName}`,
1866
+ schemaSourceUri: `${options.schemaSourceUri}/unevaluatedProperties`,
1867
+ });
1868
+ if (!result.isValid) {
1869
+ return result;
1870
+ }
1871
+ // если значение свойства изменилось, то выполняется
1872
+ // обновление исходных данных без мутации оригинала
1873
+ // (поверхностное копирование объекта)
1874
+ if (data[propName] !== result.value) {
1875
+ if (data === originalData) {
1876
+ data = {...originalData};
1877
+ }
1878
+ data[propName] = result.value;
1879
+ }
1880
+ // если проверка свойства прошла успешно,
1881
+ // то свойство помечается как проверенное
1882
+ evaluatedProperties.add(propName);
1883
+ }
1884
+ }
1885
+ }
1886
+ // stripUnknown
1887
+ if (options.stripUnknown === true) {
1888
+ // если на данном этапе есть хотя бы одно проверенное
1889
+ // свойство, то это значит, что структура объекта
1890
+ // определена схемой, и можно выполнить удаление
1891
+ // неизвестных свойств
1892
+ if (evaluatedProperties.size > 0) {
1893
+ for (const propName of Object.keys(data)) {
1894
+ if (evaluatedProperties.has(propName)) {
1895
+ continue;
1896
+ }
1897
+ // чтобы избежать мутации исходных данных,
1898
+ // выполняется копирование оригинала
1899
+ if (data === originalData) {
1900
+ data = {...originalData};
1901
+ }
1902
+ delete data[propName];
1903
+ }
1904
+ }
1905
+ }
1906
+ return {
1907
+ isValid: true,
1908
+ value: data,
1909
+ valueUri: options.dataSourceUri,
1910
+ schemaUri: options.schemaSourceUri,
1911
+ evaluatedProperties,
1912
+ };
1913
+ }