schema-shield 1.0.0 → 1.0.2

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 +38 -12
  2. package/dist/formats.d.ts.map +1 -1
  3. package/dist/index.d.ts +14 -3
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1445 -447
  6. package/dist/index.min.js +1 -1
  7. package/dist/index.min.js.map +1 -1
  8. package/dist/index.mjs +1445 -447
  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} +3 -6
  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 +402 -84
  25. package/lib/index.ts +494 -46
  26. package/lib/keywords/array-keywords.ts +215 -21
  27. package/lib/keywords/number-keywords.ts +1 -1
  28. package/lib/keywords/object-keywords.ts +218 -113
  29. package/lib/keywords/other-keywords.ts +229 -76
  30. package/lib/keywords/string-keywords.ts +97 -7
  31. package/lib/types.ts +4 -5
  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/main-utils.ts +190 -0
  36. package/lib/utils/pattern-matcher.ts +66 -0
  37. package/package.json +1 -1
  38. package/dist/utils.d.ts.map +0 -1
  39. package/lib/utils.ts +0 -362
@@ -1,26 +1,98 @@
1
- import { hasChanged, 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
- const list = schema.enum;
55
+ let enumCache = (schema as any)._enumCache as
56
+ | { primitiveSet: Set<any>; objectValues: any[] }
57
+ | undefined;
58
+
59
+ if (!enumCache) {
60
+ const primitiveSet = new Set<any>();
61
+ const objectValues: any[] = [];
62
+ const list = schema.enum;
63
+
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
+ }
71
+ }
8
72
 
9
- for (let i = 0; i < list.length; i++) {
10
- const enumItem = list[i];
73
+ enumCache = { primitiveSet, objectValues };
74
+ Object.defineProperty(schema, "_enumCache", {
75
+ value: enumCache,
76
+ enumerable: false,
77
+ configurable: false,
78
+ writable: false
79
+ });
80
+ }
11
81
 
12
- if (enumItem === data) {
13
- return;
14
- }
82
+ if (
83
+ !(typeof data === "number" && Number.isNaN(data)) &&
84
+ enumCache.primitiveSet.has(data)
85
+ ) {
86
+ return;
87
+ }
15
88
 
16
- if (
17
- enumItem !== null &&
18
- data !== null &&
19
- typeof enumItem === "object" &&
20
- typeof data === "object" &&
21
- !hasChanged(enumItem, data)
22
- ) {
23
- return;
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)) {
94
+ return;
95
+ }
24
96
  }
25
97
  }
26
98
 
@@ -28,25 +100,54 @@ export const OtherKeywords: Record<string, KeywordFunction> = {
28
100
  },
29
101
 
30
102
  allOf(schema, data, defineError) {
31
- for (let i = 0; i < schema.allOf.length; i++) {
32
- if (isObject(schema.allOf[i])) {
33
- if ("$validate" in schema.allOf[i]) {
34
- const error = schema.allOf[i].$validate(data);
35
- if (error) {
36
- return defineError("Value is not valid", { cause: error, data });
37
- }
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 });
38
112
  }
39
- continue;
113
+ return;
40
114
  }
41
115
 
42
- if (typeof schema.allOf[i] === "boolean") {
43
- if (Boolean(data) !== schema.allOf[i]) {
44
- 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 });
45
138
  }
46
139
  continue;
47
140
  }
48
141
 
49
- 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) {
50
151
  return defineError("Value is not valid", { data });
51
152
  }
52
153
  }
@@ -55,26 +156,55 @@ export const OtherKeywords: Record<string, KeywordFunction> = {
55
156
  },
56
157
 
