schema-shield 0.0.6 → 1.0.1

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 (39) hide show
  1. package/README.md +219 -65
  2. package/dist/formats.d.ts.map +1 -1
  3. package/dist/index.d.ts +25 -6
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1837 -484
  6. package/dist/index.min.js +1 -1
  7. package/dist/index.min.js.map +1 -1
  8. package/dist/index.mjs +1837 -484
  9. package/dist/keywords/array-keywords.d.ts.map +1 -1
  10. package/dist/keywords/object-keywords.d.ts.map +1 -1
  11. package/dist/keywords/other-keywords.d.ts.map +1 -1
  12. package/dist/keywords/string-keywords.d.ts.map +1 -1
  13. package/dist/types.d.ts.map +1 -1
  14. package/dist/utils/deep-freeze.d.ts +5 -0
  15. package/dist/utils/deep-freeze.d.ts.map +1 -0
  16. package/dist/utils/has-changed.d.ts +2 -0
  17. package/dist/utils/has-changed.d.ts.map +1 -0
  18. package/dist/utils/index.d.ts +5 -0
  19. package/dist/utils/index.d.ts.map +1 -0
  20. package/dist/{utils.d.ts → utils/main-utils.d.ts} +7 -9
  21. package/dist/utils/main-utils.d.ts.map +1 -0
  22. package/dist/utils/pattern-matcher.d.ts +3 -0
  23. package/dist/utils/pattern-matcher.d.ts.map +1 -0
  24. package/lib/formats.ts +468 -155
  25. package/lib/index.ts +702 -107
  26. package/lib/keywords/array-keywords.ts +260 -52
  27. package/lib/keywords/number-keywords.ts +1 -1
  28. package/lib/keywords/object-keywords.ts +295 -88
  29. package/lib/keywords/other-keywords.ts +263 -70
  30. package/lib/keywords/string-keywords.ts +123 -7
  31. package/lib/types.ts +5 -18
  32. package/lib/utils/deep-freeze.ts +208 -0
  33. package/lib/utils/has-changed.ts +51 -0
  34. package/lib/utils/index.ts +4 -0
  35. package/lib/{utils.ts → utils/main-utils.ts} +63 -77
  36. package/lib/utils/pattern-matcher.ts +66 -0
  37. package/package.json +2 -2
  38. package/tsconfig.json +4 -4
  39. package/dist/utils.d.ts.map +0 -1
@@ -1,27 +1,96 @@
1
- import { deepEqual, isCompiledSchema, isObject } from "../utils";
1
+ import { isCompiledSchema } from "../utils/main-utils";
2
2
 
3
3
  import { KeywordFunction } from "../index";
4
+ import { hasChanged } from "../utils/has-changed";
5
+
6
+ type BranchEntry =
7
+ | { kind: "validate"; validate: (data: any) => any }
8
+ | { kind: "alwaysValid" }
9
+ | { kind: "alwaysInvalid" }
10
+ | { kind: "literal"; value: any };
11
+
12
+ function toBranchEntry(item: any): BranchEntry {
13
+ if (item && typeof item === "object" && !Array.isArray(item)) {
14
+ if ("$validate" in item && typeof item.$validate === "function") {
15
+ return { kind: "validate", validate: item.$validate };
16
+ }
17
+
18
+ return { kind: "alwaysValid" };
19
+ }
20
+
21
+ if (typeof item === "boolean") {
22
+ return { kind: item ? "alwaysValid" : "alwaysInvalid" };
23
+ }
24
+
25
+ return { kind: "literal", value: item };
26
+ }
27
+
28
+ function getBranchEntries(schema: any, key: "allOf" | "anyOf" | "oneOf") {
29
+ const cacheKey = `_${key}BranchEntries`;
30
+ let entries = schema[cacheKey] as BranchEntry[] | undefined;
31
+
32
+ if (entries) {
33
+ return entries;
34
+ }
35
+
36
+ const source = schema[key] || [];
37
+ entries = [];
38
+
39
+ for (let i = 0; i < source.length; i++) {
40
+ entries.push(toBranchEntry(source[i]));
41
+ }
42
+
43
+ Object.defineProperty(schema, cacheKey, {
44
+ value: entries,
45
+ enumerable: false,
46
+ configurable: false,
47
+ writable: false
48
+ });
49
+
50
+ return entries;
51
+ }
4
52
 
