ata-validator 0.9.3 → 0.10.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/binding/ata_napi.cpp +174 -1
- package/index.js +16 -0
- package/lib/shape-classifier.js +96 -0
- package/lib/tier0.js +203 -0
- package/package.json +1 -1
- package/prebuilds/ata-darwin-arm64/node-napi-v10.node +0 -0
- package/prebuilds/darwin-arm64/ata-validator.node +0 -0
- package/prebuilds/ata-linux-arm64/node-napi-v10.node +0 -0
- package/prebuilds/ata-linux-arm64-musl/node-napi-v10.node +0 -0
- package/prebuilds/ata-linux-x64/node-napi-v10.node +0 -0
- package/prebuilds/ata-linux-x64-musl/node-napi-v10.node +0 -0
- package/prebuilds/ata-win32-x64/node-napi-v10.node +0 -0
package/binding/ata_napi.cpp
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
#include <vector>
|
|
18
18
|
|
|
19
19
|
#include "ata.h"
|
|
20
|
+
#include <simdjson.h>
|
|
20
21
|
|
|
21
22
|
// ============================================================================
|
|
22
23
|
// V8 Direct Object Traversal Engine
|
|
@@ -797,6 +798,67 @@ static void validate_napi(const schema_node_ptr& node,
|
|
|
797
798
|
// N-API Binding
|
|
798
799
|
// ============================================================================
|
|
799
800
|
|
|
801
|
+
// ============================================================================
|
|
802
|
+
// simdjson DOM to V8 JS Object conversion
|
|
803
|
+
// ============================================================================
|
|
804
|
+
|
|
805
|
+
static Napi::Value dom_to_napi(Napi::Env env, simdjson::dom::element el) {
|
|
806
|
+
using namespace simdjson;
|
|
807
|
+
switch (el.type()) {
|
|
808
|
+
case dom::element_type::OBJECT: {
|
|
809
|
+
auto obj = Napi::Object::New(env);
|
|
810
|
+
for (auto [key, val] : dom::object(el)) {
|
|
811
|
+
obj.Set(std::string(key), dom_to_napi(env, val));
|
|
812
|
+
}
|
|
813
|
+
return obj;
|
|
814
|
+
}
|
|
815
|
+
case dom::element_type::ARRAY: {
|
|
816
|
+
dom::array arr = el;
|
|
817
|
+
auto jsArr = Napi::Array::New(env, arr.size());
|
|
818
|
+
uint32_t i = 0;
|
|
819
|
+
for (auto val : arr) {
|
|
820
|
+
jsArr.Set(i++, dom_to_napi(env, val));
|
|
821
|
+
}
|
|
822
|
+
return jsArr;
|
|
823
|
+
}
|
|
824
|
+
case dom::element_type::STRING: {
|
|
825
|
+
std::string_view sv;
|
|
826
|
+
el.get(sv);
|
|
827
|
+
return Napi::String::New(env, sv.data(), sv.length());
|
|
828
|
+
}
|
|
829
|
+
case dom::element_type::INT64: {
|
|
830
|
+
int64_t v;
|
|
831
|
+
el.get(v);
|
|
832
|
+
return Napi::Number::New(env, static_cast<double>(v));
|
|
833
|
+
}
|
|
834
|
+
case dom::element_type::UINT64: {
|
|
835
|
+
uint64_t v;
|
|
836
|
+
el.get(v);
|
|
837
|
+
return Napi::Number::New(env, static_cast<double>(v));
|
|
838
|
+
}
|
|
839
|
+
case dom::element_type::DOUBLE: {
|
|
840
|
+
double v;
|
|
841
|
+
el.get(v);
|
|
842
|
+
return Napi::Number::New(env, v);
|
|
843
|
+
}
|
|
844
|
+
case dom::element_type::BOOL: {
|
|
845
|
+
bool v;
|
|
846
|
+
el.get(v);
|
|
847
|
+
return Napi::Boolean::New(env, v);
|
|
848
|
+
}
|
|
849
|
+
case dom::element_type::NULL_VALUE:
|
|
850
|
+
return env.Null();
|
|
851
|
+
default:
|
|
852
|
+
return env.Undefined();
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Thread-local simdjson DOM parser for parseJSON / validateAndParse
|
|
857
|
+
static simdjson::dom::parser& tl_dom_parser() {
|
|
858
|
+
thread_local simdjson::dom::parser parser;
|
|
859
|
+
return parser;
|
|
860
|
+
}
|
|
861
|
+
|
|
800
862
|
static Napi::Object make_result(Napi::Env env,
|
|
801
863
|
const ata::validation_result& result) {
|
|
802
864
|
Napi::Object obj = Napi::Object::New(env);
|
|
@@ -822,7 +884,8 @@ class CompiledSchema : public Napi::ObjectWrap<CompiledSchema> {
|
|
|
822
884
|
{InstanceMethod("validate", &CompiledSchema::Validate),
|
|
823
885
|
InstanceMethod("validateJSON", &CompiledSchema::ValidateJSON),
|
|
824
886
|
InstanceMethod("validateDirect", &CompiledSchema::ValidateDirect),
|
|
825
|
-
InstanceMethod("isValidJSON", &CompiledSchema::IsValidJSON)
|
|
887
|
+
InstanceMethod("isValidJSON", &CompiledSchema::IsValidJSON),
|
|
888
|
+
InstanceMethod("validateAndParse", &CompiledSchema::ValidateAndParse)});
|
|
826
889
|
auto* constructor = new Napi::FunctionReference();
|
|
827
890
|
*constructor = Napi::Persistent(func);
|
|
828
891
|
env.SetInstanceData(constructor);
|
|
@@ -944,6 +1007,77 @@ class CompiledSchema : public Napi::ObjectWrap<CompiledSchema> {
|
|
|
944
1007
|
return ValidateDirectImpl(env, info[0]);
|
|
945
1008
|
}
|
|
946
1009
|
|
|
1010
|
+
// Parse JSON with simdjson, validate against schema, return parsed JS object
|
|
1011
|
+
Napi::Value ValidateAndParse(const Napi::CallbackInfo& info) {
|
|
1012
|
+
Napi::Env env = info.Env();
|
|
1013
|
+
if (info.Length() < 1) {
|
|
1014
|
+
Napi::TypeError::New(env, "JSON string or Buffer expected")
|
|
1015
|
+
.ThrowAsJavaScriptException();
|
|
1016
|
+
return env.Undefined();
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const char* data;
|
|
1020
|
+
size_t len;
|
|
1021
|
+
|
|
1022
|
+
if (info[0].IsBuffer()) {
|
|
1023
|
+
auto buf = info[0].As<Napi::Buffer<char>>();
|
|
1024
|
+
data = buf.Data();
|
|
1025
|
+
len = buf.Length();
|
|
1026
|
+
} else if (info[0].IsString()) {
|
|
1027
|
+
auto [d, l] = extract_string(env, info[0]);
|
|
1028
|
+
data = d;
|
|
1029
|
+
len = l;
|
|
1030
|
+
} else {
|
|
1031
|
+
Napi::TypeError::New(env, "JSON string or Buffer expected")
|
|
1032
|
+
.ThrowAsJavaScriptException();
|
|
1033
|
+
return env.Undefined();
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// Parse with simdjson
|
|
1037
|
+
simdjson::padded_string padded(data, len);
|
|
1038
|
+
auto& parser = tl_dom_parser();
|
|
1039
|
+
auto doc_result = parser.parse(padded);
|
|
1040
|
+
if (doc_result.error()) {
|
|
1041
|
+
auto obj = Napi::Object::New(env);
|
|
1042
|
+
obj.Set("valid", false);
|
|
1043
|
+
obj.Set("value", env.Null());
|
|
1044
|
+
auto errors = Napi::Array::New(env, 1);
|
|
1045
|
+
auto err = Napi::Object::New(env);
|
|
1046
|
+
err.Set("code", Napi::Number::New(env, static_cast<int>(ata::error_code::invalid_json)));
|
|
1047
|
+
err.Set("path", Napi::String::New(env, ""));
|
|
1048
|
+
err.Set("message", Napi::String::New(env, "Invalid JSON"));
|
|
1049
|
+
errors[0u] = err;
|
|
1050
|
+
obj.Set("errors", errors);
|
|
1051
|
+
return obj;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// Validate
|
|
1055
|
+
auto valResult = ata::validate(schema_, std::string_view(data, len));
|
|
1056
|
+
|
|
1057
|
+
// Convert DOM to JS object
|
|
1058
|
+
Napi::Value jsValue = dom_to_napi(env, doc_result.value());
|
|
1059
|
+
|
|
1060
|
+
// Build result
|
|
1061
|
+
auto obj = Napi::Object::New(env);
|
|
1062
|
+
obj.Set("valid", valResult.valid);
|
|
1063
|
+
obj.Set("value", jsValue);
|
|
1064
|
+
if (valResult.valid) {
|
|
1065
|
+
obj.Set("errors", Napi::Array::New(env, 0));
|
|
1066
|
+
} else {
|
|
1067
|
+
Napi::Array errors = Napi::Array::New(env, valResult.errors.size());
|
|
1068
|
+
for (size_t i = 0; i < valResult.errors.size(); ++i) {
|
|
1069
|
+
Napi::Object err = Napi::Object::New(env);
|
|
1070
|
+
err.Set("code",
|
|
1071
|
+
Napi::Number::New(env, static_cast<int>(valResult.errors[i].code)));
|
|
1072
|
+
err.Set("path", Napi::String::New(env, valResult.errors[i].path));
|
|
1073
|
+
err.Set("message", Napi::String::New(env, valResult.errors[i].message));
|
|
1074
|
+
errors[i] = err;
|
|
1075
|
+
}
|
|
1076
|
+
obj.Set("errors", errors);
|
|
1077
|
+
}
|
|
1078
|
+
return obj;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
947
1081
|
private:
|
|
948
1082
|
Napi::Value ValidateDirectImpl(Napi::Env env, Napi::Value value) {
|
|
949
1083
|
compiled_schema_internal ctx;
|
|
@@ -996,6 +1130,44 @@ Napi::Value GetVersion(const Napi::CallbackInfo& info) {
|
|
|
996
1130
|
return Napi::String::New(info.Env(), std::string(ata::version()));
|
|
997
1131
|
}
|
|
998
1132
|
|
|
1133
|
+
// Standalone JSON parser using simdjson — returns parsed JS object
|
|
1134
|
+
Napi::Value ParseJSON(const Napi::CallbackInfo& info) {
|
|
1135
|
+
Napi::Env env = info.Env();
|
|
1136
|
+
if (info.Length() < 1) {
|
|
1137
|
+
Napi::TypeError::New(env, "JSON string or Buffer expected")
|
|
1138
|
+
.ThrowAsJavaScriptException();
|
|
1139
|
+
return env.Undefined();
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
const char* data;
|
|
1143
|
+
size_t len;
|
|
1144
|
+
|
|
1145
|
+
if (info[0].IsBuffer()) {
|
|
1146
|
+
auto buf = info[0].As<Napi::Buffer<char>>();
|
|
1147
|
+
data = buf.Data();
|
|
1148
|
+
len = buf.Length();
|
|
1149
|
+
} else if (info[0].IsString()) {
|
|
1150
|
+
auto [d, l] = CompiledSchema::extract_string(env, info[0]);
|
|
1151
|
+
data = d;
|
|
1152
|
+
len = l;
|
|
1153
|
+
} else {
|
|
1154
|
+
Napi::TypeError::New(env, "JSON string or Buffer expected")
|
|
1155
|
+
.ThrowAsJavaScriptException();
|
|
1156
|
+
return env.Undefined();
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Parse with simdjson using thread-local parser
|
|
1160
|
+
simdjson::padded_string padded(data, len);
|
|
1161
|
+
auto& parser = tl_dom_parser();
|
|
1162
|
+
auto result = parser.parse(padded);
|
|
1163
|
+
if (result.error()) {
|
|
1164
|
+
Napi::Error::New(env, "Invalid JSON").ThrowAsJavaScriptException();
|
|
1165
|
+
return env.Undefined();
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
return dom_to_napi(env, result.value());
|
|
1169
|
+
}
|
|
1170
|
+
|
|
999
1171
|
// --- Thread Pool ---
|
|
1000
1172
|
class ThreadPool {
|
|
1001
1173
|
public:
|
|
@@ -1531,6 +1703,7 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
1531
1703
|
CompiledSchema::Init(env, exports);
|
|
1532
1704
|
exports.Set("validate", Napi::Function::New(env, ValidateOneShot));
|
|
1533
1705
|
exports.Set("version", Napi::Function::New(env, GetVersion));
|
|
1706
|
+
exports.Set("parseJSON", Napi::Function::New(env, ParseJSON));
|
|
1534
1707
|
exports.Set("fastRegister", Napi::Function::New(env, FastRegister));
|
|
1535
1708
|
exports.Set("fastValidate", Napi::Function::New(env, FastValidateSlow));
|
|
1536
1709
|
|
package/index.js
CHANGED
|
@@ -9,6 +9,8 @@ const {
|
|
|
9
9
|
compileToJSCombined,
|
|
10
10
|
} = require("./lib/js-compiler");
|
|
11
11
|
const { normalizeDraft7 } = require("./lib/draft7");
|
|
12
|
+
const { classify } = require("./lib/shape-classifier");
|
|
13
|
+
const { buildTier0Plan, tier0Validate } = require("./lib/tier0");
|
|
12
14
|
|
|
13
15
|
// Extract default values from a schema tree. Returns a function that applies
|
|
14
16
|
// defaults to an object in-place (mutates), or null if no defaults exist.
|
|
@@ -413,6 +415,20 @@ class Validator {
|
|
|
413
415
|
enumerable: false,
|
|
414
416
|
configurable: false,
|
|
415
417
|
});
|
|
418
|
+
|
|
419
|
+
// Tier 0 fast path: override isValidObject with a direct bound validator.
|
|
420
|
+
// All other methods (validate, validateJSON, etc.) stay on the lazy stubs above.
|
|
421
|
+
// Tier 0/1 are boolean-only; error paths continue through codegen.
|
|
422
|
+
const _tier = classify(schemaObj);
|
|
423
|
+
if (_tier.tier === 0) {
|
|
424
|
+
const _plan = buildTier0Plan(schemaObj);
|
|
425
|
+
this.isValidObject = (data) => tier0Validate(_plan, data);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Populate identity cache so repeated `new Validator(sameSchema)` short-circuits.
|
|
429
|
+
if (!opts && typeof schema === "object" && schema !== null) {
|
|
430
|
+
_identityCache.set(schema, this);
|
|
431
|
+
}
|
|
416
432
|
}
|
|
417
433
|
|
|
418
434
|
_ensureCompiled() {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Classifies a JSON Schema into one of three execution tiers:
|
|
4
|
+
// 0 - simple object or top-level primitive, fast-path validator
|
|
5
|
+
// 1 - nested objects/arrays, no composition, generic interpreter
|
|
6
|
+
// 2 - composition, $ref, dynamic, etc. -> existing codegen
|
|
7
|
+
//
|
|
8
|
+
// Tier 0/1 validators are BOOLEAN only. Error-returning paths stay on codegen.
|
|
9
|
+
|
|
10
|
+
const PRIMITIVE_TYPES = new Set(['string', 'number', 'integer', 'boolean']);
|
|
11
|
+
|
|
12
|
+
// Meta keywords that are always safe to see at any node (annotations, no validation impact)
|
|
13
|
+
const META_KEYS = new Set([
|
|
14
|
+
'$schema', '$id', '$comment',
|
|
15
|
+
'title', 'description', 'default', 'examples', 'deprecated', 'readOnly', 'writeOnly',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const TIER0_OBJECT_ALLOWED = new Set([
|
|
19
|
+
'type', 'properties', 'required', 'additionalProperties',
|
|
20
|
+
...META_KEYS,
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const TIER0_PRIMITIVE_ALLOWED = new Set([
|
|
24
|
+
'type', 'enum', 'const',
|
|
25
|
+
'minLength', 'maxLength',
|
|
26
|
+
'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum',
|
|
27
|
+
'multipleOf', 'format',
|
|
28
|
+
...META_KEYS,
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const MAX_TIER0_PROPS = 10;
|
|
32
|
+
const MAX_TIER0_ENUM = 256;
|
|
33
|
+
|
|
34
|
+
function isPrimitiveType(t) {
|
|
35
|
+
return typeof t === 'string' && PRIMITIVE_TYPES.has(t);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isPrimitiveEnumValue(v) {
|
|
39
|
+
const t = typeof v;
|
|
40
|
+
return v === null || t === 'string' || t === 'number' || t === 'boolean';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isTier0Primitive(schema) {
|
|
44
|
+
if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) return false;
|
|
45
|
+
if (!isPrimitiveType(schema.type)) return false;
|
|
46
|
+
for (const k of Object.keys(schema)) {
|
|
47
|
+
if (!TIER0_PRIMITIVE_ALLOWED.has(k)) return false;
|
|
48
|
+
}
|
|
49
|
+
if (schema.enum !== undefined) {
|
|
50
|
+
if (!Array.isArray(schema.enum)) return false;
|
|
51
|
+
if (schema.enum.length === 0 || schema.enum.length > MAX_TIER0_ENUM) return false;
|
|
52
|
+
for (const v of schema.enum) {
|
|
53
|
+
if (!isPrimitiveEnumValue(v)) return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (schema.const !== undefined && !isPrimitiveEnumValue(schema.const)) return false;
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isTier0Object(schema) {
|
|
61
|
+
if (schema.type !== 'object') return false;
|
|
62
|
+
for (const k of Object.keys(schema)) {
|
|
63
|
+
if (!TIER0_OBJECT_ALLOWED.has(k)) return false;
|
|
64
|
+
}
|
|
65
|
+
const ap = schema.additionalProperties;
|
|
66
|
+
if (ap !== undefined && ap !== true && ap !== false) return false;
|
|
67
|
+
if (schema.required !== undefined) {
|
|
68
|
+
if (!Array.isArray(schema.required)) return false;
|
|
69
|
+
for (const r of schema.required) if (typeof r !== 'string') return false;
|
|
70
|
+
}
|
|
71
|
+
const props = schema.properties;
|
|
72
|
+
if (props === undefined) return true;
|
|
73
|
+
if (typeof props !== 'object' || props === null || Array.isArray(props)) return false;
|
|
74
|
+
const keys = Object.keys(props);
|
|
75
|
+
if (keys.length > MAX_TIER0_PROPS) return false;
|
|
76
|
+
for (const k of keys) {
|
|
77
|
+
if (!isTier0Primitive(props[k])) return false;
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function classify(schema) {
|
|
83
|
+
if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) {
|
|
84
|
+
return { tier: 2, plan: null };
|
|
85
|
+
}
|
|
86
|
+
if (isTier0Primitive(schema)) return { tier: 0, plan: null };
|
|
87
|
+
if (isTier0Object(schema)) return { tier: 0, plan: null };
|
|
88
|
+
return { tier: 2, plan: null };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
classify,
|
|
93
|
+
MAX_TIER0_PROPS,
|
|
94
|
+
MAX_TIER0_ENUM,
|
|
95
|
+
PRIMITIVE_TYPES,
|
|
96
|
+
};
|
package/lib/tier0.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Tier 0 fast path: shared parametric validator for simple schemas.
|
|
4
|
+
// All tier-0 Validators call the same tier0Validate() function;
|
|
5
|
+
// the per-instance difference is the plan object.
|
|
6
|
+
// V8 sees one function with monomorphic hidden classes and JIT-compiles it once.
|
|
7
|
+
|
|
8
|
+
const TYPE_MASK = {
|
|
9
|
+
string: 1,
|
|
10
|
+
number: 2,
|
|
11
|
+
integer: 4,
|
|
12
|
+
boolean: 8,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const T_STRING = TYPE_MASK.string;
|
|
16
|
+
const T_NUMBER = TYPE_MASK.number;
|
|
17
|
+
const T_INTEGER = TYPE_MASK.integer;
|
|
18
|
+
const T_BOOLEAN = TYPE_MASK.boolean;
|
|
19
|
+
|
|
20
|
+
// Numeric constraint flags, packed into constraint.numFlags.
|
|
21
|
+
// Using bit flags means the validator does a cheap bitwise-and instead of
|
|
22
|
+
// five Number.isNaN() calls per numeric property when only one bound is set.
|
|
23
|
+
const F_MIN = 1;
|
|
24
|
+
const F_MAX = 2;
|
|
25
|
+
const F_EXCL_MIN = 4;
|
|
26
|
+
const F_EXCL_MAX = 8;
|
|
27
|
+
const F_MULT = 16;
|
|
28
|
+
|
|
29
|
+
// Build a constraint tuple for one primitive-typed property.
|
|
30
|
+
// All fields have the same layout so every constraint shares one hidden class.
|
|
31
|
+
function primConstraint(key, propSchema) {
|
|
32
|
+
const t = propSchema.type;
|
|
33
|
+
const hasEnum = Array.isArray(propSchema.enum);
|
|
34
|
+
const hasConst = propSchema.const !== undefined;
|
|
35
|
+
let numFlags = 0;
|
|
36
|
+
if (typeof propSchema.minimum === 'number') numFlags |= F_MIN;
|
|
37
|
+
if (typeof propSchema.maximum === 'number') numFlags |= F_MAX;
|
|
38
|
+
if (typeof propSchema.exclusiveMinimum === 'number') numFlags |= F_EXCL_MIN;
|
|
39
|
+
if (typeof propSchema.exclusiveMaximum === 'number') numFlags |= F_EXCL_MAX;
|
|
40
|
+
if (typeof propSchema.multipleOf === 'number') numFlags |= F_MULT;
|
|
41
|
+
return {
|
|
42
|
+
key,
|
|
43
|
+
typeMask: TYPE_MASK[t] | 0,
|
|
44
|
+
numFlags,
|
|
45
|
+
hasEnum,
|
|
46
|
+
hasConst,
|
|
47
|
+
enumSet: hasEnum ? new Set(propSchema.enum) : null,
|
|
48
|
+
constVal: hasConst ? propSchema.const : undefined,
|
|
49
|
+
minLen: typeof propSchema.minLength === 'number' ? propSchema.minLength : -1,
|
|
50
|
+
maxLen: typeof propSchema.maxLength === 'number' ? propSchema.maxLength : -1,
|
|
51
|
+
min: typeof propSchema.minimum === 'number' ? propSchema.minimum : 0,
|
|
52
|
+
max: typeof propSchema.maximum === 'number' ? propSchema.maximum : 0,
|
|
53
|
+
exclMin: typeof propSchema.exclusiveMinimum === 'number' ? propSchema.exclusiveMinimum : 0,
|
|
54
|
+
exclMax: typeof propSchema.exclusiveMaximum === 'number' ? propSchema.exclusiveMaximum : 0,
|
|
55
|
+
multipleOf: typeof propSchema.multipleOf === 'number' ? propSchema.multipleOf : 0,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildTier0Plan(schema) {
|
|
60
|
+
if (schema.type !== 'object') {
|
|
61
|
+
return {
|
|
62
|
+
isPrimitive: true,
|
|
63
|
+
constraints: [primConstraint('__root__', schema)],
|
|
64
|
+
requiredMask: 0,
|
|
65
|
+
additionalAllowed: true,
|
|
66
|
+
knownKeys: null,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const props = schema.properties || {};
|
|
70
|
+
const keys = Object.keys(props);
|
|
71
|
+
const required = schema.required ? new Set(schema.required) : null;
|
|
72
|
+
const constraints = new Array(keys.length);
|
|
73
|
+
const knownKeys = new Set();
|
|
74
|
+
let requiredMask = 0;
|
|
75
|
+
for (let i = 0; i < keys.length; i++) {
|
|
76
|
+
const k = keys[i];
|
|
77
|
+
constraints[i] = primConstraint(k, props[k]);
|
|
78
|
+
knownKeys.add(k);
|
|
79
|
+
if (required && required.has(k)) requiredMask |= (1 << i);
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
isPrimitive: false,
|
|
83
|
+
constraints,
|
|
84
|
+
requiredMask,
|
|
85
|
+
additionalAllowed: schema.additionalProperties !== false,
|
|
86
|
+
knownKeys,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// checkPrimitive stays exported for Tier 1 reuse.
|
|
91
|
+
function checkPrimitive(c, v) {
|
|
92
|
+
const m = c.typeMask;
|
|
93
|
+
if (m === T_STRING) {
|
|
94
|
+
if (typeof v !== 'string') return false;
|
|
95
|
+
const minLen = c.minLen;
|
|
96
|
+
const maxLen = c.maxLen;
|
|
97
|
+
if (minLen >= 0 && v.length < minLen) return false;
|
|
98
|
+
if (maxLen >= 0 && v.length > maxLen) return false;
|
|
99
|
+
} else if (m === T_INTEGER) {
|
|
100
|
+
if (typeof v !== 'number' || !Number.isInteger(v)) return false;
|
|
101
|
+
const f = c.numFlags;
|
|
102
|
+
if (f !== 0) {
|
|
103
|
+
if ((f & F_MIN) && v < c.min) return false;
|
|
104
|
+
if ((f & F_MAX) && v > c.max) return false;
|
|
105
|
+
if ((f & F_EXCL_MIN) && v <= c.exclMin) return false;
|
|
106
|
+
if ((f & F_EXCL_MAX) && v >= c.exclMax) return false;
|
|
107
|
+
if ((f & F_MULT) && v % c.multipleOf !== 0) return false;
|
|
108
|
+
}
|
|
109
|
+
} else if (m === T_NUMBER) {
|
|
110
|
+
if (typeof v !== 'number') return false;
|
|
111
|
+
const f = c.numFlags;
|
|
112
|
+
if (f !== 0) {
|
|
113
|
+
if ((f & F_MIN) && v < c.min) return false;
|
|
114
|
+
if ((f & F_MAX) && v > c.max) return false;
|
|
115
|
+
if ((f & F_EXCL_MIN) && v <= c.exclMin) return false;
|
|
116
|
+
if ((f & F_EXCL_MAX) && v >= c.exclMax) return false;
|
|
117
|
+
if ((f & F_MULT) && v % c.multipleOf !== 0) return false;
|
|
118
|
+
}
|
|
119
|
+
} else if (m === T_BOOLEAN) {
|
|
120
|
+
if (typeof v !== 'boolean') return false;
|
|
121
|
+
} else {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
if (c.hasEnum && !c.enumSet.has(v)) return false;
|
|
125
|
+
if (c.hasConst && v !== c.constVal) return false;
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Inlined object validator. Separating the primitive path removes a dead
|
|
130
|
+
// branch from the hot object path.
|
|
131
|
+
function tier0ValidateObject(plan, data) {
|
|
132
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data)) return false;
|
|
133
|
+
const cs = plan.constraints;
|
|
134
|
+
const n = cs.length;
|
|
135
|
+
const reqMask = plan.requiredMask;
|
|
136
|
+
let seenMask = 0;
|
|
137
|
+
for (let i = 0; i < n; i++) {
|
|
138
|
+
const c = cs[i];
|
|
139
|
+
const v = data[c.key];
|
|
140
|
+
if (v === undefined) {
|
|
141
|
+
if (reqMask & (1 << i)) return false;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
seenMask |= (1 << i);
|
|
145
|
+
// Inlined type + constraint check
|
|
146
|
+
const m = c.typeMask;
|
|
147
|
+
if (m === T_STRING) {
|
|
148
|
+
if (typeof v !== 'string') return false;
|
|
149
|
+
const minLen = c.minLen;
|
|
150
|
+
const maxLen = c.maxLen;
|
|
151
|
+
if (minLen >= 0 && v.length < minLen) return false;
|
|
152
|
+
if (maxLen >= 0 && v.length > maxLen) return false;
|
|
153
|
+
} else if (m === T_INTEGER) {
|
|
154
|
+
if (typeof v !== 'number' || !Number.isInteger(v)) return false;
|
|
155
|
+
const f = c.numFlags;
|
|
156
|
+
if (f !== 0) {
|
|
157
|
+
if ((f & F_MIN) && v < c.min) return false;
|
|
158
|
+
if ((f & F_MAX) && v > c.max) return false;
|
|
159
|
+
if ((f & F_EXCL_MIN) && v <= c.exclMin) return false;
|
|
160
|
+
if ((f & F_EXCL_MAX) && v >= c.exclMax) return false;
|
|
161
|
+
if ((f & F_MULT) && v % c.multipleOf !== 0) return false;
|
|
162
|
+
}
|
|
163
|
+
} else if (m === T_NUMBER) {
|
|
164
|
+
if (typeof v !== 'number') return false;
|
|
165
|
+
const f = c.numFlags;
|
|
166
|
+
if (f !== 0) {
|
|
167
|
+
if ((f & F_MIN) && v < c.min) return false;
|
|
168
|
+
if ((f & F_MAX) && v > c.max) return false;
|
|
169
|
+
if ((f & F_EXCL_MIN) && v <= c.exclMin) return false;
|
|
170
|
+
if ((f & F_EXCL_MAX) && v >= c.exclMax) return false;
|
|
171
|
+
if ((f & F_MULT) && v % c.multipleOf !== 0) return false;
|
|
172
|
+
}
|
|
173
|
+
} else if (m === T_BOOLEAN) {
|
|
174
|
+
if (typeof v !== 'boolean') return false;
|
|
175
|
+
} else {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
if (c.hasEnum && !c.enumSet.has(v)) return false;
|
|
179
|
+
if (c.hasConst && v !== c.constVal) return false;
|
|
180
|
+
}
|
|
181
|
+
if ((seenMask & reqMask) !== reqMask) return false;
|
|
182
|
+
if (!plan.additionalAllowed) {
|
|
183
|
+
const known = plan.knownKeys;
|
|
184
|
+
for (const k in data) {
|
|
185
|
+
if (!Object.prototype.hasOwnProperty.call(data, k)) continue;
|
|
186
|
+
if (!known.has(k)) return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function tier0Validate(plan, data) {
|
|
193
|
+
if (plan.isPrimitive) return checkPrimitive(plan.constraints[0], data);
|
|
194
|
+
return tier0ValidateObject(plan, data);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = {
|
|
198
|
+
buildTier0Plan,
|
|
199
|
+
tier0Validate,
|
|
200
|
+
tier0ValidateObject,
|
|
201
|
+
checkPrimitive,
|
|
202
|
+
TYPE_MASK,
|
|
203
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Ultra-fast JSON Schema validator. 4.7x faster validation, 1,800x 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",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|