57
158
  anyOf(schema, data, defineError) {
58
- for (let i = 0; i < schema.anyOf.length; i++) {
59
- if (isObject(schema.anyOf[i])) {
60
- if ("$validate" in schema.anyOf[i]) {
61
- const error = schema.anyOf[i].$validate(data);
62
- if (!error) {
63
- return;
64
- }
65
- 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;
66
168
  }
169
+ return defineError("Value is not valid", { data });
170
+ }
171
+
172
+ if (onlyBranch.kind === "alwaysValid") {
67
173
  return;
68
- } else {
69
- if (typeof schema.anyOf[i] === "boolean") {
70
- if (Boolean(data) === schema.anyOf[i]) {
71
- return;
72
- }
73
- }
174
+ }
175
+
176
+ if (onlyBranch.kind === "alwaysInvalid") {
177
+ return defineError("Value is not valid", { data });
178
+ }
74
179
 
75
- if (data === schema.anyOf[i]) {
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];
189
+
190
+ if (branch.kind === "validate") {
191
+ const error = branch.validate(data);
192
+ if (!error) {
76
193
  return;
77
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;
78
208
  }
79
209
  }
80
210
 
@@ -82,41 +212,51 @@ export const OtherKeywords: Record<string, KeywordFunction> = {
82
212
  },
83
213
 
84
214
  oneOf(schema, data, defineError) {
85
- const list = schema.oneOf;
86
- let validCount = 0;
215
+ const branches = getBranchEntries(schema, "oneOf");
87
216
 
88
- for (let i = 0; i < list.length; i++) {
89
- const sub = list[i];
90
-
91
- if (isObject(sub)) {
92
- if ("$validate" in sub) {
93
- const error = sub.$validate(data);
94
- if (!error) {
95
- validCount++;
96
- if (validCount > 1) {
97
- return defineError("Value is not valid", { data });
98
- }
99
- }
100
- continue;
101
- }
102
- validCount++;
103
- if (validCount > 1) {
104
- return defineError("Value is not valid", { data });
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;
105
224
  }
106
- continue;
225
+ return defineError("Value is not valid", { data });
107
226
  }
108
227
 
109
- if (typeof sub === "boolean") {
110
- if (Boolean(data) === sub) {
111
- validCount++;
112
- if (validCount > 1) {
113
- return defineError("Value is not valid", { data });
114
- }
115
- }
116
- continue;
228
+ if (onlyBranch.kind === "alwaysValid") {
229
+ return;
117
230
  }
118
231
 
119
- if (data === sub) {
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;
255
+ } else {
256
+ isValid = data === branch.value;
257
+ }
258
+
259
+ if (isValid) {
120
260
  validCount++;
121
261
  if (validCount > 1) {
122
262
  return defineError("Value is not valid", { data });
@@ -137,8 +277,12 @@ export const OtherKeywords: Record<string, KeywordFunction> = {
137
277
  }
138
278
 
139
279
  if (
140
- (isObject(data) &&
141
- isObject(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) &&
142
286
  !hasChanged(data, schema.const)) ||
143
287
  (Array.isArray(data) &&
144
288
  Array.isArray(schema.const) &&
@@ -190,11 +334,15 @@ export const OtherKeywords: Record<string, KeywordFunction> = {
190
334
  return;
191
335
  }
192
336
 
193
- if (isObject(schema.not)) {
337
+ if (
338
+ schema.not &&
339
+ typeof schema.not === "object" &&
340
+ !Array.isArray(schema.not)
341
+ ) {
194
342
  if ("$validate" in schema.not) {
195
- const error = schema.not.$validate(data);
343
+ const error = (schema.not as any).$validate(data);
196
344
  if (!error) {
197
- return defineError("Value is not valid", { cause: error, data });
345
+ return defineError("Value is not valid", { data });
198
346
  }
199
347
  return;
200
348
  }
@@ -206,6 +354,10 @@ export const OtherKeywords: Record<string, KeywordFunction> = {
206
354
 
207
355
  $ref(schema, data, defineError, instance) {
208
356
  if (schema._resolvedRef) {
357
+ if (schema.$validate !== schema._resolvedRef) {
358
+ schema.$validate = schema._resolvedRef;
359
+ }
360
+
209
361
  return schema._resolvedRef(data);
210
362
  }
211
363
 
@@ -225,6 +377,7 @@ export const OtherKeywords: Record<string, KeywordFunction> = {
225
377
  }
226
378
 
227
379
  schema._resolvedRef = targetSchema.$validate;
380
+ schema.$validate = schema._resolvedRef;
228
381
  return schema._resolvedRef(data);
229
382
  }
230
383
  };
@@ -1,4 +1,8 @@
1
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,12 +26,24 @@ export const StringKeywords: Record<string, KeywordFunction> = {
22
26
  return;
23
27
  }
24
28
 
25
- let patternRegexp = (schema as any)._patternRegexp as RegExp | undefined;
26
- if (!patternRegexp) {
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;
36
+
37
+ if (!patternMatch) {
27
38
  try {
28
- patternRegexp = new RegExp(schema.pattern, "u");
29
- Object.defineProperty(schema, "_patternRegexp", {
30
- value: patternRegexp,
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,
31
47
  enumerable: false,
32
48
  configurable: false,
33
49
  writable: false
@@ -40,7 +56,28 @@ export const StringKeywords: Record<string, KeywordFunction> = {
40
56
  }
41
57
  }
42
58
 
43
- 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) {
44
81
  return;
45
82
  }
46
83
 
@@ -58,6 +95,12 @@ export const StringKeywords: Record<string, KeywordFunction> = {
58
95
  | FormatFunction
59
96
  | false
60
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;
61
104
 
62
105
  if (formatValidate === undefined) {
63
106
  formatValidate = instance.getFormat(schema.format);
@@ -69,7 +112,54 @@ export const StringKeywords: Record<string, KeywordFunction> = {
69
112
  });
70
113
  }
71
114
 
72
- if (!formatValidate || formatValidate(data)) {
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) {
73
163
  return;
74
164
  }
75
165
 
package/lib/types.ts CHANGED
@@ -1,9 +1,8 @@
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
8
  return Array.isArray(data);
@@ -27,11 +26,11 @@ export const Types: Record<string, TypeFunction | false> = {
27
26
  // Not implemented yet
28
27
  timestamp: false,
29
28
  int8: false,
30
- unit8: false,
29
+ uint8: false,
31
30
  int16: false,
32
- unit16: false,
31
+ uint16: false,
33
32
  int32: false,
34
- unit32: false,
33
+ uint32: false,
35
34
  float32: false,
36
35
  float64: false
37
36
  };