5
53
  export const OtherKeywords: Record<string, KeywordFunction> = {
6
54
  enum(schema, data, defineError) {
7
- // Check if data is an array or an object
8
- const isArray = Array.isArray(data);
9
- const isObject = typeof data === "object" && data !== null;
55
+ let enumCache = (schema as any)._enumCache as
56
+ | { primitiveSet: Set<any>; objectValues: any[] }
57
+ | undefined;
10
58
 
11
- for (let i = 0; i < schema.enum.length; i++) {
12
- const enumItem = schema.enum[i];
59
+ if (!enumCache) {
60
+ const primitiveSet = new Set<any>();
61
+ const objectValues: any[] = [];
62
+ const list = schema.enum;
13
63
 
14
- // Simple equality check
15
- if (enumItem === data) {
16
- return;
64
+ for (let i = 0; i < list.length; i++) {
65
+ const enumItem = list[i];
66
+ if (enumItem !== null && typeof enumItem === "object") {
67
+ objectValues.push(enumItem);
68
+ } else {
69
+ primitiveSet.add(enumItem);
70
+ }
17
71
  }
18
72
 
19
- // If data is an array or an object, check for deep equality
20
- if (
21
- (isArray && Array.isArray(enumItem)) ||
22
- (isObject && typeof enumItem === "object" && enumItem !== null)
23
- ) {
24
- if (deepEqual(enumItem, data)) {
73
+ enumCache = { primitiveSet, objectValues };
74
+ Object.defineProperty(schema, "_enumCache", {
75
+ value: enumCache,
76
+ enumerable: false,
77
+ configurable: false,
78
+ writable: false
79
+ });
80
+ }
81
+
82
+ if (
83
+ !(typeof data === "number" && Number.isNaN(data)) &&
84
+ enumCache.primitiveSet.has(data)
85
+ ) {
86
+ return;
87
+ }
88
+
89
+ if (data !== null && typeof data === "object") {
90
+ // Conservative exact-semantics path.
91
+ // Future opt-in optimization: structural hashing buckets for large enums.
92
+ for (let i = 0; i < enumCache.objectValues.length; i++) {
93
+ if (!hasChanged(enumCache.objectValues[i], data)) {
25
94
  return;
26
95
  }
27
96
  }
@@ -31,25 +100,54 @@ export const OtherKeywords: Record<string, KeywordFunction> = {
31
100
  },
32
101
 
33
102
  allOf(schema, data, defineError) {
34
- for (let i = 0; i < schema.allOf.length; i++) {
35
- if (isObject(schema.allOf[i])) {
36
- if ("$validate" in schema.allOf[i]) {
37
- const error = schema.allOf[i].$validate(data);
38
- if (error) {
39
- return defineError("Value is not valid", { cause: error, data });
40
- }
103
+ const branches = getBranchEntries(schema, "allOf");
104
+
105
+ if (branches.length === 1) {
106
+ const onlyBranch = branches[0];
107
+
108
+ if (onlyBranch.kind === "validate") {
109
+ const error = onlyBranch.validate(data);
110
+ if (error) {
111
+ return defineError("Value is not valid", { cause: error, data });
41
112
  }
42
- continue;
113
+ return;
43
114
  }
44
115
 
45
- if (typeof schema.allOf[i] === "boolean") {
46
- if (Boolean(data) !== schema.allOf[i]) {
47
- return defineError("Value is not valid", { data });
116
+ if (onlyBranch.kind === "alwaysValid") {
117
+ return;
118
+ }
119
+
120
+ if (onlyBranch.kind === "alwaysInvalid") {
121
+ return defineError("Value is not valid", { data });
122
+ }
123
+
124
+ if (data !== onlyBranch.value) {
125
+ return defineError("Value is not valid", { data });
126
+ }
127
+
128
+ return;
129
+ }
130
+
131
+ for (let i = 0; i < branches.length; i++) {
132
+ const branch = branches[i];
133
+
134
+ if (branch.kind === "validate") {
135
+ const error = branch.validate(data);
136
+ if (error) {
137
+ return defineError("Value is not valid", { cause: error, data });
48
138
  }
49
139
  continue;
50
140
  }
51
141
 
52
- if (data !== schema.allOf[i]) {
142
+ if (branch.kind === "alwaysValid") {
143
+ continue;
144
+ }
145
+
146
+ if (branch.kind === "alwaysInvalid") {
147
+ return defineError("Value is not valid", { data });
148
+ }
149
+
150
+ if (data !== branch.value) {
53
151
  return defineError("Value is not valid", { data });
54
152
  }
55
153
  }
@@ -58,26 +156,55 @@ export const OtherKeywords: Record<string, KeywordFunction> = {
58
156
  },
59
157
 
60
158
  anyOf(schema, data, defineError) {
61
- for (let i = 0; i < schema.anyOf.length; i++) {
62
- if (isObject(schema.anyOf[i])) {
63
- if ("$validate" in schema.anyOf[i]) {
64
- const error = schema.anyOf[i].$validate(data);
65
- if (!error) {
66
- return;
67
- }
68
- continue;
159
+ const branches = getBranchEntries(schema, "anyOf");
160
+
161
+ if (branches.length === 1) {
162
+ const onlyBranch = branches[0];
163
+
164
+ if (onlyBranch.kind === "validate") {
165
+ const error = onlyBranch.validate(data);
166
+ if (!error) {
167
+ return;
69
168
  }
169
+ return defineError("Value is not valid", { data });
170
+ }
171
+
172
+ if (onlyBranch.kind === "alwaysValid") {
70
173
  return;
71
- } else {
72
- if (typeof schema.anyOf[i] === "boolean") {
73
- if (Boolean(data) === schema.anyOf[i]) {
74
- return;
75
- }
76
- }
174
+ }
175
+
176
+ if (onlyBranch.kind === "alwaysInvalid") {
177
+ return defineError("Value is not valid", { data });
178
+ }
179
+
180
+ if (data === onlyBranch.value) {
181
+ return;
182
+ }
183
+
184
+ return defineError("Value is not valid", { data });
185
+ }
186
+
187
+ for (let i = 0; i < branches.length; i++) {
188
+ const branch = branches[i];
77
189
 
78
- if (data === schema.anyOf[i]) {
190
+ if (branch.kind === "validate") {
191
+ const error = branch.validate(data);
192
+ if (!error) {
79
193
  return;
80
194
  }
195
+ continue;
196
+ }
197
+
198
+ if (branch.kind === "alwaysValid") {
199
+ return;
200
+ }
201
+
202
+ if (branch.kind === "alwaysInvalid") {
203
+ continue;
204
+ }
205
+
206
+ if (data === branch.value) {
207
+ return;
81
208
  }
82
209
  }
83
210
 
@@ -85,28 +212,54 @@ export const OtherKeywords: Record<string, KeywordFunction> = {
85
212
  },
86
213
 
87
214
  oneOf(schema, data, defineError) {
88
- let validCount = 0;
89
- for (let i = 0; i < schema.oneOf.length; i++) {
90
- if (isObject(schema.oneOf[i])) {
91
- if ("$validate" in schema.oneOf[i]) {
92
- const error = schema.oneOf[i].$validate(data);
93
- if (!error) {
94
- validCount++;
95
- }
96
- continue;
215
+ const branches = getBranchEntries(schema, "oneOf");
216
+
217
+ if (branches.length === 1) {
218
+ const onlyBranch = branches[0];
219
+
220
+ if (onlyBranch.kind === "validate") {
221
+ const error = onlyBranch.validate(data);
222
+ if (!error) {
223
+ return;
97
224
  }
98
- validCount++;
99
- continue;
225
+ return defineError("Value is not valid", { data });
226
+ }
227
+
228
+ if (onlyBranch.kind === "alwaysValid") {
229
+ return;
230
+ }
231
+
232
+ if (onlyBranch.kind === "alwaysInvalid") {
233
+ return defineError("Value is not valid", { data });
234
+ }
235
+
236
+ if (data === onlyBranch.value) {
237
+ return;
238
+ }
239
+
240
+ return defineError("Value is not valid", { data });
241
+ }
242
+
243
+ let validCount = 0;
244
+
245
+ for (let i = 0; i < branches.length; i++) {
246
+ const branch = branches[i];
247
+ let isValid = false;
248
+
249
+ if (branch.kind === "validate") {
250
+ isValid = !branch.validate(data);
251
+ } else if (branch.kind === "alwaysValid") {
252
+ isValid = true;
253
+ } else if (branch.kind === "alwaysInvalid") {
254
+ isValid = false;
100
255
  } else {
101
- if (typeof schema.oneOf[i] === "boolean") {
102
- if (Boolean(data) === schema.oneOf[i]) {
103
- validCount++;
104
- }
105
- continue;
106
- }
256
+ isValid = data === branch.value;
257
+ }
107
258
 
108
- if (data === schema.oneOf[i]) {
109
- validCount++;
259
+ if (isValid) {
260
+ validCount++;
261
+ if (validCount > 1) {
262
+ return defineError("Value is not valid", { data });
110
263
  }
111
264
  }
112
265
  }
@@ -119,21 +272,28 @@ export const OtherKeywords: Record<string, KeywordFunction> = {
119
272
  },
120
273
 
121
274
  const(schema, data, defineError) {
275
+ if (data === schema.const) {
276
+ return;
277
+ }
278
+
122
279
  if (
123
- data === schema.const ||
124
- (isObject(data) &&
125
- isObject(schema.const) &&
126
- deepEqual(data, schema.const)) ||
280
+ (data &&
281
+ typeof data === "object" &&
282
+ !Array.isArray(data) &&
283
+ schema.const &&
284
+ typeof schema.const === "object" &&
285
+ !Array.isArray(schema.const) &&
286
+ !hasChanged(data, schema.const)) ||
127
287
  (Array.isArray(data) &&
128
288
  Array.isArray(schema.const) &&
129
- deepEqual(data, schema.const))
289
+ !hasChanged(data, schema.const))
130
290
  ) {
131
291
  return;
132
292
  }
133
293
  return defineError("Value is not valid", { data });
134
294
  },
135
295
 
136
- if(schema, data, defineError) {
296
+ if(schema, data) {
137
297
  if ("then" in schema === false && "else" in schema === false) {
138
298
  return;
139
299
  }
@@ -174,11 +334,15 @@ export const OtherKeywords: Record<string, KeywordFunction> = {
174
334
  return;
175
335
  }
176
336
 
177
- if (isObject(schema.not)) {
337
+ if (
338
+ schema.not &&
339
+ typeof schema.not === "object" &&
340
+ !Array.isArray(schema.not)
341
+ ) {
178
342
  if ("$validate" in schema.not) {
179
- const error = schema.not.$validate(data);
343
+ const error = (schema.not as any).$validate(data);
180
344
  if (!error) {
181
- return defineError("Value is not valid", { cause: error, data });
345
+ return defineError("Value is not valid", { data });
182
346
  }
183
347
  return;
184
348
  }
@@ -186,5 +350,34 @@ export const OtherKeywords: Record<string, KeywordFunction> = {
186
350
  }
187
351
 
188
352
  return defineError("Value is not valid", { data });
353
+ },
354
+
355
+ $ref(schema, data, defineError, instance) {
356
+ if (schema._resolvedRef) {
357
+ if (schema.$validate !== schema._resolvedRef) {
358
+ schema.$validate = schema._resolvedRef;
359
+ }
360
+
361
+ return schema._resolvedRef(data);
362
+ }
363
+
364
+ const refPath = schema.$ref;
365
+ let targetSchema = instance.getSchemaRef(refPath);
366
+
367
+ if (!targetSchema) {
368
+ targetSchema = instance.getSchemaById(refPath);
369
+ }
370
+
371
+ if (!targetSchema) {
372
+ return defineError(`Missing reference: ${refPath}`);
373
+ }
374
+
375
+ if (!targetSchema.$validate) {
376
+ return;
377
+ }
378
+
379
+ schema._resolvedRef = targetSchema.$validate;
380
+ schema.$validate = schema._resolvedRef;
381
+ return schema._resolvedRef(data);
189
382
  }
190
383
  };
@@ -1,4 +1,8 @@
1
- import { KeywordFunction } from "../index";
1
+ import { FormatFunction, KeywordFunction } from "../index";
2
+ import { compilePatternMatcher } from "../utils/pattern-matcher";
3
+
4
+ const PATTERN_MATCH_CACHE_LIMIT = 512;
5
+ const FORMAT_RESULT_CACHE_LIMIT = 512;
2
6
 
3
7
  export const StringKeywords: Record<string, KeywordFunction> = {
4
8
  minLength(schema, data, defineError) {
@@ -22,13 +26,58 @@ export const StringKeywords: Record<string, KeywordFunction> = {
22
26
  return;
23
27
  }
24
28
 
25
- const patternRegexp = new RegExp(schema.pattern, "u");
29
+ let patternMatch = (schema as any)._patternMatch as
30
+ | ((value: string) => boolean)
31
+ | undefined;
32
+
33
+ let patternMatchCache = (schema as any)._patternMatchCache as
34
+ | Map<string, boolean>
35
+ | undefined;
26
36
 
27
- if (patternRegexp instanceof RegExp === false) {
28
- return defineError("Invalid regular expression", { data });
37
+ if (!patternMatch) {
38
+ try {
39
+ const compiled = compilePatternMatcher(schema.pattern);
40
+ patternMatch =
41
+ compiled instanceof RegExp
42
+ ? (value: string) => compiled.test(value)
43
+ : compiled;
44
+
45
+ Object.defineProperty(schema, "_patternMatch", {
46
+ value: patternMatch,
47
+ enumerable: false,
48
+ configurable: false,
49
+ writable: false
50
+ });
51
+ } catch (error) {
52
+ return defineError("Invalid regular expression", {
53
+ data,
54
+ cause: error
55
+ });
56
+ }
29
57
  }
30
58
 
31
- if (patternRegexp.test(data)) {
59
+ if (!patternMatchCache) {
60
+ patternMatchCache = new Map<string, boolean>();
61
+ Object.defineProperty(schema, "_patternMatchCache", {
62
+ value: patternMatchCache,
63
+ enumerable: false,
64
+ configurable: false,
65
+ writable: false
66
+ });
67
+ } else if (patternMatchCache.has(data)) {
68
+ if (patternMatchCache.get(data)) {
69
+ return;
70
+ }
71
+
72
+ return defineError("Value does not match the pattern", { data });
73
+ }
74
+
75
+ const isMatch = patternMatch(data);
76
+ if (patternMatchCache.size < PATTERN_MATCH_CACHE_LIMIT) {
77
+ patternMatchCache.set(data, isMatch);
78
+ }
79
+
80
+ if (isMatch) {
32
81
  return;
33
82
  }
34
83
 
@@ -42,8 +91,75 @@ export const StringKeywords: Record<string, KeywordFunction> = {
42
91
  return;
43
92
  }
44
93
 
45
- const formatValidate = instance.getFormat(schema.format);
46
- if (!formatValidate || formatValidate(data)) {
94
+ let formatValidate = (schema as any)._formatValidate as
95
+ | FormatFunction
96
+ | false
97
+ | undefined;
98
+ let formatResultCacheEnabled = (schema as any)._formatResultCacheEnabled as
99
+ | boolean
100
+ | undefined;
101
+ let formatResultCache = (schema as any)._formatResultCache as
102
+ | Map<string, boolean>
103
+ | undefined;
104
+
105
+ if (formatValidate === undefined) {
106
+ formatValidate = instance.getFormat(schema.format);
107
+ Object.defineProperty(schema, "_formatValidate", {
108
+ value: formatValidate,
109
+ enumerable: false,
110
+ configurable: false,
111
+ writable: false
112
+ });
113
+ }
114
+
115
+ if (!formatValidate) {
116
+ return;
117
+ }
118
+
119
+ if (formatResultCacheEnabled === undefined) {
120
+ formatResultCacheEnabled = instance.isDefaultFormatValidator(
121
+ schema.format,
122
+ formatValidate
123
+ );
124
+
125
+ Object.defineProperty(schema, "_formatResultCacheEnabled", {
126
+ value: formatResultCacheEnabled,
127
+ enumerable: false,
128
+ configurable: false,
129
+ writable: false
130
+ });
131
+ }
132
+
133
+ if (!formatResultCacheEnabled) {
134
+ if (formatValidate(data)) {
135
+ return;
136
+ }
137
+
138
+ return defineError("Value does not match the format", { data });
139
+ }
140
+
141
+ if (!formatResultCache) {
142
+ formatResultCache = new Map<string, boolean>();
143
+ Object.defineProperty(schema, "_formatResultCache", {
144
+ value: formatResultCache,
145
+ enumerable: false,
146
+ configurable: false,
147
+ writable: false
148
+ });
149
+ } else if (formatResultCache.has(data)) {
150
+ if (formatResultCache.get(data)) {
151
+ return;
152
+ }
153
+
154
+ return defineError("Value does not match the format", { data });
155
+ }
156
+
157
+ const isValid = formatValidate(data);
158
+ if (formatResultCache.size < FORMAT_RESULT_CACHE_LIMIT) {
159
+ formatResultCache.set(data, isValid);
160
+ }
161
+
162
+ if (isValid) {
47
163
  return;
48
164
  }
49
165
 
package/lib/types.ts CHANGED
@@ -1,22 +1,11 @@
1
1
  import { TypeFunction } from "./index";
2
- import { isObject } from "./utils";
3
2
 
4
3
  export const Types: Record<string, TypeFunction | false> = {
5
4
  object(data) {
6
- return isObject(data);
5
+ return data !== null && typeof data === "object" && !Array.isArray(data);
7
6
  },
8
7
  array(data) {
9
- if (Array.isArray(data)) {
10
- return true;
11
- }
12
-
13
- return (
14
- typeof data === "object" &&
15
- data !== null &&
16
- "length" in data &&
17
- "0" in data &&
18
- Object.keys(data).length - 1 === data.length
19
- );
8
+ return Array.isArray(data);
20
9
  },
21
10
  string(data) {
22
11
  return typeof data === "string";
@@ -37,13 +26,11 @@ export const Types: Record<string, TypeFunction | false> = {
37
26
  // Not implemented yet
38
27
  timestamp: false,
39
28
  int8: false,
40
- unit8: false,
29
+ uint8: false,
41
30
  int16: false,
42
- unit16: false,
31
+ uint16: false,
43
32
  int32: false,
44
- unit32: false,
33
+ uint32: false,
45
34
  float32: false,
46
35
  float64: false
47
-
48
-
49
36
  };