ata-validator 1.61.2 → 1.62.0
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.
- package/README.md +159 -37
- package/bin/ata.js +35 -2
- package/compat.js +15 -2
- package/compiled-standard.d.ts +6 -0
- package/compiled-standard.mjs +66 -0
- package/index.d.ts +7 -0
- package/index.js +10 -0
- package/lib/aot-build.js +27 -1
- package/lib/aot-impl.js +108 -17
- package/lib/buffer-gate.js +25 -14
- package/lib/bun-target.js +21 -5
- package/lib/clone-emit.js +115 -10
- package/lib/codegen-engine.js +94 -18
- package/lib/compiled-common.js +11 -4
- package/lib/compiled-standard.js +33 -0
- package/lib/compiled-verdict.js +2 -2
- package/lib/compiled.js +1 -1
- package/lib/data-positions.js +18 -0
- package/lib/defaults.js +101 -23
- package/lib/describe-schema.js +38 -5
- package/lib/diagnose.js +51 -29
- package/lib/enrich-error.js +23 -1
- package/lib/error-sort-rank.js +12 -0
- package/lib/formats-source.js +12 -1
- package/lib/formats.js +19 -19
- package/lib/interpreter-core.js +23 -4
- package/lib/js-compiler-errors.js +70 -72
- package/lib/js-compiler.js +212 -72
- package/lib/json-rejections.js +1 -1
- package/lib/migrate.js +24 -5
- package/lib/native-attach.browser.js +7 -0
- package/lib/native-attach.js +76 -0
- package/lib/native-positions.js +7 -0
- package/lib/plan-compiler.js +7 -1
- package/lib/regex-linear.js +88 -0
- package/lib/rejections.js +13 -3
- package/lib/render-compact.js +5 -5
- package/lib/render-pretty.js +23 -10
- package/lib/safe-regex.js +29 -4
- package/lib/scan-compiler.js +16 -4
- package/lib/schema-diff.js +103 -0
- package/lib/stream.js +293 -0
- package/lib/tier0.js +6 -2
- package/lib/ts-gen.js +25 -12
- package/lib/unique-items.js +11 -3
- package/lib/validator-core.js +61 -66
- package/lib/verify.js +25 -6
- package/lib/version.js +1 -1
- package/package.json +24 -8
- package/stream.d.ts +51 -0
- package/stream.js +3 -0
- package/stream.mjs +3 -0
package/lib/codegen-engine.js
CHANGED
|
@@ -440,7 +440,7 @@ function installCodegenPaths (ctx) {
|
|
|
440
440
|
let nativeText; // undefined until the first large document
|
|
441
441
|
const nativeVerdict = (jsonStr) => {
|
|
442
442
|
if (nativeText === undefined) {
|
|
443
|
-
nativeText = !!getNative() && !require('./buffer-gate.js').bufferNeedsSlowPath(schemaObj, this._schemaMap, this._keywords);
|
|
443
|
+
nativeText = !!getNative() && !require('./buffer-gate.js').bufferNeedsSlowPath(schemaObj, this._schemaMap, this._keywords, this._userFormats);
|
|
444
444
|
if (nativeText) {
|
|
445
445
|
this._ensureNative();
|
|
446
446
|
if (!(this._fastSlot >= 0)) nativeText = false;
|
|
@@ -715,7 +715,7 @@ installCodegenPaths.installScanner = function installScanner (schemaObj, options
|
|
|
715
715
|
// access expression read through a parent that could be absent or null:
|
|
716
716
|
// `{ c: { properties: { d: { additionalProperties: false } } } }` threw a
|
|
717
717
|
// TypeError from validate({}) whenever the optional `c` was missing.
|
|
718
|
-
function emitRemovals(node, access, lines, depth, seen) {
|
|
718
|
+
function emitRemovals(node, access, lines, depth, seen, own) {
|
|
719
719
|
if (!node || typeof node !== 'object' || !node.properties) return;
|
|
720
720
|
if (seen.has(node)) return;
|
|
721
721
|
seen.add(node);
|
|
@@ -733,13 +733,15 @@ function emitRemovals(node, access, lines, depth, seen) {
|
|
|
733
733
|
for (const key of keys) {
|
|
734
734
|
const prop = node.properties[key];
|
|
735
735
|
if (prop && typeof prop === 'object' && prop.properties) {
|
|
736
|
-
|
|
736
|
+
// Into own keys only: a nested object the document inherits is not
|
|
737
|
+
// part of it, and deleting from it would strip a shared object.
|
|
738
|
+
emitRemovals(prop, `${access}[${JSON.stringify(key)}]`, body, depth + 1, seen, `Object.hasOwn(${access},${JSON.stringify(key)})&&`);
|
|
737
739
|
}
|
|
738
740
|
}
|
|
739
741
|
seen.delete(node);
|
|
740
742
|
if (body.length === 0) return;
|
|
741
743
|
if (depth === 0) lines.push(...body);
|
|
742
|
-
else lines.push(`if(${access}!==null&&typeof ${access}==='object'&&!Array.isArray(${access})){${body.join('\n')}}`);
|
|
744
|
+
else lines.push(`if(${own}${access}!==null&&typeof ${access}==='object'&&!Array.isArray(${access})){${body.join('\n')}}`);
|
|
743
745
|
}
|
|
744
746
|
|
|
745
747
|
// The coercions the closure pass applies, emitted. Values: `number` and
|
|
@@ -779,6 +781,15 @@ function emitCoercions(node, ov, lines, st, depth) {
|
|
|
779
781
|
if (st.seen.has(node)) { st.cycle = true; return; }
|
|
780
782
|
st.seen.add(node);
|
|
781
783
|
if (node.properties) {
|
|
784
|
+
// Own properties only, as the closure pass has it: a value the object
|
|
785
|
+
// inherits belongs to some other object, and `k in o` once wrapped
|
|
786
|
+
// Object.prototype's own `constructor` into an own array key. On a plain
|
|
787
|
+
// object (prototype Object.prototype) a read that gives a primitive or
|
|
788
|
+
// an object is own for every key but the ones Object.prototype has, so
|
|
789
|
+
// the flag settles the common case and hasOwn is consulted otherwise.
|
|
790
|
+
const pk = '_pk' + st.n++;
|
|
791
|
+
let flagged = false;
|
|
792
|
+
const flag = () => { if (!flagged) { flagged = true; lines.push(`const ${pk}=Object.getPrototypeOf(${ov})===Object.prototype`); } };
|
|
782
793
|
for (const [key, prop] of Object.entries(node.properties)) {
|
|
783
794
|
// Coercion writes with plain assignment, which for a key named
|
|
784
795
|
// __proto__ rewrites the prototype instead. The raw value still goes
|
|
@@ -786,13 +797,15 @@ function emitCoercions(node, ov, lines, st, depth) {
|
|
|
786
797
|
if (key === '__proto__' || !prop || typeof prop !== 'object') continue;
|
|
787
798
|
const k = JSON.stringify(key);
|
|
788
799
|
const a = `${ov}[${k}]`;
|
|
789
|
-
|
|
800
|
+
const own = key in Object.prototype ? `Object.hasOwn(${ov},${k})` : `(${pk}||Object.hasOwn(${ov},${k}))`;
|
|
801
|
+
if (isScalarCoercible(prop)) { flag(); lines.push(`if(${own}){`, ...scalarCoercion(a, prop.type), '}'); }
|
|
790
802
|
// Wrapping a lone value in an array is a top-level rule only, as it was.
|
|
791
|
-
else if (depth === 0 && prop.type === 'array' && st.arrayMode) lines.push(`if(${
|
|
803
|
+
else if (depth === 0 && prop.type === 'array' && st.arrayMode) { flag(); lines.push(`if(${own}&&${a}!==undefined&&!Array.isArray(${a}))${a}=[${a}]`); }
|
|
792
804
|
// A leaf, the common case, has nothing below it to coerce.
|
|
793
805
|
if ((prop.properties || prop.items) && coercesInside(prop, new Set(), st.memo)) {
|
|
806
|
+
flag();
|
|
794
807
|
const n = '_c' + st.n++;
|
|
795
|
-
lines.push(`{const ${n}=${a};if(typeof ${n}==='object'&&${n}!==null){`);
|
|
808
|
+
lines.push(`{const ${n}=${a};if(typeof ${n}==='object'&&${n}!==null&&${own}){`);
|
|
796
809
|
emitCoercions(prop, n, lines, st, depth + 1);
|
|
797
810
|
lines.push('}}');
|
|
798
811
|
}
|
|
@@ -815,14 +828,23 @@ function emitCoercions(node, ov, lines, st, depth) {
|
|
|
815
828
|
}
|
|
816
829
|
// From the closure pass, on the first reference a defaults walk meets: loaded
|
|
817
830
|
// with this module it joined every process's cold start.
|
|
818
|
-
|
|
819
|
-
|
|
831
|
+
// A fragment `$ref` is read against the resource its node belongs to (an
|
|
832
|
+
// embedded `$id`), as validation and the closure pass read it.
|
|
833
|
+
let _defaultsMod = null;
|
|
834
|
+
const defaultsMod = () => _defaultsMod || (_defaultsMod = require('./defaults'));
|
|
835
|
+
const refOf = (rs, node) => {
|
|
836
|
+
if (typeof node.$ref !== 'string') return null;
|
|
837
|
+
if (rs.res === undefined) rs.res = defaultsMod().resourceRoots(rs.root);
|
|
838
|
+
return defaultsMod().refTarget(rs.root, rs.res, node);
|
|
839
|
+
};
|
|
820
840
|
// Whether a default is reachable from a schema through the places the pass
|
|
821
841
|
// goes (lib/defaults.js reachesDefault, which this pass is held to): the
|
|
822
842
|
// same walk, with answers kept per schema object for the whole pass, except
|
|
823
843
|
// one that rests on a schema still being decided (a recursive reference). It
|
|
824
844
|
// started over for every property, quadratic in the depth of the schema.
|
|
825
|
-
|
|
845
|
+
// `res`, the resources map, is built on the first `$ref` the walk follows:
|
|
846
|
+
// most schemas have none, and the walk over the schema is not worth making.
|
|
847
|
+
function reachState(root) { return { root, memo: new Map(), res: undefined }; }
|
|
826
848
|
function hasDefaultsInside(node, rs) { return reachDefault(node, rs, new Set()) === 1; }
|
|
827
849
|
function reachDefault(node, rs, stack) {
|
|
828
850
|
if (!node || typeof node !== 'object') return 0;
|
|
@@ -838,7 +860,7 @@ function reachDefault(node, rs, stack) {
|
|
|
838
860
|
if (prop && typeof prop === 'object' && (prop.default !== undefined || check(prop))) { yes = true; break; }
|
|
839
861
|
}
|
|
840
862
|
}
|
|
841
|
-
if (!yes && typeof node.$ref === 'string') yes = check(
|
|
863
|
+
if (!yes && typeof node.$ref === 'string') yes = check(refOf(rs, node));
|
|
842
864
|
if (!yes) for (const list of [node.allOf, node.prefixItems]) { if (Array.isArray(list) && list.some(check)) { yes = true; break; } }
|
|
843
865
|
if (!yes) for (const one of [node.items, node.additionalProperties]) { if (check(one)) { yes = true; break; } }
|
|
844
866
|
if (!yes) for (const map of [node.patternProperties, node.dependentSchemas]) { if (map && typeof map === 'object' && Object.keys(map).some((k) => check(map[k]))) { yes = true; break; } }
|
|
@@ -868,6 +890,8 @@ function emitDefaultsAt(schema, expr, lines, st) {
|
|
|
868
890
|
// A pass that would expand to more than this many lines (references written
|
|
869
891
|
// out again at every use) is left to the closure pass.
|
|
870
892
|
const DEFAULTS_MAX_LINES = 4000;
|
|
893
|
+
// The most default-value source the generated pass writes, in characters.
|
|
894
|
+
const DEFAULTS_MAX_BYTES = 256 * 1024;
|
|
871
895
|
// How many schema objects emitDefaults would write out, references expanded
|
|
872
896
|
// at every use, or Infinity where it would decline (a cycle, or defaults
|
|
873
897
|
// under patternProperties). Each writes at least one line, so a count over
|
|
@@ -886,13 +910,22 @@ function defaultsVisits(node, st, stack) {
|
|
|
886
910
|
add(node.items);
|
|
887
911
|
add(node.additionalProperties);
|
|
888
912
|
if (node.dependentSchemas && typeof node.dependentSchemas === 'object') Object.values(node.dependentSchemas).forEach(add);
|
|
889
|
-
if (typeof node.$ref === 'string') add(
|
|
913
|
+
if (typeof node.$ref === 'string') add(refOf(st.reach, node));
|
|
890
914
|
if (Array.isArray(node.allOf)) node.allOf.forEach(add);
|
|
891
915
|
stack.delete(node);
|
|
892
916
|
if (n > DEFAULTS_MAX_LINES) n = Infinity;
|
|
893
917
|
st.visits.set(node, n);
|
|
894
918
|
return n;
|
|
895
919
|
}
|
|
920
|
+
// A default as source that rebuilds it fresh on every fill. An object
|
|
921
|
+
// literal reads a "__proto__" key as the prototype slot, so a default that
|
|
922
|
+
// holds one (own, as JSON.parse writes it) is rebuilt by JSON.parse from its
|
|
923
|
+
// text instead: as a literal the filled value had no such key and inherited
|
|
924
|
+
// from the default's inner object.
|
|
925
|
+
function defaultLiteral(v) {
|
|
926
|
+
const text = JSON.stringify(v);
|
|
927
|
+
return v !== null && typeof v === 'object' && text.includes('"__proto__"') ? `JSON.parse(${JSON.stringify(text)})` : text;
|
|
928
|
+
}
|
|
896
929
|
function emitDefaults(node, ov, lines, st) {
|
|
897
930
|
if (st.seen.has(node) || lines.length > DEFAULTS_MAX_LINES) { st.cycle = true; return; }
|
|
898
931
|
st.seen.add(node);
|
|
@@ -915,7 +948,13 @@ function emitDefaults(node, ov, lines, st) {
|
|
|
915
948
|
const inherited = key in Object.prototype;
|
|
916
949
|
const absent = inherited ? `!Object.hasOwn(${ov},${k})` : `(${ov}[${k}]===undefined||!${pk})&&!Object.hasOwn(${ov},${k})`;
|
|
917
950
|
if (prop.default !== undefined) {
|
|
918
|
-
const def =
|
|
951
|
+
const def = defaultLiteral(prop.default);
|
|
952
|
+
// A shared definition is written out at every reference to it, its
|
|
953
|
+
// defaults with it: 200 references to a 50 KB default made 10 MB of
|
|
954
|
+
// source. Past the budget the closure pass, which holds each default
|
|
955
|
+
// once, takes the schema.
|
|
956
|
+
st.defaultBytes = (st.defaultBytes || 0) + def.length;
|
|
957
|
+
if (st.defaultBytes > DEFAULTS_MAX_BYTES) { st.cycle = true; st.seen.delete(node); return; }
|
|
919
958
|
// Assignment to a key named __proto__ hits the prototype setter
|
|
920
959
|
// instead of creating a property; defineProperty writes an own key.
|
|
921
960
|
lines.push(key === '__proto__'
|
|
@@ -978,7 +1017,7 @@ function emitDefaults(node, ov, lines, st) {
|
|
|
978
1017
|
// written out here (one back into what is being written is a cycle, left
|
|
979
1018
|
// to the closure pass), then each `allOf` member, in the closure pass's order.
|
|
980
1019
|
if (typeof node.$ref === 'string') {
|
|
981
|
-
const target =
|
|
1020
|
+
const target = refOf(st.reach, node);
|
|
982
1021
|
if (target && hasDefaultsInside(target, st.reach)) emitDefaults(target, ov, lines, st);
|
|
983
1022
|
}
|
|
984
1023
|
if (Array.isArray(node.allOf)) {
|
|
@@ -1075,8 +1114,15 @@ function buildParse (self, decline, extended) {
|
|
|
1075
1114
|
self._ensureCompiled();
|
|
1076
1115
|
const verdict = extended ? (d) => self.isValidObject(d) : self._jsFn;
|
|
1077
1116
|
if (typeof self._jsFn !== 'function') return decline('the schema has no generated verdict function');
|
|
1078
|
-
|
|
1079
|
-
|
|
1117
|
+
// With a preprocess pass (defaults, removeAdditional), validate() checks
|
|
1118
|
+
// the document after the pass has run, and a check of the raw input can
|
|
1119
|
+
// answer the other way: a default can make a conditional apply. The
|
|
1120
|
+
// verdict is then taken on a filled clone of the input, the caller's
|
|
1121
|
+
// object left alone, and the copy handed back is checked as well, so
|
|
1122
|
+
// parse() never returns a value validate() would refuse.
|
|
1123
|
+
const fill = self._preprocess;
|
|
1124
|
+
const filled = typeof fill === 'function';
|
|
1125
|
+
const reject = (data) => {
|
|
1080
1126
|
const e = new Error('validation failed');
|
|
1081
1127
|
e.name = 'AtaValidationError';
|
|
1082
1128
|
let target = data;
|
|
@@ -1099,8 +1145,28 @@ function buildParse (self, decline, extended) {
|
|
|
1099
1145
|
set (v) { Object.defineProperty(e, 'errors', { value: v, writable: true, configurable: true, enumerable: true }); },
|
|
1100
1146
|
});
|
|
1101
1147
|
throw e;
|
|
1148
|
+
};
|
|
1149
|
+
if (!filled) {
|
|
1150
|
+
return (data) => {
|
|
1151
|
+
if (!verdict(data)) reject(data);
|
|
1152
|
+
return copy(data);
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
return (data) => {
|
|
1156
|
+
let doc;
|
|
1157
|
+
try { doc = structuredClone(data); } catch { doc = undefined; }
|
|
1158
|
+
if (doc === undefined && data !== undefined) {
|
|
1159
|
+
throw new TypeError('parse() is not available for this value: it cannot be copied to fill its defaults before the check. Use validate() instead.');
|
|
1160
|
+
}
|
|
1161
|
+
if (doc !== null && typeof doc === 'object') fill(doc);
|
|
1162
|
+
if (!verdict(doc)) reject(data);
|
|
1163
|
+
// The copy is built from the input, filling its defaults as written, as
|
|
1164
|
+
// it is without a preprocess pass.
|
|
1165
|
+
const out = copy(data);
|
|
1166
|
+
if (!verdict(out)) {
|
|
1167
|
+
throw new TypeError('parse() is not available for this value: the copy that keeps the declared properties is not one the schema accepts. Use validate() instead.');
|
|
1102
1168
|
}
|
|
1103
|
-
return
|
|
1169
|
+
return out;
|
|
1104
1170
|
};
|
|
1105
1171
|
}
|
|
1106
1172
|
|
|
@@ -1147,7 +1213,17 @@ function coldTwin (self, Validator) {
|
|
|
1147
1213
|
if (t === undefined) {
|
|
1148
1214
|
t = null;
|
|
1149
1215
|
if (!self._interpretOnly && !self._initialized && mayExpandPast(self, COLD_MIN)) {
|
|
1150
|
-
try {
|
|
1216
|
+
try {
|
|
1217
|
+
t = new Validator(self._rawSchema, Object.assign({}, self._options, { engine: 'interpreter' }));
|
|
1218
|
+
// The registry as it is now, not as the constructor options had it:
|
|
1219
|
+
// a schema registered with addSchema() after construction is in the
|
|
1220
|
+
// live map only, and a twin without it answered against the schema
|
|
1221
|
+
// the registry had replaced.
|
|
1222
|
+
if (self._schemaMap && self._schemaMap.size > 0) {
|
|
1223
|
+
t._schemaMap = new Map(self._schemaMap);
|
|
1224
|
+
t._schemaMapShared = false;
|
|
1225
|
+
}
|
|
1226
|
+
} catch { t = null; }
|
|
1151
1227
|
}
|
|
1152
1228
|
self._twin = t;
|
|
1153
1229
|
}
|
package/lib/compiled-common.js
CHANGED
|
@@ -109,13 +109,20 @@ function combine(a, b) {
|
|
|
109
109
|
};
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
// The JSON entry points under a check, as the runtime has them.
|
|
113
|
-
|
|
112
|
+
// The JSON entry points under a check, as the runtime has them. `fill` is
|
|
113
|
+
// the wrapper's defaults pass, or null: the check sees the document the
|
|
114
|
+
// schema checked, defaults filled, not the raw text.
|
|
115
|
+
function extendJSON(base, { check, errors }, fill) {
|
|
116
|
+
const prepared = (text) => {
|
|
117
|
+
const p = parse(text);
|
|
118
|
+
if (!p.error && fill) fill(p.value);
|
|
119
|
+
return p;
|
|
120
|
+
};
|
|
114
121
|
return {
|
|
115
122
|
validateJSON: base.validateJSON && ((text) => {
|
|
116
123
|
const res = base.validateJSON(text);
|
|
117
124
|
if (!res.valid) return res;
|
|
118
|
-
const p =
|
|
125
|
+
const p = prepared(text);
|
|
119
126
|
if (p.error) return res;
|
|
120
127
|
if (check(p.value)) return res;
|
|
121
128
|
const e = errors(p.value);
|
|
@@ -123,7 +130,7 @@ function extendJSON(base, { check, errors }) {
|
|
|
123
130
|
}),
|
|
124
131
|
isValidJSON: (text) => {
|
|
125
132
|
if (!base.isValidJSON(text)) return false;
|
|
126
|
-
const p =
|
|
133
|
+
const p = prepared(text);
|
|
127
134
|
return p.error ? true : check(p.value);
|
|
128
135
|
},
|
|
129
136
|
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// bareStandard: the compiled-module wrapper for a validator that is only ever
|
|
4
|
+
// handed to a Standard Schema consumer (a router's validator middleware, a
|
|
5
|
+
// form library, an RPC layer) and never called by its own code. Such a
|
|
6
|
+
// consumer reads `~standard` and nothing else, and an issue carries a message
|
|
7
|
+
// and a path, so the wrapper is built on a module written with
|
|
8
|
+
// `plainErrors: true`, whose errors are those two things in schema order, and
|
|
9
|
+
// carries no error pipeline, no Standard Schema install and no extension hook.
|
|
10
|
+
// The issues are the runtime's (tests/test_compiled_standard.js). A bundler
|
|
11
|
+
// plugin puts this in place of `new Validator(schema)` where it sees that use.
|
|
12
|
+
|
|
13
|
+
const { standardSchemaFor } = require('./standard-schema');
|
|
14
|
+
|
|
15
|
+
const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
|
|
16
|
+
// When the module's error function answers valid where its verdict said no,
|
|
17
|
+
// the disagreement is reported rather than turned into an acceptance.
|
|
18
|
+
const VERDICT_DISAGREES = Object.freeze({ valid: false, errors: Object.freeze([Object.freeze({ keyword: 'validation', instancePath: '', schemaPath: '#', params: Object.freeze({}), message: 'schema validation failed' })]) });
|
|
19
|
+
|
|
20
|
+
function bareStandard(mod, schema, useDefaults) {
|
|
21
|
+
const fill = useDefaults !== false && typeof mod.applyDefaults === 'function' ? mod.applyDefaults : null;
|
|
22
|
+
const isValid = mod.isValid;
|
|
23
|
+
const errFn = mod.validate;
|
|
24
|
+
const validate = (data) => {
|
|
25
|
+
if (fill !== null) fill(data);
|
|
26
|
+
if (isValid(data)) return VALID;
|
|
27
|
+
const r = errFn(data);
|
|
28
|
+
return r && r.valid === false ? r : VERDICT_DISAGREES;
|
|
29
|
+
};
|
|
30
|
+
return { '~standard': standardSchemaFor(null, () => schema, (_, value) => validate(value)) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { bareStandard };
|
package/lib/compiled-verdict.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
const { prepare, parse, installExtension, extendJSON } = require('./compiled-common');
|
|
11
11
|
|
|
12
12
|
function fromCompiledVerdict(mod, schema, options) {
|
|
13
|
-
const { isValid } = prepare(mod, schema, options, 'fromCompiledVerdict');
|
|
13
|
+
const { fill, isValid } = prepare(mod, schema, options, 'fromCompiledVerdict');
|
|
14
14
|
const api = {
|
|
15
15
|
isValidObject(data) {
|
|
16
16
|
return isValid(data);
|
|
@@ -22,7 +22,7 @@ function fromCompiledVerdict(mod, schema, options) {
|
|
|
22
22
|
};
|
|
23
23
|
installExtension(api, schema, (base, ext) => ({
|
|
24
24
|
isValidObject: (d) => base.isValidObject(d) && ext.check(d),
|
|
25
|
-
isValidJSON: extendJSON(base, ext).isValidJSON,
|
|
25
|
+
isValidJSON: extendJSON(base, ext, fill).isValidJSON,
|
|
26
26
|
}));
|
|
27
27
|
return api;
|
|
28
28
|
}
|
package/lib/compiled.js
CHANGED
|
@@ -87,7 +87,7 @@ function fromCompiled(mod, schema, options) {
|
|
|
87
87
|
},
|
|
88
88
|
};
|
|
89
89
|
installExtension(api, schema, (base, ext) => {
|
|
90
|
-
const json = extendJSON(base, ext);
|
|
90
|
+
const json = extendJSON(base, ext, fill);
|
|
91
91
|
return {
|
|
92
92
|
// The verdict is the schema's and the check's. A failure lists the
|
|
93
93
|
// schema's errors, then the check's, read when somebody asks.
|
package/lib/data-positions.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
/**
|
|
4
5
|
* Build pointer → { byteOffset, length, line, col, text } from a JSON
|
|
5
6
|
* input buffer. Called only when validation fails AND richErrors is on
|
|
@@ -156,6 +157,16 @@ function buildDataPositionMap (input) {
|
|
|
156
157
|
col: off - lineStart[line] + 1,
|
|
157
158
|
text: lines[line] || '',
|
|
158
159
|
};
|
|
160
|
+
// A minified document is one line, and each error carried all of it:
|
|
161
|
+
// 4000 errors on a 100 KB line serialized to 800 MB. A long line is cut
|
|
162
|
+
// to 200 characters from 60 before the value, and textCol says where the
|
|
163
|
+
// value starts in the cut text; col stays the column in the line.
|
|
164
|
+
if (entry.text.length > 200) {
|
|
165
|
+
let s = entry.col > 61 ? entry.col - 61 : 0;
|
|
166
|
+
if (s + 200 > entry.text.length) s = entry.text.length - 200;
|
|
167
|
+
entry.text = entry.text.slice(s, s + 200);
|
|
168
|
+
entry.textCol = entry.col - s;
|
|
169
|
+
}
|
|
159
170
|
if (keyStarts[k] >= 0) {
|
|
160
171
|
const keyLine = lineOf(keyStarts[k]);
|
|
161
172
|
entry.keyOffset = keyStarts[k];
|
|
@@ -355,6 +366,13 @@ function buildTargetedPositionMap (input, wanted) {
|
|
|
355
366
|
col: off - lineStart[line] + 1,
|
|
356
367
|
text: lines[line] || '',
|
|
357
368
|
};
|
|
369
|
+
// A long line is cut around the value, as in buildDataPositionMap.
|
|
370
|
+
if (entry.text.length > 200) {
|
|
371
|
+
let s = entry.col > 61 ? entry.col - 61 : 0;
|
|
372
|
+
if (s + 200 > entry.text.length) s = entry.text.length - 200;
|
|
373
|
+
entry.text = entry.text.slice(s, s + 200);
|
|
374
|
+
entry.textCol = entry.col - s;
|
|
375
|
+
}
|
|
358
376
|
if (keyStarts[k] >= 0) {
|
|
359
377
|
const keyLine = lineOf(keyStarts[k]);
|
|
360
378
|
entry.keyOffset = keyStarts[k];
|
package/lib/defaults.js
CHANGED
|
@@ -23,11 +23,21 @@ const MAX_DEPTH = 512;
|
|
|
23
23
|
|
|
24
24
|
// Build the pass for a schema: a function that fills defaults into a value in
|
|
25
25
|
// place (mutates), or null when no default is reachable from the schema.
|
|
26
|
-
|
|
26
|
+
// `keyMatcher` turns a patternProperties pattern into something with a
|
|
27
|
+
// `test` method; without one the platform RegExp is used. The runtime hands
|
|
28
|
+
// in the matcher validation uses (see safeKeyMatcher below), so a key that
|
|
29
|
+
// makes a backtracking pattern run for seconds costs no more here than it
|
|
30
|
+
// does to validate. A module written ahead of time embeds this function as
|
|
31
|
+
// text and calls it without one.
|
|
32
|
+
function buildDefaultsApplier(schema, keyMatcher) {
|
|
27
33
|
if (typeof schema !== "object" || schema === null) return null;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
// `visits`: per call, the schemas already applied to each object through a
|
|
35
|
+
// `$ref` or an `allOf` member. Applying one twice to the same object fills
|
|
36
|
+
// nothing new, and following every edge of a chain such as
|
|
37
|
+
// n_i = allOf [n_(i-1), n_(i-1)] cost 2^n applications of one schema.
|
|
38
|
+
const ctx = { root: schema, memo: new Map(), reach: new Map(), path: [], res: resourceRoots(schema), match: keyMatcher || keyPattern, visits: null };
|
|
39
|
+
const fn = compileNode(schema, ctx);
|
|
40
|
+
return fn === null ? null : (data) => { ctx.path.length = 0; ctx.visits = null; fn(data, 0, null); };
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
// Write an own property. Plain assignment of a key named `__proto__` does
|
|
@@ -70,13 +80,45 @@ function localRef(root, ref) {
|
|
|
70
80
|
return t !== null && typeof t === "object" ? t : null;
|
|
71
81
|
}
|
|
72
82
|
|
|
83
|
+
// For a schema that embeds other resources (a subschema with its own `$id`),
|
|
84
|
+
// the resource each schema object belongs to: a fragment `$ref` inside one
|
|
85
|
+
// is read against that resource, as validation reads it, not against the
|
|
86
|
+
// outer root. Null when nothing is embedded, which is nearly always. An `$id`
|
|
87
|
+
// that is only a fragment (`#foo`, a draft-07 anchor) starts no resource.
|
|
88
|
+
// Values under `default`, `const`, `enum` and `examples` are data, not
|
|
89
|
+
// schemas, and are not walked.
|
|
90
|
+
function resourceRoots(root) {
|
|
91
|
+
if (root === null || typeof root !== "object") return null;
|
|
92
|
+
const map = new Map();
|
|
93
|
+
let embedded = false;
|
|
94
|
+
const walk = (node, res) => {
|
|
95
|
+
if (node === null || typeof node !== "object" || map.has(node)) return;
|
|
96
|
+
if (Array.isArray(node)) { map.set(node, res); for (let i = 0; i < node.length; i++) walk(node[i], res); return; }
|
|
97
|
+
if (node !== root && typeof node.$id === "string" && node.$id.charCodeAt(0) !== 35) { res = node; embedded = true; }
|
|
98
|
+
map.set(node, res);
|
|
99
|
+
for (const k of Object.keys(node)) {
|
|
100
|
+
if (k === "default" || k === "const" || k === "enum" || k === "examples") continue;
|
|
101
|
+
walk(node[k], res);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
walk(root, root);
|
|
105
|
+
return embedded ? map : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// The schema a `$ref` on `node` names, read against the resource `node`
|
|
109
|
+
// belongs to.
|
|
110
|
+
function refTarget(root, res, node) {
|
|
111
|
+
const base = res !== null ? res.get(node) : undefined;
|
|
112
|
+
return localRef(base !== undefined ? base : root, node.$ref);
|
|
113
|
+
}
|
|
114
|
+
|
|
73
115
|
// Whether a default is reachable from a schema, through the same places the
|
|
74
116
|
// pass goes: 1 yes, 0 no, 2 not known yet because the walk came back to a
|
|
75
117
|
// schema it is still deciding (a recursive reference). Answers are kept per
|
|
76
118
|
// schema object in `memo`, except one that rests on an undecided schema,
|
|
77
119
|
// which is worked out again when asked; the walk used to start over for every
|
|
78
120
|
// schema object it compiled, quadratic in the depth of the schema.
|
|
79
|
-
function reachesDefault(node, root, memo, stack) {
|
|
121
|
+
function reachesDefault(node, root, memo, stack, res) {
|
|
80
122
|
if (node === null || typeof node !== "object") return 0;
|
|
81
123
|
const known = memo.get(node);
|
|
82
124
|
if (known !== undefined) return known;
|
|
@@ -85,9 +127,9 @@ function reachesDefault(node, root, memo, stack) {
|
|
|
85
127
|
const props = valuesOf(node.properties);
|
|
86
128
|
let yes = props.some((p) => p && typeof p === "object" && p.default !== undefined);
|
|
87
129
|
let open = false;
|
|
88
|
-
const below = props.concat(
|
|
130
|
+
const below = props.concat(refTarget(root, res, node), listOf(node.allOf), listOf(node.prefixItems), node.items, node.additionalProperties, valuesOf(node.patternProperties), valuesOf(node.dependentSchemas));
|
|
89
131
|
for (let i = 0; !yes && i < below.length; i++) {
|
|
90
|
-
const r = reachesDefault(below[i], root, memo, stack);
|
|
132
|
+
const r = reachesDefault(below[i], root, memo, stack, res);
|
|
91
133
|
if (r === 1) yes = true;
|
|
92
134
|
else if (r === 2) open = true;
|
|
93
135
|
}
|
|
@@ -119,10 +161,11 @@ function copyOf(v) {
|
|
|
119
161
|
// applied to the same value. A schema reached again (a recursive reference)
|
|
120
162
|
// takes the function being built for it, so a cycle in the schema is a loop
|
|
121
163
|
// in the calls, bounded by the depth of the data.
|
|
122
|
-
function compileNode(node,
|
|
164
|
+
function compileNode(node, ctx) {
|
|
123
165
|
if (node === null || typeof node !== "object") return null;
|
|
166
|
+
const memo = ctx.memo, path = ctx.path;
|
|
124
167
|
if (memo.has(node)) return memo.get(node);
|
|
125
|
-
if (reachesDefault(node, root, reach, new Set()) !== 1) { memo.set(node, null); return null; }
|
|
168
|
+
if (reachesDefault(node, ctx.root, ctx.reach, new Set(), ctx.res) !== 1) { memo.set(node, null); return null; }
|
|
126
169
|
let body = null;
|
|
127
170
|
// `path`: the schemas applied to the enclosing values, outermost first.
|
|
128
171
|
// `made`: inside a value the pass itself made from a default, the same as a
|
|
@@ -133,6 +176,16 @@ function compileNode(node, root, memo, reach, path) {
|
|
|
133
176
|
// schema, and Renovate's took this pass to the heap limit.
|
|
134
177
|
const fn = (data, depth, made) => {
|
|
135
178
|
if (depth > MAX_DEPTH) return;
|
|
179
|
+
// Only what is applied through a `$ref` or `allOf` is counted (fn.shared,
|
|
180
|
+
// set below), and only outside a made value, where every application
|
|
181
|
+
// does the same work.
|
|
182
|
+
if (fn.shared === true && made === null && typeof data === "object" && data !== null) {
|
|
183
|
+
const visits = ctx.visits || (ctx.visits = new Map());
|
|
184
|
+
let done = visits.get(data);
|
|
185
|
+
if (done === undefined) visits.set(data, (done = new Set()));
|
|
186
|
+
if (done.has(node)) return;
|
|
187
|
+
done.add(node);
|
|
188
|
+
}
|
|
136
189
|
if (made !== null) {
|
|
137
190
|
if (made.has(node)) return;
|
|
138
191
|
made.add(node);
|
|
@@ -154,31 +207,31 @@ function compileNode(node, root, memo, reach, path) {
|
|
|
154
207
|
const p = props[key];
|
|
155
208
|
if (!p || typeof p !== "object") continue;
|
|
156
209
|
if (p.default !== undefined) own.push([key, p.default]);
|
|
157
|
-
const child = compileNode(p,
|
|
210
|
+
const child = compileNode(p, ctx);
|
|
158
211
|
if (child !== null) below.push([key, child]);
|
|
159
212
|
}
|
|
160
213
|
}
|
|
161
214
|
// Arrays: every element under `items`, the elements `prefixItems` names by
|
|
162
215
|
// position (then `items` covers only the ones after them).
|
|
163
|
-
const tuple = Array.isArray(node.prefixItems) ? node.prefixItems.map((s) => compileNode(s,
|
|
164
|
-
const each = compileNode(node.items,
|
|
216
|
+
const tuple = Array.isArray(node.prefixItems) ? node.prefixItems.map((s) => compileNode(s, ctx)) : [];
|
|
217
|
+
const each = compileNode(node.items, ctx);
|
|
165
218
|
// Objects: keys a pattern matches, and the keys neither `properties` nor a
|
|
166
219
|
// pattern names; and a schema for each key present.
|
|
167
220
|
const patterns = [];
|
|
168
221
|
if (node.patternProperties && typeof node.patternProperties === "object") {
|
|
169
222
|
for (const src of Object.keys(node.patternProperties)) {
|
|
170
|
-
const re =
|
|
171
|
-
const t = compileNode(node.patternProperties[src],
|
|
223
|
+
const re = ctx.match(src);
|
|
224
|
+
const t = compileNode(node.patternProperties[src], ctx);
|
|
172
225
|
if (re === null) continue;
|
|
173
226
|
patterns.push([re, t]);
|
|
174
227
|
}
|
|
175
228
|
}
|
|
176
|
-
const extra = compileNode(node.additionalProperties,
|
|
229
|
+
const extra = compileNode(node.additionalProperties, ctx);
|
|
177
230
|
const declared = props && typeof props === "object" ? props : null;
|
|
178
231
|
const dependent = [];
|
|
179
232
|
if (node.dependentSchemas && typeof node.dependentSchemas === "object") {
|
|
180
233
|
for (const key of Object.keys(node.dependentSchemas)) {
|
|
181
|
-
const t = compileNode(node.dependentSchemas[key],
|
|
234
|
+
const t = compileNode(node.dependentSchemas[key], ctx);
|
|
182
235
|
if (t !== null) dependent.push([key, t]);
|
|
183
236
|
}
|
|
184
237
|
}
|
|
@@ -186,19 +239,22 @@ function compileNode(node, root, memo, reach, path) {
|
|
|
186
239
|
|
|
187
240
|
const also = [];
|
|
188
241
|
if (typeof node.$ref === "string") {
|
|
189
|
-
const t = compileNode(
|
|
190
|
-
if (t !== null) also.push(t);
|
|
242
|
+
const t = compileNode(refTarget(ctx.root, ctx.res, node), ctx);
|
|
243
|
+
if (t !== null) { t.shared = true; also.push(t); }
|
|
191
244
|
}
|
|
192
245
|
if (Array.isArray(node.allOf)) {
|
|
193
246
|
for (const s of node.allOf) {
|
|
194
|
-
const t = compileNode(s,
|
|
195
|
-
if (t !== null) also.push(t);
|
|
247
|
+
const t = compileNode(s, ctx);
|
|
248
|
+
if (t !== null) { t.shared = true; also.push(t); }
|
|
196
249
|
}
|
|
197
250
|
}
|
|
198
251
|
|
|
199
252
|
body = (data, depth, made) => {
|
|
253
|
+
// A value that is not an object has nothing to fill, here or in any
|
|
254
|
+
// schema applied to it.
|
|
255
|
+
if (typeof data !== "object" || data === null) return;
|
|
200
256
|
let fresh = null;
|
|
201
|
-
|
|
257
|
+
{
|
|
202
258
|
for (let i = 0; i < own.length; i++) {
|
|
203
259
|
const key = own[i][0];
|
|
204
260
|
if (!Object.hasOwn(data, key)) {
|
|
@@ -240,6 +296,28 @@ function compileNode(node, root, memo, reach, path) {
|
|
|
240
296
|
|
|
241
297
|
// The functions a module written ahead of time embeds as its applyDefaults
|
|
242
298
|
// where the generated pass declines a schema (lib/aot-impl.js).
|
|
243
|
-
const EMBED = ['buildDefaultsApplier', 'setOwn', 'localRef', 'reachesDefault', 'valuesOf', 'listOf', 'keyPattern', 'copyOf', 'compileNode'];
|
|
299
|
+
const EMBED = ['buildDefaultsApplier', 'setOwn', 'localRef', 'resourceRoots', 'refTarget', 'reachesDefault', 'valuesOf', 'listOf', 'keyPattern', 'copyOf', 'compileNode'];
|
|
300
|
+
|
|
301
|
+
// patternProperties keys tested in linear time: the linear-time engine
|
|
302
|
+
// (lib/safe-regex.js, which validation uses for these patterns too) for a
|
|
303
|
+
// pattern in its subset, the platform RegExp otherwise, as validation does.
|
|
304
|
+
// A key that makes a backtracking pattern run for seconds then costs no more
|
|
305
|
+
// here than it does to validate. lib/js-compiler.js also hands patterns the
|
|
306
|
+
// RegExp runs in linear time to the RegExp, for speed; that check is left
|
|
307
|
+
// out here so the closure pass, which lite carries, does not pull it in.
|
|
308
|
+
// Loaded on the first pattern; most schemas have none.
|
|
309
|
+
const PROP_ESCAPE = /\\[pP]\{/;
|
|
310
|
+
function safeKeyMatcher(src) {
|
|
311
|
+
if (!PROP_ESCAPE.test(src)) {
|
|
312
|
+
try {
|
|
313
|
+
const safe = require("./safe-regex");
|
|
314
|
+
if (safe.patternIsSafe(src)) return safe.compileSafe(src);
|
|
315
|
+
} catch {}
|
|
316
|
+
}
|
|
317
|
+
return keyPattern(src);
|
|
318
|
+
}
|
|
244
319
|
|
|
245
|
-
module.exports = {
|
|
320
|
+
module.exports = {
|
|
321
|
+
buildDefaultsApplier: (schema) => buildDefaultsApplier(schema, safeKeyMatcher),
|
|
322
|
+
setOwn, localRef, resourceRoots, refTarget, safeKeyMatcher, MAX_DEPTH, EMBED,
|
|
323
|
+
};
|
package/lib/describe-schema.js
CHANGED
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
// $defs. Anything else is described as `any` rather than guessed at.
|
|
27
27
|
|
|
28
28
|
const MAX_DEPTH = 12;
|
|
29
|
+
// anyOf branches that each lead to the same next definition describe
|
|
30
|
+
// width^layers lines, 16 million for 4 wide and 12 deep. A prompt has no use
|
|
31
|
+
// for more than this; past it the description stops and says so.
|
|
32
|
+
const MAX_LINES = 2000;
|
|
29
33
|
|
|
30
34
|
function lit (v) {
|
|
31
35
|
try { return JSON.stringify(v); } catch (_) { return String(v); }
|
|
@@ -95,6 +99,7 @@ function describeObjectBody (schema, depth, defs, lines) {
|
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
function describeNode (schema, label, depth, defs, lines) {
|
|
102
|
+
if (lines.length >= MAX_LINES) return;
|
|
98
103
|
const pad = ' '.repeat(depth);
|
|
99
104
|
if (schema === true || schema === undefined) { lines.push(`${pad}${label}: any`); return; }
|
|
100
105
|
if (schema === false) { lines.push(`${pad}${label}: nothing is allowed here`); return; }
|
|
@@ -103,19 +108,43 @@ function describeNode (schema, label, depth, defs, lines) {
|
|
|
103
108
|
return;
|
|
104
109
|
}
|
|
105
110
|
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
// A chain of references is followed here, each definition once: one that
|
|
112
|
+
// leads back to itself without a property in between describes no value.
|
|
113
|
+
let target = schema.$ref ? resolveRef(schema, defs) : null;
|
|
114
|
+
if (target) {
|
|
115
|
+
const chain = new Set([schema]);
|
|
116
|
+
while (target && typeof target === 'object' && target.$ref && !chain.has(target)) {
|
|
117
|
+
chain.add(target);
|
|
118
|
+
const next = resolveRef(target, defs);
|
|
119
|
+
if (!next) break;
|
|
120
|
+
target = next;
|
|
121
|
+
}
|
|
122
|
+
if (chain.has(target)) { lines.push(`${pad}${label}: any`); return; }
|
|
123
|
+
describeNode(target, label, depth, defs, lines);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
108
126
|
|
|
109
|
-
// allOf is the intersection, so its parts describe the same value.
|
|
127
|
+
// allOf is the intersection, so its parts describe the same value. The
|
|
128
|
+
// parts' own allOf are merged in the same pass, each schema once: a part
|
|
129
|
+
// that led back to this schema copied its allOf back in and the merge
|
|
130
|
+
// recursed until the stack ran out.
|
|
110
131
|
if (Array.isArray(schema.allOf) && schema.allOf.length) {
|
|
111
132
|
const merged = Object.assign({}, schema);
|
|
112
133
|
delete merged.allOf;
|
|
113
|
-
|
|
134
|
+
const seen = new Set([schema]);
|
|
135
|
+
const queue = schema.allOf.slice();
|
|
136
|
+
while (queue.length) {
|
|
137
|
+
const part = queue.shift();
|
|
114
138
|
const resolved = part && part.$ref ? resolveRef(part, defs) || part : part;
|
|
115
|
-
if (resolved
|
|
139
|
+
if (!resolved || typeof resolved !== 'object' || seen.has(resolved)) continue;
|
|
140
|
+
seen.add(resolved);
|
|
141
|
+
if (Array.isArray(resolved.allOf)) queue.push(...resolved.allOf);
|
|
142
|
+
Object.assign(merged, resolved, {
|
|
116
143
|
properties: Object.assign({}, merged.properties, resolved.properties),
|
|
117
144
|
required: [].concat(merged.required || [], resolved.required || []),
|
|
118
145
|
});
|
|
146
|
+
delete merged.allOf;
|
|
147
|
+
if (resolved.$ref !== undefined && resolved !== part) delete merged.$ref;
|
|
119
148
|
}
|
|
120
149
|
if (!merged.properties) delete merged.properties;
|
|
121
150
|
if (!merged.required || !merged.required.length) delete merged.required;
|
|
@@ -170,6 +199,10 @@ function describeSchema (schema, opts) {
|
|
|
170
199
|
const defs = (schema && (schema.$defs || schema.definitions)) || null;
|
|
171
200
|
const lines = [];
|
|
172
201
|
describeNode(schema, name, 0, defs, lines);
|
|
202
|
+
if (lines.length >= MAX_LINES) {
|
|
203
|
+
lines.length = MAX_LINES;
|
|
204
|
+
lines.push(`(description cut at ${MAX_LINES} lines)`);
|
|
205
|
+
}
|
|
173
206
|
return lines.join('\n');
|
|
174
207
|
}
|
|
175
208
|
|