ata-validator 0.12.2 → 0.12.3
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/index.d.ts +11 -0
- package/index.js +87 -13
- package/lib/js-compiler.js +15 -3
- package/package.json +1 -1
package/index.d.ts
CHANGED
|
@@ -81,6 +81,17 @@ export class Validator {
|
|
|
81
81
|
/** Generate a standalone JS module string for zero-compile loading. Returns null if schema can't be standalone-compiled. */
|
|
82
82
|
toStandalone(): string | null;
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Generate a self-contained module string with `validate`/`isValid` exports.
|
|
86
|
+
* The output has zero runtime dependency on ata-validator.
|
|
87
|
+
*
|
|
88
|
+
* - format: 'esm' (default) or 'cjs'.
|
|
89
|
+
* - abortEarly: if true, invalid results are a shared frozen stub (smaller output, no error details).
|
|
90
|
+
*
|
|
91
|
+
* Returns null if the schema cannot be compiled to a standalone module.
|
|
92
|
+
*/
|
|
93
|
+
toStandaloneModule(options?: { format?: 'esm' | 'cjs'; abortEarly?: boolean }): string | null;
|
|
94
|
+
|
|
84
95
|
/** Load a pre-compiled standalone module. Zero schema compilation at startup. */
|
|
85
96
|
static fromStandalone(mod: StandaloneModule, schema: object | string, options?: ValidatorOptions): Validator;
|
|
86
97
|
|
package/index.js
CHANGED
|
@@ -290,14 +290,41 @@ const _CP_LEN_SOURCE = `function _cpLen(s) {
|
|
|
290
290
|
// (which must materialize the full JS object tree). Buffer.from + NAPI ~2x faster.
|
|
291
291
|
const SIMDJSON_THRESHOLD = 8192;
|
|
292
292
|
|
|
293
|
+
// Resolve a JSON Schema path like "#/properties/name/type" to the schema object
|
|
294
|
+
// that *contains* the failing keyword. Used by verbose mode to populate
|
|
295
|
+
// `parentSchema` on validation errors. Returns undefined if the path can't be
|
|
296
|
+
// walked (malformed pointer or missing intermediate node).
|
|
297
|
+
function resolveSchemaByPath(rootSchema, schemaPath) {
|
|
298
|
+
if (!schemaPath || typeof schemaPath !== 'string' || !schemaPath.startsWith('#')) {
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
const stripped = schemaPath.slice(1);
|
|
302
|
+
if (!stripped || stripped === '/') return rootSchema;
|
|
303
|
+
const parts = stripped.split('/').filter(Boolean).map(s => s.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
304
|
+
// The last segment is the keyword that failed (e.g. "type"); parentSchema is
|
|
305
|
+
// the schema object that owns that keyword, so walk all but the last segment.
|
|
306
|
+
let target = rootSchema;
|
|
307
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
308
|
+
if (target == null || typeof target !== 'object') return undefined;
|
|
309
|
+
target = target[parts[i]];
|
|
310
|
+
}
|
|
311
|
+
return target;
|
|
312
|
+
}
|
|
313
|
+
|
|
293
314
|
function parsePointerPath(path) {
|
|
294
315
|
if (!path) return [];
|
|
295
316
|
return path
|
|
296
317
|
.split("/")
|
|
297
318
|
.filter(Boolean)
|
|
298
|
-
.map((seg) =>
|
|
299
|
-
|
|
300
|
-
|
|
319
|
+
.map((seg) => {
|
|
320
|
+
const decoded = seg.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
321
|
+
// Per Standard Schema V1: array indices should be emitted as numbers,
|
|
322
|
+
// object keys as strings. Treat all-digit segments as numeric indices.
|
|
323
|
+
if (/^(0|[1-9][0-9]*)$/.test(decoded)) {
|
|
324
|
+
return { key: Number(decoded) };
|
|
325
|
+
}
|
|
326
|
+
return { key: decoded };
|
|
327
|
+
});
|
|
301
328
|
}
|
|
302
329
|
|
|
303
330
|
function createPaddedBuffer(jsonStr) {
|
|
@@ -366,6 +393,15 @@ class Validator {
|
|
|
366
393
|
// Schema map for cross-schema $ref resolution
|
|
367
394
|
this._schemaMap = buildSchemaMap(options.schemas) || new Map();
|
|
368
395
|
|
|
396
|
+
// User-supplied format checkers: { formatName: (value) => boolean }.
|
|
397
|
+
// Looked up at runtime when a schema references a format the built-in
|
|
398
|
+
// registry does not know about.
|
|
399
|
+
this._userFormats = options.formats || null;
|
|
400
|
+
|
|
401
|
+
// Verbose mode: when on, errors carry parentSchema (the schema object that
|
|
402
|
+
// produced the error). Matches ajv's `verbose: true` behavior.
|
|
403
|
+
this._verbose = !!options.verbose;
|
|
404
|
+
|
|
369
405
|
// Lazy stubs: trigger compilation on first call, then re-dispatch
|
|
370
406
|
this.validate = (data) => {
|
|
371
407
|
this._ensureCompiled();
|
|
@@ -464,7 +500,9 @@ class Validator {
|
|
|
464
500
|
const mapKey = this._schemaMap.size > 0
|
|
465
501
|
? this._schemaStr + '\0' + [...this._schemaMap.keys()].sort().join('\0')
|
|
466
502
|
: this._schemaStr;
|
|
467
|
-
|
|
503
|
+
// Custom formats are JS functions: bypass the compile cache since they can
|
|
504
|
+
// differ between validators that share the same schema string.
|
|
505
|
+
const cached = this._userFormats ? null : _compileCache.get(mapKey);
|
|
468
506
|
let jsFn, jsCombinedFn, jsErrFn, _isCodegen = false;
|
|
469
507
|
var _forceNapi = typeof process !== 'undefined' && process.env && process.env.ATA_FORCE_NAPI;
|
|
470
508
|
if (cached && !_forceNapi) {
|
|
@@ -473,12 +511,15 @@ class Validator {
|
|
|
473
511
|
jsErrFn = cached.errFn;
|
|
474
512
|
_isCodegen = !!cached.isCodegen;
|
|
475
513
|
} else if (!_forceNapi) {
|
|
476
|
-
const
|
|
514
|
+
const uf = this._userFormats;
|
|
515
|
+
const _cgFn = compileToJSCodegen(schemaObj, sm, uf);
|
|
477
516
|
jsFn = _cgFn || compileToJS(schemaObj, null, sm);
|
|
478
|
-
jsCombinedFn = compileToJSCombined(schemaObj, VALID_RESULT, sm);
|
|
479
|
-
jsErrFn = compileToJSCodegenWithErrors(schemaObj, sm);
|
|
517
|
+
jsCombinedFn = compileToJSCombined(schemaObj, VALID_RESULT, sm, uf);
|
|
518
|
+
jsErrFn = compileToJSCodegenWithErrors(schemaObj, sm, uf);
|
|
480
519
|
_isCodegen = !!_cgFn;
|
|
481
|
-
|
|
520
|
+
if (!uf) {
|
|
521
|
+
_compileCache.set(mapKey, { jsFn, combined: jsCombinedFn, errFn: jsErrFn, isCodegen: _isCodegen });
|
|
522
|
+
}
|
|
482
523
|
} else {
|
|
483
524
|
jsFn = null; jsCombinedFn = null; jsErrFn = null;
|
|
484
525
|
}
|
|
@@ -617,6 +658,24 @@ class Validator {
|
|
|
617
658
|
}
|
|
618
659
|
: (data) => (jsFn(data) ? VALID_RESULT : errFn(data));
|
|
619
660
|
}
|
|
661
|
+
// Verbose mode: populate parentSchema on each error.
|
|
662
|
+
// Errors may be frozen, so clone them with the extra field.
|
|
663
|
+
if (this._verbose) {
|
|
664
|
+
const inner = this.validate;
|
|
665
|
+
const root = this._schemaObj;
|
|
666
|
+
this.validate = (data) => {
|
|
667
|
+
const result = inner(data);
|
|
668
|
+
if (result && !result.valid && result.errors) {
|
|
669
|
+
const enriched = result.errors.map((err) =>
|
|
670
|
+
err && err.parentSchema === undefined
|
|
671
|
+
? { ...err, parentSchema: resolveSchemaByPath(root, err.schemaPath) }
|
|
672
|
+
: err
|
|
673
|
+
);
|
|
674
|
+
return { valid: false, errors: enriched };
|
|
675
|
+
}
|
|
676
|
+
return result;
|
|
677
|
+
};
|
|
678
|
+
}
|
|
620
679
|
this.isValidObject = jsFn;
|
|
621
680
|
const hybridFn = jsFn._hybridFactory
|
|
622
681
|
? jsFn._hybridFactory(VALID_RESULT, errFn)
|
|
@@ -821,19 +880,24 @@ class Validator {
|
|
|
821
880
|
const mapKey = this._schemaMap.size > 0
|
|
822
881
|
? this._schemaStr + '\0' + [...this._schemaMap.keys()].sort().join('\0')
|
|
823
882
|
: this._schemaStr;
|
|
824
|
-
|
|
883
|
+
// Custom formats are JS functions: skip the shared cache so different
|
|
884
|
+
// validators with the same schema string but different formats don't collide.
|
|
885
|
+
const cached = this._userFormats ? null : _compileCache.get(mapKey);
|
|
825
886
|
if (cached && cached.jsFn) {
|
|
826
887
|
this._jsFn = cached.jsFn;
|
|
827
888
|
this.isValidObject = cached.jsFn;
|
|
828
889
|
return;
|
|
829
890
|
}
|
|
830
|
-
const
|
|
891
|
+
const uf = this._userFormats;
|
|
892
|
+
const jsFn = compileToJSCodegen(this._schemaObj, sm, uf) || compileToJS(this._schemaObj, null, sm);
|
|
831
893
|
this._jsFn = jsFn;
|
|
832
894
|
if (jsFn) {
|
|
833
895
|
this.isValidObject = jsFn;
|
|
834
896
|
// seed cache with codegen, combined/errFn filled later by _ensureCompiled
|
|
835
|
-
if (!
|
|
836
|
-
|
|
897
|
+
if (!uf) {
|
|
898
|
+
if (!cached) _compileCache.set(mapKey, { jsFn, combined: null, errFn: null });
|
|
899
|
+
else cached.jsFn = jsFn;
|
|
900
|
+
}
|
|
837
901
|
}
|
|
838
902
|
}
|
|
839
903
|
|
|
@@ -1122,15 +1186,21 @@ Validator.bundle = function (schemas, opts) {
|
|
|
1122
1186
|
};
|
|
1123
1187
|
|
|
1124
1188
|
// Zero-dependency self-contained bundle — no require('ata-validator') needed at runtime.
|
|
1189
|
+
// opts.format: 'cjs' (default) or 'esm'.
|
|
1125
1190
|
Validator.bundleStandalone = function (schemas, opts) {
|
|
1191
|
+
// Cross-schema $ref resolution: every Validator in the bundle needs to know
|
|
1192
|
+
// about the others so $ref to a sibling $id can resolve at compile time.
|
|
1193
|
+
const bundleOpts = { ...(opts || {}), schemas };
|
|
1194
|
+
const format = (opts && opts.format) || 'cjs';
|
|
1126
1195
|
const R = "Object.freeze({valid:true,errors:Object.freeze([])})";
|
|
1127
1196
|
const fns = schemas.map((schema) => {
|
|
1128
|
-
const v = new Validator(schema,
|
|
1197
|
+
const v = new Validator(schema, bundleOpts);
|
|
1129
1198
|
v._ensureCompiled();
|
|
1130
1199
|
const jsFn = v._jsFn;
|
|
1131
1200
|
if (!jsFn || !jsFn._hybridSource) return "null";
|
|
1132
1201
|
const jsErrFn = compileToJSCodegenWithErrors(
|
|
1133
1202
|
typeof schema === "string" ? JSON.parse(schema) : schema,
|
|
1203
|
+
v._schemaMap,
|
|
1134
1204
|
);
|
|
1135
1205
|
const errBody =
|
|
1136
1206
|
jsErrFn && jsErrFn._errSource
|
|
@@ -1138,6 +1208,10 @@ Validator.bundleStandalone = function (schemas, opts) {
|
|
|
1138
1208
|
: "return{valid:false,errors:[{code:'error',path:'',message:'validation failed'}]}";
|
|
1139
1209
|
return `(function(R){var E=function(d){var _all=true;${errBody}};return function(d){${jsFn._hybridSource}}})(R)`;
|
|
1140
1210
|
});
|
|
1211
|
+
const arr = `[${fns.join(",")}]`;
|
|
1212
|
+
if (format === 'esm') {
|
|
1213
|
+
return `// Auto-generated by ata-validator — do not edit\nconst R=${R};\nconst validators=${arr};\nexport default validators;\nexport { validators };\n`;
|
|
1214
|
+
}
|
|
1141
1215
|
return `'use strict';\nvar R=${R};\nmodule.exports=[${fns.join(",")}];\n`;
|
|
1142
1216
|
};
|
|
1143
1217
|
|
package/lib/js-compiler.js
CHANGED
|
@@ -768,7 +768,7 @@ function hasAdditionalPropertiesSchema(schema) {
|
|
|
768
768
|
|
|
769
769
|
// --- Codegen mode: generates a single Function (NOT CSP-safe) ---
|
|
770
770
|
// This matches ajv's approach: one monolithic function, V8 JIT fully inlines it
|
|
771
|
-
function compileToJSCodegen(schema, schemaMap) {
|
|
771
|
+
function compileToJSCodegen(schema, schemaMap, userFormats) {
|
|
772
772
|
if (typeof schema === 'boolean') return schema ? () => true : () => false
|
|
773
773
|
if (typeof schema !== 'object' || schema === null) return null
|
|
774
774
|
|
|
@@ -827,7 +827,7 @@ function compileToJSCodegen(schema, schemaMap) {
|
|
|
827
827
|
}
|
|
828
828
|
}
|
|
829
829
|
|
|
830
|
-
const ctx = { varCounter: 0, helpers: [], helperCode: [], preamble: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: schema }
|
|
830
|
+
const ctx = { varCounter: 0, helpers: [], helperCode: [], preamble: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: schema, userFormats: userFormats || null }
|
|
831
831
|
const lines = []
|
|
832
832
|
genCode(schema, 'd', lines, ctx)
|
|
833
833
|
|
|
@@ -1320,7 +1320,19 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1320
1320
|
|
|
1321
1321
|
if (schema.format) {
|
|
1322
1322
|
const fc = FORMAT_CODEGEN[schema.format]
|
|
1323
|
-
if (fc)
|
|
1323
|
+
if (fc) {
|
|
1324
|
+
lines.push(fc(v, isStr))
|
|
1325
|
+
} else if (ctx.userFormats && typeof ctx.userFormats[schema.format] === 'function') {
|
|
1326
|
+
// User-supplied format checker: thread the function via closure and call at runtime.
|
|
1327
|
+
const safeName = schema.format.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
1328
|
+
const closureName = `_uf_${safeName}`
|
|
1329
|
+
if (!ctx.closureVars.includes(closureName)) {
|
|
1330
|
+
ctx.closureVars.push(closureName)
|
|
1331
|
+
ctx.closureVals.push(ctx.userFormats[schema.format])
|
|
1332
|
+
}
|
|
1333
|
+
const guard = isStr ? '' : `typeof ${v}==='string'&&`
|
|
1334
|
+
lines.push(`if(${guard}!${closureName}(${v}))return false`)
|
|
1335
|
+
}
|
|
1324
1336
|
}
|
|
1325
1337
|
|
|
1326
1338
|
// uniqueItems — tiered strategy based on expected array size
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.3",
|
|
4
4
|
"description": "Ultra-fast JSON Schema validator. 5x faster validation, 159,000x faster compilation. Works without native addon. Cross-schema $ref, Draft 2020-12 + Draft 7, V8-optimized JS codegen, simdjson, RE2, multi-core. Standard Schema V1 compatible.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "index.mjs",
|